Rebasely Beta: data may be purged

examples/MCP-tools Public

Markdown · 41 lines · 2.6 KiB · at 2090b14

---id: http-transporttype: conceptowner: justincomponent: mcp-serverdescribes:  - src/index.ts  - src/worker.tsreviewed: 2026-10-15---# The HTTP transport`src/index.ts` is the Worker entry: it points `fetch` at `handle` in `src/worker.ts` and exports the `RateLimiter` Durable Object class. Keeping the handler out of the entry file means the tests import it under plain Node, without `cloudflare:workers`.## Routes| Route | What it does || --- | --- || `POST /mcp` | One JSON-RPC message in, one JSON response out. || `OPTIONS /mcp` | CORS preflight, 204. || `GET`, `DELETE /mcp` | 405: there are no sessions and no standalone SSE stream. || `GET /stats` | Usage counts; see [[usage-counts]]. || `GET /` | A one-line note saying where the endpoint is. |## A requestFor each `POST /mcp`, `serveMcp` builds a new `McpServer` with `createServer`, connects it to a new `WebStandardStreamableHTTPServerTransport` with `sessionIdGenerator: undefined` and `enableJsonResponse: true`, and returns whatever `handleRequest` produces. The transport checks the `Accept` header (clients must accept both `application/json` and `text/event-stream`), parses the body, and the response is plain JSON rather than an SSE stream. Nothing survives the request, so any isolate in any location can answer the next one. [[0001-stateless-streamable-http]] has the reasoning.A stateless transport cannot be reused: the SDK throws if `handleRequest` is called twice on one. Building a server per request is cheap (three tool registrations), so we do not try to pool them.## CORSEvery `/mcp` and `/stats` response carries `access-control-allow-origin: *` and allows the MCP headers (`mcp-protocol-version`, `mcp-session-id`, `last-event-id`). The server has no cookies or credentials, so an open origin costs nothing and lets the MCP Inspector and web agents connect directly.## Rate limit`createServer` takes an `admit` hook, asked once per tool call after the arguments pass the input schema. The Worker answers it from the caller's `RateLimiter` object, named by the `cf-connecting-ip` header. A refusal comes back to the model as an `isError` result saying how many seconds to wait; `initialize` and `tools/list` are never charged. If the Durable Object call throws, the call is admitted and the error logged. [[rate-limits]] covers the bucket itself.## Usage counting`createServer` also takes an `onCall` hook, called only for calls that were admitted. The Worker hands it a function that passes the KV write to `ctx.waitUntil`, so the count happens after the response has gone and a KV error is logged rather than returned.