When your MCP server is wrapped with OAuthProvider from @cloudflare/workers-oauth-provider, authenticated user information is available inside tools via getMcpAuthContext():
McpAgent requires us to define 2 bits, server and init().init() is the initialization logic that runs every time our MCP server is started (each client session goes to a different Agent instance).
In there you will normally setup all your tools/resources and anything else you might need. In this case, we are only setting the tool square.That was just the McpAgent, but we still need a Worker to route requests to our MCP server. McpAgent exports a static method that deals with that for you. That is what TinyMcp.serve(...) is for.
It returns an object with a fetch handler that can act as our Worker entrypoint and deal with the Streamable HTTP transport for us, so we can deploy our MCP directly!
It is a very simple MCP indeed, but you can get a feel of how fast you can get a server up and running. You can deploy this worker and test your MCP with any client.
To get a feel of what a more realistic MCP might look like, let’s deploy an MCP that lets anyone that knows our secret password access a shared R2 bucket.
This is an example of a custom authorization flow. Do not use this in production.
1
Define the McpAgent
Create your McpAgent with tools that interact with R2:
import { McpAgent } from "agents/mcp";import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";import { z } from "zod";import { env } from "cloudflare:workers";export class StorageMcp extends McpAgent { server = new McpServer({ name: "", version: "v1.0.0" }); async init() { const textRes = (text: string) => ({ content: [{ type: "text" as const, text }] }); this.server.registerTool( "writeFile", { description: "Store text as a file with the given path", inputSchema: { path: z.string().describe("Absolute path of the file"), content: z.string().describe("The content to store") } }, async ({ path, content }) => { try { await env.BUCKET.put(path, content); return textRes(`Successfully stored contents to ${path}`); } catch (e: unknown) { return textRes(`Couldn't save to file. Found error ${e}`); } } ); this.server.registerTool( "readFile", { description: "Read the contents of a file", inputSchema: { path: z.string().describe("Absolute path of the file to read") } }, async ({ path }) => { const obj = await env.BUCKET.get(path); if (!obj || !obj.body) return textRes(`Error reading file at ${path}: not found`); try { return textRes(await obj.text()); } catch (e: unknown) { return textRes(`Error reading file at ${path}: ${e}`); } } ); this.server.registerTool( "whoami", { description: "Check who the user is" }, async () => { return textRes(`${this.props?.userId}`); } ); }}
2
Create the OAuth flow
Build a simple password-based authentication flow:
McpAgent supports specifying a data jurisdiction for your MCP server, which is particularly useful for satisfying GDPR and other data residency regulations.
To comply with GDPR requirements, you can specify the "eu" jurisdiction to ensure that all data processed by your MCP server remains within the European Union:
MCP servers can request additional input from the user during a tool call using elicitation. This is useful for confirmation dialogs, requesting amounts, or any interactive tool flow.Elicitation is supported via McpAgent (which manages the request/response lifecycle through Durable Object storage) or via WorkerTransport (for stateful non-McpAgent setups).
import { McpAgent } from "agents/mcp";import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";import { z } from "zod";export class MyMCP extends McpAgent<Env, { counter: number }> { server = new McpServer({ name: "Elicitation Demo", version: "1.0.0" }); initialState = { counter: 0 }; async init() { this.server.registerTool( "increase-counter", { description: "Increase the counter", inputSchema: { confirm: z.boolean().describe("Do you want to increase the counter?") } }, async ({ confirm }, extra) => { if (!confirm) { return { content: [{ type: "text", text: "Cancelled." }] }; } const result = await this.server.server.elicitInput( { message: "By how much?", requestedSchema: { type: "object", properties: { amount: { type: "number", title: "Amount" } }, required: ["amount"] } }, { relatedRequestId: extra.requestId } ); if (result.action !== "accept" || !result.content?.amount) { return { content: [{ type: "text", text: "Cancelled." }] }; } const amount = Number(result.content.amount); this.setState({ counter: this.state.counter + amount }); return { content: [ { type: "text", text: `Counter increased by ${amount}, now ${this.state.counter}` } ] }; } ); }}export default MyMCP.serve("/mcp");
WorkerTransport is a server-side transport for running MCP servers in stateless Workers while optionally persisting session state. It is used internally by createMcpHandler() but can also be used directly for advanced scenarios like stateful sessions without McpAgent.
import { WorkerTransport, type TransportState } from "agents/mcp";const transport = new WorkerTransport({ sessionIdGenerator: () => crypto.randomUUID(), enableJsonResponse: false, storage: { get: () => kv.get<TransportState>("mcp_state"), set: (state: TransportState) => kv.put<TransportState>("mcp_state", state) }});