Rebasely Beta: data may be purged

examples/MCP-tools Public

TypeScript · 84 lines · 4.3 KiB · at dae155a

import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";import type { RateLimiter } from "./rate-limiter/index.ts";import { createServer } from "./server.ts";import { MAX_DAYS, countCall, readUsage } from "./usage.ts";export type Env = { USAGE: KVNamespace; RATE_LIMITER: DurableObjectNamespace<RateLimiter> };// 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": "GET, 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 === "/stats" && request.method === "GET") return withCors(await stats(request, env));  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.  // Counting happens after the response is sent; a KV hiccup never fails a tool call.  const client = clientOf(request);  const server = createServer({    admit: () => admit(env, client),    onCall: (tool, ok) => ctx.waitUntil(countCall(env.USAGE, tool, ok).catch((err) => console.error("usage count failed", err))),  });  const transport = new WebStandardStreamableHTTPServerTransport({ sessionIdGenerator: undefined, enableJsonResponse: true });  await server.connect(transport);  return transport.handleRequest(request);}// Who is calling, for the rate limit. Cloudflare sets cf-connecting-ip on every request that// reaches the Worker; without it (local tests, odd proxies) all such callers share one bucket.function clientOf(request: Request): string {  return request.headers.get("cf-connecting-ip") ?? "unknown";}// Spends one of the client's tokens. If the limiter itself fails, the call goes through: a// broken limit should cost us a little abuse, not every caller their answers.async function admit(env: Env, client: string): Promise<string | null> {  try {    const decision = await env.RATE_LIMITER.getByName(client).take();    if (decision.ok) return null;    const wait = decision.retryAfterSeconds;    return `Rate limit reached for this client. Wait ${wait} second${wait === 1 ? "" : "s"} before the next tool call; calling sooner will be refused the same way.`;  } catch (err) {    console.error("rate limiter failed; admitting the call", err);    return null;  }}// GET /stats?days=7: calls and errors per tool for the last few UTC days, newest first.async function stats(request: Request, env: Env): Promise<Response> {  const param = new URL(request.url).searchParams.get("days") ?? "7";  const days = Number(param);  if (!Number.isInteger(days) || days < 1 || days > MAX_DAYS) {    return Response.json({ error: `days must be a whole number from 1 to ${MAX_DAYS}` }, { status: 400 });  }  return Response.json({ days: await readUsage(env.USAGE, days) }, { headers: { "cache-control": "no-store" } });}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;}