# mcp-shared

Shared transport + auth library for the in-house MCP servers. This is the
Phase A foundation of the MCP consolidation effort. Phase B will migrate
`advancedmd-mcp` as the pilot consumer.

## What this is

A tiny library (about 300 LOC of TypeScript plus a Python port) that
provides three things:

1. `requireBearer(req, expectedToken)` -- a constant-time bearer-token
   middleware that returns either `null` (pass) or a 401 `Response`.
2. `serveMcpOverHttp({ server, port, token, ... })` -- a one-call helper
   that wires an MCP `Server` instance to a Streamable HTTP listener on
   Bun, gated by `requireBearer`, with an unauthenticated `/health`
   probe.
3. `MCP_PORTS` -- the canonical port table for in-house MCP servers,
   used to prevent collisions.

The goal: every in-house MCP server should look like

```ts
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { serveMcpOverHttp, MCP_PORTS } from "../mcp-shared";

const server = new Server(
  { name: "advancedmd", version: "0.0.1" },
  { capabilities: { tools: {} } },
);
// ... register tools ...
serveMcpOverHttp({
  server,
  port: MCP_PORTS.advancedmd,
  token: process.env.MCP_BEARER_TOKEN!,
});
```

instead of each server hand-rolling its own HTTP listener, auth check,
and SSE/Streamable HTTP wire-up.

## Port table

The `MCP_PORTS` constant in `ports.ts`:

| Service             | Port  |
| ------------------- | ----- |
| email               | 18810 |
| teams-mcp           | 18811 |
| advancedmd          | 18812 |
| ringcentral         | 18813 |
| ringcentral-admin   | 18814 |
| docstrange          | 18815 |
| rippling            | 18816 |

Already-reserved non-MCP ports in this repo (do NOT collide):

| Port  | Service                                      |
| ----- | -------------------------------------------- |
| 18800 | sendblue-channel (webhook)                   |
| 18802 | sendblue-channel (proactive)                 |
| 18803 | rc-notifications-proxy                       |
| 3978  | teams-channel (Bot Framework webhook)        |

## Funnel-route convention

When all seven MCP servers sit behind a single Cloudflare tunnel, the
edge routes look like:

```
https://mcp.example.com/email              -> :18810/mcp
https://mcp.example.com/teams-mcp          -> :18811/mcp
https://mcp.example.com/advancedmd         -> :18812/mcp
https://mcp.example.com/ringcentral        -> :18813/mcp
https://mcp.example.com/ringcentral-admin  -> :18814/mcp
https://mcp.example.com/docstrange         -> :18815/mcp
https://mcp.example.com/rippling           -> :18816/mcp
```

Each backend always serves `/mcp` as its primary route; the edge does
the path rewrite. `/health` is also exposed by every backend and is
unauthenticated, so the tunnel layer can probe liveness without
distributing the bearer token.

## Routes

Every server using `serveMcpOverHttp` exposes:

| Path        | Method | Auth     | Purpose                              |
| ----------- | ------ | -------- | ------------------------------------ |
| /health     | GET    | none     | liveness, returns `{ok:true,...}`    |
| /mcp        | ANY    | bearer   | Streamable HTTP MCP transport        |

Legacy SSE (`/sse` + `/messages`) is **not supported in Phase A**.
Passing `legacySse: true` will throw at startup. An earlier draft
shipped a placeholder bridge that returned a JSON-RPC `-32601` for
every dispatched tool call -- worse than a 404, because a client
would connect via `/sse`, receive the `endpoint` event, and then
fail every subsequent tool call. A real second-Server-backed legacy
path will land in a follow-up PR.

## attachTo mode

For servers that already run a Bun HTTP listener (e.g. `teams-channel`
with its Bot Framework webhook on :3978, or `email-channel` with its
webhook), pass `attachTo` and `serveMcpOverHttp` will return a composed
fetch handler instead of binding its own port:

```ts
const handler = serveMcpOverHttp({
  server,
  port: 0, // ignored in attachTo mode
  token: process.env.MCP_BEARER_TOKEN!,
  attachTo: existingWebhookHandler,
});
Bun.serve({ port: 3978, fetch: handler.fetch });
```

The same-process two-port style (one port for the webhook, one for
MCP) is also fine -- `attachTo` is just an option, not the default.

## Logs

The transport logs only:

- one line on bind success
- bearer-failure counts, rate-limited to one line per minute

No request paths, no header values, no token material.

## Python

A parallel implementation lives in `python/` for the two MCP servers
written in Python (`ringcentral-admin`, `docstrange-mcp`). See
`python/README.md`.

## Testing

```
bun test tools/mcp-shared
```

The test suite covers `requireBearer` behaviour (eight cases including
constant-time-compare sanity) and the transport end-to-end (boot, auth
gate, `/health`, Streamable HTTP round trip, legacy SSE, and the
`attachTo` fall-through).
