Rebasely Beta: data may be purged

examples/MCP-tools Public

Serve the server over Streamable HTTP from a Worker

Uses the SDK's Web-standard transport in stateless mode with JSON responses: a new server and transport per POST, so any isolate can answer any request. GET and DELETE get a 405 because there are no sessions, and CORS is open so browser clients such as the MCP Inspector can connect directly.
ee5ceb565dd6ffdd7a7298815d356d4f42f718dc
Parent a82d7bc Browse filesHistory

Changed files

6 files changed112 additions2 deletionsagainst a82d7bc

  1. ChangedREADME.md+2−1
  2. Addedsrc/index.ts+5−0
  3. Changedsrc/server.ts+1−1
  4. Addedsrc/worker.ts+46−0
  5. Addedtest/worker.test.ts+51−0
  6. Addedwrangler.jsonc+7−0
ChangedREADME.md+2−1
@@ -1,8 +1,9 @@
# mcp-tools
-A small MCP server with exact, read-only helpers for models. It starts with `convert_units`.
+A small MCP server with exact, read-only helpers for models, running as a Cloudflare Worker. It serves MCP over Streamable HTTP at `/mcp`.
```sh
npm install
npm test
+npm run dev # http://localhost:8787/mcp
```
View file
Addedsrc/index.ts+5−0
@@ -0,0 +1,5 @@
+// The Worker's entry point. The handler lives in worker.ts so tests can import it without
+// anything that only loads in workerd.
+import { handle, type Env } from "./worker.ts";
+
+export default { fetch: handle } satisfies ExportedHandler<Env>;
View file
Changedsrc/server.ts+1−1
@@ -15,7 +15,7 @@
};
// Builds an MCP server with every tool registered. The server holds no state between requests,
-// so a caller can make one per request, and each test makes its own.
+// so src/worker.ts makes one per request and each test makes its own.
export function createServer(options: ServerOptions = {}): McpServer {
const server = new McpServer(SERVER_INFO, {
instructions: "Small, exact helpers: unit conversion. Each tool is read-only and answers from its input alone.",
View file
Addedsrc/worker.ts+46−0
@@ -0,0 +1,46 @@
+import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
+import { createServer } from "./server.ts";
+
+export type Env = Record<string, unknown>;
+
+// Browser clients (the MCP Inspector in direct mode, web agents) call /mcp cross-origin. The
+// server has no cookies or credentials to protect, so any origin may call it.
+const CORS = {
+ "access-control-allow-origin": "*",
+ "access-control-allow-methods": "POST, OPTIONS",
+ "access-control-allow-headers": "content-type, accept, authorization, mcp-protocol-version, mcp-session-id, last-event-id",
+ "access-control-expose-headers": "mcp-session-id, mcp-protocol-version",
+ "access-control-max-age": "86400",
+};
+
+export async function handle(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
+ const { pathname } = new URL(request.url);
+ if (pathname === "/mcp") return withCors(await serveMcp(request, env, ctx));
+ if (pathname === "/" && request.method === "GET") {
+ return new Response("mcp-tools: an MCP server. Point a client at /mcp (Streamable HTTP).\n", { headers: { "content-type": "text/plain; charset=utf-8" } });
+ }
+ return new Response("Not found\n", { status: 404 });
+}
+
+async function serveMcp(request: Request, _env: Env, _ctx: ExecutionContext): Promise<Response> {
+ if (request.method === "OPTIONS") return new Response(null, { status: 204 });
+ // Stateless: no sessions, so there is no standalone SSE stream to GET and nothing to DELETE.
+ if (request.method !== "POST") {
+ return Response.json(
+ { jsonrpc: "2.0", error: { code: -32000, message: "This server is stateless: send JSON-RPC requests with POST." }, id: null },
+ { status: 405, headers: { allow: "POST, OPTIONS" } },
+ );
+ }
+ // A fresh server and transport for every request. Nothing carries over, so any isolate can
+ // answer any request and there is no session to lose.
+ const server = createServer();
+ const transport = new WebStandardStreamableHTTPServerTransport({ sessionIdGenerator: undefined, enableJsonResponse: true });
+ await server.connect(transport);
+ return transport.handleRequest(request);
+}
+
+function withCors(response: Response): Response {
+ const out = new Response(response.body, response);
+ for (const [k, v] of Object.entries(CORS)) out.headers.set(k, v);
+ return out;
+}
View file
Addedtest/worker.test.ts+51−0
@@ -0,0 +1,51 @@
+import assert from "node:assert/strict";
+import { test } from "node:test";
+import { handle, type Env } from "../src/worker.ts";
+
+const env: Env = {};
+const ctx = { waitUntil() {}, passThroughOnException() {}, props: {} } as unknown as ExecutionContext;
+
+function rpc(method: string, params?: unknown, id = 1): Request {
+ return new Request("http://localhost/mcp", {
+ method: "POST",
+ headers: { "content-type": "application/json", accept: "application/json, text/event-stream", "mcp-protocol-version": "2025-06-18" },
+ body: JSON.stringify({ jsonrpc: "2.0", id, method, ...(params ? { params } : {}) }),
+ });
+}
+
+test("POST /mcp answers initialize as JSON", async () => {
+ const res = await handle(rpc("initialize", { protocolVersion: "2025-06-18", capabilities: {}, clientInfo: { name: "test", version: "0" } }), env, ctx);
+ assert.equal(res.status, 200);
+ assert.match(res.headers.get("content-type") ?? "", /application\/json/);
+ assert.equal(res.headers.get("mcp-session-id"), null);
+ const body = (await res.json()) as { result: { serverInfo: { name: string } } };
+ assert.equal(body.result.serverInfo.name, "mcp-tools");
+});
+
+test("each request gets its own server, so tools/call needs no session", async () => {
+ const res = await handle(rpc("tools/call", { name: "convert_units", arguments: { value: 1, from: "ft", to: "in" } }), env, ctx);
+ const body = (await res.json()) as { result: { structuredContent: { result: number } } };
+ assert.equal(body.result.structuredContent.result, 12);
+});
+
+test("GET and DELETE on /mcp are refused with 405", async () => {
+ for (const method of ["GET", "DELETE"]) {
+ const res = await handle(new Request("http://localhost/mcp", { method }), env, ctx);
+ assert.equal(res.status, 405);
+ assert.equal(res.headers.get("allow"), "POST, OPTIONS");
+ }
+});
+
+test("CORS preflights succeed and responses carry the CORS headers", async () => {
+ const pre = await handle(new Request("http://localhost/mcp", { method: "OPTIONS" }), env, ctx);
+ assert.equal(pre.status, 204);
+ assert.equal(pre.headers.get("access-control-allow-origin"), "*");
+ assert.match(pre.headers.get("access-control-allow-headers") ?? "", /mcp-protocol-version/);
+ const res = await handle(rpc("tools/list"), env, ctx);
+ assert.equal(res.headers.get("access-control-allow-origin"), "*");
+});
+
+test("other paths are 404", async () => {
+ const res = await handle(new Request("http://localhost/nope"), env, ctx);
+ assert.equal(res.status, 404);
+});
View file
Addedwrangler.jsonc+7−0
@@ -0,0 +1,7 @@
+// mcp-tools: the MCP server, at /mcp. Run it locally with `npm run dev`.
+{
+ "name": "mcp-tools",
+ "main": "src/index.ts",
+ "compatibility_date": "2026-09-01",
+ "observability": { "enabled": true }
+}
View file

Checks

No checks reported for this commit.

Runs

No runs for this commit.