Skip to main content
Glama
README.md
# Composio MCP Bridge

A remote MCP (Model Context Protocol) server for [Poke](https://poke.com) (or
any MCP client) that proxies tool calls straight through to **the caller's
own 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. The
server 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, or
written to disk.

## Auth flow

1. **You** sign up at [composio.dev](https://composio.dev) and grab an API
   key from Settings → API Keys. That's your own Composio account, your own
   quota, your own connected apps.
2. When setting up the custom MCP integration in Poke, set the **Server URL**
   to:
   ```
   https://<your-deployment>/mcp?composio_api_key=YOUR_COMPOSIO_KEY
   ```
   (Query param, because Poke's custom MCP integrations can't reliably send
   custom HTTP headers. If your MCP client *can* send headers, `Authorization:
   Bearer <key>` or `x-composio-api-key: <key>` both work too — checked in
   that order, header wins if both are present.)
3. From here on, every tool call this server makes runs against *your*
   Composio 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. Show
     that link in chat; once the user finishes sign-in, the connection lives
     in your Composio account.
   - `list_connections` — see what's already connected.
   - `get_tools` — list the exact Composio tool slugs available for a
     toolkit (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 fresh
     Connect Link instead of a raw error, so the agent can hand it straight
     to the user without a second round trip.
4. **Multiple people under one key** (optional): pass an `x-composio-user-id`
   header or `user_id` query param to separate multiple end users inside the
   same Composio account/project. If you don't set one, everything uses a
   single `default` user id — the common case for one person automating
   their own accounts.

## Why this shape

- **No server-side secrets to steal.** The server's own `.env` has zero
  Composio credentials in it — see `.env.example`. If this deployment were
  ever compromised, there's no shared master key sitting in it to leak.
- **True multi-tenant for free.** Anyone with their own Composio key can
  point their own Poke integration at the same deployed URL and get their
  own isolated set of connections — no per-user database, no OAuth app
  registered under your name.
- **Real MCP SDK, not hand-rolled JSON-RPC.** Built on
  `@modelcontextprotocol/sdk`'s `StreamableHTTPServerTransport` in stateless
  mode — a fresh `McpServer` + transport per request, torn down right after.
  This means it correctly speaks `initialize`, `tools/list`, `tools/call`,
  and `ping` per spec, instead of reimplementing the protocol by hand.

## Security

- **Never logged, cached, or persisted.** The API key only ever exists in
  request-scoped memory, for the lifetime of the one call it arrived with.
- **Format validation.** Keys are checked against a safe length/charset
  pattern before ever being used, so obviously malformed input gets a clean
  400 instead of hitting Composio.
- **Rate limiting per key.** An in-memory limiter keyed by a SHA-256
  fingerprint of the API key (never the raw key itself) caps requests per
  rolling minute (`RATE_LIMIT_PER_MINUTE`, default 30). This is best-effort:
  fine on a single long-running process (Railway/Fly.io); on serverless
  platforms with multiple isolates (Vercel) each isolate keeps its own
  counters, so for a hard cross-instance guarantee swap this for a shared
  store like Upstash Redis.
- **HTTPS only in production.** Requests arriving over plain HTTP (checked
  via `x-forwarded-proto`) get rejected with 400, unless `ALLOW_INSECURE=true`
  for local testing.
- **Clean error messages, no stack traces.** Bad/expired keys, 429s from
  Composio, 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)
```bash
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
```bash
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 server
behavior 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 straight
through (e.g. `GOOGLESHEETS_BATCH_GET`, `GMAIL_SEND_EMAIL`). Always call
`get_tools` for a toolkit first to confirm the current slug names and
expected arguments — Composio's own catalog is the source of truth, and
slugs occasionally get renamed as toolkits evolve.