Composio MCP Bridge
A remote MCP (Model Context Protocol) server for Poke (orany MCP client) that proxies tool calls straight through to the caller'sown Composio account — bring-your-own-key, not a shared quota.
Poke agent --(your Composio API key, every request)--> this server --(fresh Composio client)--> Composio --> Gmail/Slack/Sheets/etc
There is no "connect your account to us" step and no shared Composio project.Each person supplies their own Composio API key on every single request. Theserver builds a brand new Composio SDK client for that one request, uses it,and throws it away — nothing about the key is ever logged, cached, orwritten to disk.
Auth flow
- You sign up at composio.dev and grab an APIkey from Settings → API Keys. That's your own Composio account, your ownquota, your own connected apps.
- When setting up the custom MCP integration in Poke, set the Server URLto:
(Query param, because Poke's custom MCP integrations can't reliably sendcustom HTTP headers. If your MCP client can send headers,https://<your-deployment>/mcp?composio_api_key=YOUR_COMPOSIO_KEYAuthorization: Bearer <key>orx-composio-api-key: <key>both work too — checked inthat order, header wins if both are present.) - From here on, every tool call this server makes runs against yourComposio account:
list_toolkits— see what apps Composio supports (Gmail, Slack,GitHub, Google Sheets, …).connect_toolkit— get a Composio Connect Link (a hosted Google/Slack/etc sign-in URL) for a toolkit you haven't connected yet. Showthat link in chat; once the user finishes sign-in, the connection livesin your Composio account.list_connections— see what's already connected.get_tools— list the exact Composio tool slugs available for atoolkit (and what arguments they expect).execute_tool— actually run one, e.g.tool_slug: "GMAIL_SEND_EMAIL".If the toolkit isn't connected yet, this automatically returns a freshConnect Link instead of a raw error, so the agent can hand it straightto the user without a second round trip.
- Multiple people under one key (optional): pass an
x-composio-user-idheader oruser_idquery param to separate multiple end users inside thesame Composio account/project. If you don't set one, everything uses asingledefaultuser id — the common case for one person automatingtheir own accounts.
Why this shape
- No server-side secrets to steal. The server's own
.envhas zeroComposio credentials in it — see.env.example. If this deployment wereever compromised, there's no shared master key sitting in it to leak. - True multi-tenant for free. Anyone with their own Composio key canpoint their own Poke integration at the same deployed URL and get theirown isolated set of connections — no per-user database, no OAuth appregistered under your name.
- Real MCP SDK, not hand-rolled JSON-RPC. Built on
@modelcontextprotocol/sdk'sStreamableHTTPServerTransportin statelessmode — a freshMcpServer+ transport per request, torn down right after.This means it correctly speaksinitialize,tools/list,tools/call,andpingper spec, instead of reimplementing the protocol by hand.
Security
- Never logged, cached, or persisted. The API key only ever exists inrequest-scoped memory, for the lifetime of the one call it arrived with.
- Format validation. Keys are checked against a safe length/charsetpattern before ever being used, so obviously malformed input gets a clean400 instead of hitting Composio.
- Rate limiting per key. An in-memory limiter keyed by a SHA-256fingerprint of the API key (never the raw key itself) caps requests perrolling minute (
RATE_LIMIT_PER_MINUTE, default 30). This is best-effort:fine on a single long-running process (Railway/Fly.io); on serverlessplatforms with multiple isolates (Vercel) each isolate keeps its owncounters, so for a hard cross-instance guarantee swap this for a sharedstore like Upstash Redis. - HTTPS only in production. Requests arriving over plain HTTP (checkedvia
x-forwarded-proto) get rejected with 400, unlessALLOW_INSECURE=truefor local testing. - Clean error messages, no stack traces. Bad/expired keys, 429s fromComposio, and unexpected failures are all mapped to short, safe messages —see
src/errors.ts. Internal exceptions never reach the client verbatim.
Project layout
src/
server.ts entrypoint for standalone Express (Railway/Fly.io/local)
app.ts Express app: HTTPS check, key extraction, rate limit, MCP endpoint
tools.ts the 5 MCP tools, all proxying to the per-request Composio client
composio.ts creates a fresh Composio SDK client per request
keyAuth.ts pulls the API key + optional user id off the request
security.ts key fingerprinting, log scrubbing, rate limiter
errors.ts maps Composio/axios errors to clean user-facing messages
api/mcp.ts Vercel serverless adapter (reuses src/app.ts)
Deploying
Railway or Fly.io (standalone Express)
npm install
npm run build
npm start # or let the platform run `npm run build && npm start`
A Dockerfile and fly.toml are included for Fly.io (fly launch, fly deploy).Railway auto-detects Node from package.json, no extra config needed.
Your MCP endpoint will be: https://<your-app-domain>/mcp
Vercel
npm i -g vercel
vercel
vercel --prod
vercel.json rewrites /mcp → /api/mcp, so the endpoint is still just:https://<your-vercel-domain>/mcp
Environment variables
See .env.example. There are no per-user secrets here — only serverbehavior config:
| Var | Purpose |
|---|---|
PORT |
Port for the standalone server (ignored on Vercel) |
NODE_ENV |
Set to production to enable the HTTPS-only check |
RATE_LIMIT_PER_MINUTE |
Max requests per key per rolling minute (default 30) |
ALLOW_INSECURE |
Set true only for local HTTP testing |
A note on tool slugs
execute_tool and get_tools pass exact Composio tool slugs straightthrough (e.g. GOOGLESHEETS_BATCH_GET, GMAIL_SEND_EMAIL). Always callget_tools for a toolkit first to confirm the current slug names andexpected arguments — Composio's own catalog is the source of truth, andslugs occasionally get renamed as toolkits evolve.