agents 0.29.0 → 0.30.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +3 -2
  2. package/dist/browser/tools/pi.d.ts +6 -2
  3. package/dist/browser/tools/pi.js +6 -2
  4. package/dist/browser/tools/pi.js.map +1 -1
  5. package/dist/chat-sdk/index.js +1 -1
  6. package/dist/cli.js +46 -45
  7. package/dist/experimental/channels/web/index.js +1 -1
  8. package/dist/harness/container/index.js +1 -1
  9. package/dist/harness/opencode/index.js +1 -1
  10. package/dist/harness/pi/index.d.ts +60 -35
  11. package/dist/harness/pi/index.js +271 -119
  12. package/dist/harness/pi/index.js.map +1 -1
  13. package/dist/index.js +1 -1
  14. package/dist/lifecycle/index.js +1 -1
  15. package/dist/{lifecycle-ClT4DmF2.js → lifecycle-v1rKHV2C.js} +57 -17
  16. package/dist/lifecycle-v1rKHV2C.js.map +1 -0
  17. package/dist/mcp/index.js +1 -1
  18. package/dist/models/pi-ai/index.js +2 -2
  19. package/dist/models/pi-ai/index.js.map +1 -1
  20. package/dist/{src-6lr7MtM_.js → src-DfLzdwlK.js} +3 -3
  21. package/dist/{src-6lr7MtM_.js.map → src-DfLzdwlK.js.map} +1 -1
  22. package/dist/web/fetch/tools/pi.d.ts +10 -6
  23. package/dist/web/fetch/tools/pi.js +10 -6
  24. package/dist/web/fetch/tools/pi.js.map +1 -1
  25. package/dist/web/search/tools/pi.d.ts +10 -6
  26. package/dist/web/search/tools/pi.js +10 -6
  27. package/dist/web/search/tools/pi.js.map +1 -1
  28. package/dist/websockets/index.js +1 -1
  29. package/dist/{websockets-CqxnntmE.js → websockets-CyoeosdO.js} +2 -2
  30. package/dist/{websockets-CqxnntmE.js.map → websockets-CyoeosdO.js.map} +1 -1
  31. package/dist/workflows.js +1 -1
  32. package/docs/get-current-agent.md +4 -7
  33. package/docs/harnesses/pi.md +62 -28
  34. package/docs/human-in-the-loop.md +3 -7
  35. package/docs/index.md +2 -2
  36. package/docs/lifecycle.md +7 -1
  37. package/docs/mcp-client.md +2 -28
  38. package/docs/mcp-servers.md +121 -414
  39. package/docs/mcp-transports.md +30 -289
  40. package/docs/models-pi-ai.md +5 -1
  41. package/docs/routing.md +0 -2
  42. package/docs/securing-mcp-servers.md +7 -2
  43. package/package.json +11 -6
  44. package/dist/lifecycle-ClT4DmF2.js.map +0 -1
@@ -1,33 +1,17 @@
1
1
  # Creating MCP Servers
2
2
 
3
- This guide covers the different ways to create MCP servers with the Agents SDK and helps you choose the right approach.
3
+ Build an MCP server on Workers with `createMcpHandler` from `agents/mcp/server` and an MCP SDK v2 `McpServer`. The handler is stateless: every request gets a fresh server from your factory, so there is no Durable Object, session, or WebSocket to manage. It serves draft MCP `2026-07-28` and stateless 2025-era clients from the same endpoint.
4
4
 
5
- ## Choosing an Approach
6
-
7
- | Approach | Stateful? | Requires Durable Objects? | Best for |
8
- | ---------------------------------------------- | --------- | ------------------------- | ------------------------------------------------- |
9
- | `createMcpHandler()` | No | No | New servers and the draft MCP protocol |
10
- | `McpAgent` (deprecated legacy path) | Yes | Yes | Existing stateful SDK v1 deployments |
11
- | Raw `WebStandardStreamableHTTPServerTransport` | No | No | Low-level control without the Agents HTTP wrapper |
12
-
13
- - **`createMcpHandler()`** is the current server-development path. It serves draft MCP `2026-07-28` and supports stateless published 2025 clients by default.
14
- - **`McpAgent`** is a retained, feature-frozen SDK v1 path for existing stateful deployments. New servers should use `createMcpHandler()`.
15
- - **Raw transport** gives you low-level control if the standard handler lifecycle is not suitable.
16
-
17
- `McpAgent` and the other retained SDK v1 registration APIs use the upstream
18
- Zod-based schema contract. Define their tool input and output schemas as Zod
19
- shapes; AI SDK flexible-schema adapters are not accepted. For a new MCP server
20
- that needs Standard Schema support, use `@modelcontextprotocol/server` v2 with
21
- `createMcpHandler()`.
22
-
23
- ## Stateless MCP Server with `createMcpHandler()`
24
-
25
- The simplest way to create an MCP server. Install the exact SDK v2 server peer, then use the isolated server entry point so legacy Agents transports and MCP clients stay out of your Worker bundle:
5
+ ## Install
26
6
 
27
7
  ```sh
28
8
  pnpm add agents @modelcontextprotocol/server@2.3.1 zod
29
9
  ```
30
10
 
11
+ Use the exact `@modelcontextprotocol/server` version required by your installed Agents release.
12
+
13
+ ## Create a server
14
+
31
15
  ```typescript
32
16
  import { McpServer } from "@modelcontextprotocol/server";
33
17
  import { createMcpHandler } from "agents/mcp/server";
@@ -53,432 +37,171 @@ function createServer() {
53
37
  return server;
54
38
  }
55
39
 
40
+ const handler = createMcpHandler(createServer);
41
+
56
42
  export default {
57
43
  fetch(request, env, ctx) {
58
- return createMcpHandler(createServer)(request, env, ctx);
44
+ return handler(request, env, ctx);
59
45
  }
60
46
  } satisfies ExportedHandler;
61
47
  ```
62
48
 
63
- `createMcpHandler` requires a factory so concurrent Worker requests receive isolated server instances. A function input is always treated as an SDK v2 factory.
49
+ The server is served at `/mcp` by default. `agents/mcp/server` uses `AsyncLocalStorage`, so enable `nodejs_compat`:
64
50
 
65
- ### `createMcpHandler` options
66
-
67
- ```typescript
68
- createMcpHandler(() => createServer(), {
69
- route: "/mcp", // exact path to handle (default: "/mcp")
70
- corsOptions: {
71
- origin: "https://app.example.com"
72
- },
73
- allowedHostnames: ["mcp.example.com"], // custom-domain Host policy
74
- allowedOriginHostnames: ["app.example.com"], // browser Origin policy
75
- authContext: { props: {} }, // optional application props override
76
- legacy: "stateless", // upstream default; use "reject" for Stateless-only
77
- responseMode: "auto" // upstream SDK response shaping
78
- });
51
+ ```jsonc
52
+ {
53
+ "name": "hello-mcp",
54
+ "main": "src/index.ts",
55
+ "compatibility_date": "2026-06-11",
56
+ "compatibility_flags": ["nodejs_compat"]
57
+ }
79
58
  ```
80
59
 
81
- `createMcpHandler` supports upstream SDK v2 handler options except `bus`, which is not exposed by the Agents SDK. Supplying `bus` throws a `TypeError`.
82
-
83
- To publish change events, use the returned handler's `notify` methods, such as `handler.notify.toolsChanged()`. Create the handler once at module scope and reuse it for requests and notifications. Notifications are isolate-local: they do not reach subscriptions in other Worker isolates. See the [handler notification API](https://developers.cloudflare.com/agents/model-context-protocol/apis/handler-api/#publish-list-and-resource-changes) for the available methods.
84
-
85
- Use `createLegacyMcpHandler` for SDK v1 WorkerTransport, storage, session, and event-store options. The SDK v2 `bus` option is not available on that handler either.
86
-
87
- The handler validates every present `Origin` header before serving the request. It rejects malformed, opaque, and non-HTTP origins. Requests without `Origin` remain valid for non-browser MCP clients.
88
-
89
- Its default allowlist includes localhost-class origins and the endpoint's `workers.dev` hostname. A concrete `corsOptions.origin` adds that hostname automatically. The handler also applies matching Host checks to localhost and `workers.dev` endpoints. For a custom domain with wildcard CORS, configure `allowedHostnames` and `allowedOriginHostnames` explicitly. Values are hostnames without a scheme or port, and matching ignores the Origin's scheme and port.
90
-
91
- Pass `allowedOriginHostnames: "*"` only when equivalent Origin validation runs in trusted middleware before the handler. This explicitly disables the handler's Origin check, including malformed and opaque Origin rejection. MCP HTTP servers are required to validate browser Origins, so do not use this as an unauthenticated public-server shortcut.
92
-
93
- ### 2025 compatibility and elicitation
94
-
95
- The default `legacy: "stateless"` lane supports ordinary 2025 tools, resources, and prompts. It has no session return path for push-style server-to-client requests; attempts to sample, elicit, or list roots fail immediately with guidance to use a sessionful transport.
96
-
97
- Applications that must serve both generations can route before `createMcpHandler`: send Stateless requests to a strict handler and Legacy requests to an existing session-addressed Agent or transport. See [`examples/mcp-elicitation-mrtr`](../../examples/mcp-elicitation-mrtr/) for Stateless Elicitation and [`examples/mcp-elicitation`](../../examples/mcp-elicitation/) for Legacy Elicitation.
98
-
99
- For an explicit Legacy handler, use the retained Legacy API:
100
-
101
- ```typescript
102
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
103
- import { createLegacyMcpHandler } from "agents/mcp";
104
-
105
- const server = new McpServer({ name: "legacy", version: "1.0.0" });
106
- export default createLegacyMcpHandler(server);
107
- ```
60
+ `createMcpHandler` takes a factory, not a server instance, so concurrent requests never share one `McpServer`. Create the handler once at module scope and call it for each request.
108
61
 
109
- Passing an SDK v1 server directly to `createMcpHandler` still forwards to this API for compatibility, but that overload is deprecated, emits a migration warning, and is removed in the next major release. `createLegacyMcpHandler` and `WorkerTransport` themselves are not deprecated.
62
+ Deploy the Worker and connect any MCP client to `https://<your-worker>.workers.dev/mcp`, or run `wrangler dev` and point the [MCP Inspector](https://github.com/modelcontextprotocol/inspector) at `http://localhost:8787/mcp`.
110
63
 
111
- ### Accessing Authenticated User Context
64
+ ## Keep state in bindings
112
65
 
113
- When your MCP server is wrapped with `OAuthProvider` from `@cloudflare/workers-oauth-provider`, provider-issued tokens are available through standard SDK v2 `AuthInfo`. The existing `getMcpAuthContext()` application-props helper remains supported:
66
+ Nothing survives between requests inside the handler. Keep data in bindings and read them from your tools with `env` from `cloudflare:workers`:
114
67
 
115
68
  ```typescript
116
- import { createMcpHandler, getMcpAuthContext } from "agents/mcp/server";
69
+ import { env } from "cloudflare:workers";
117
70
 
118
71
  server.registerTool(
119
- "whoami",
120
- { description: "Returns the authenticated user" },
121
- async (context) => {
122
- const auth = getMcpAuthContext();
72
+ "readFile",
73
+ {
74
+ description: "Read a file from the shared bucket",
75
+ inputSchema: { path: z.string() }
76
+ },
77
+ async ({ path }) => {
78
+ const object = await env.BUCKET.get(path);
123
79
  return {
124
80
  content: [
125
- {
126
- type: "text",
127
- text: JSON.stringify({
128
- clientId: context.http?.authInfo?.clientId,
129
- scopes: context.http?.authInfo?.scopes,
130
- props: auth?.props
131
- })
132
- }
81
+ { type: "text", text: object ? await object.text() : "Not found" }
133
82
  ]
134
83
  };
135
84
  }
136
85
  );
137
86
  ```
138
87
 
139
- Do not log or return `context.http.authInfo.token`. External-token resolvers continue providing `getMcpAuthContext().props` but do not synthesize incomplete standard metadata.
140
-
141
- ## Stateful MCP Server with `McpAgent` (legacy)
142
-
143
- `McpAgent` gives each client session its own Durable Object with persistent state. It remains available for existing SDK v1 deployments but is deprecated and feature-frozen; new servers should use the stateless handler or explicitly compose a separate legacy route where a session is required.
144
-
145
- ### Writing TinyMCP
146
-
147
- Prototyping is very easy! If you want to quickly deploy an MCP, it only takes ~20 lines of code:
88
+ For per-user state, call an Agent from the tool. The Agent owns the data, and the MCP server stays stateless:
148
89
 
149
90
  ```typescript
150
- import { McpAgent } from "agents/mcp";
151
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
152
- import { z } from "zod";
91
+ import { Agent, getAgentByName } from "agents";
153
92
 
154
- // Our MCP server!
155
- export class TinyMcp extends McpAgent {
156
- server = new McpServer({ name: "", version: "v1.0.0" });
157
-
158
- async init() {
159
- this.server.registerTool(
160
- "square",
161
- {
162
- description: "Squares a number",
163
- inputSchema: { number: z.number() }
164
- },
165
- async ({ number }) => ({
166
- content: [{ type: "text", text: String(number ** 2) }]
167
- })
168
- );
93
+ export class Notes extends Agent<Env, { notes: string[] }> {
94
+ initialState = { notes: [] };
95
+
96
+ add(note: string) {
97
+ this.setState({ notes: [...this.state.notes, note] });
98
+ return this.state.notes.length;
169
99
  }
170
100
  }
171
101
 
172
- // This is literally all there is to our Worker
173
- export default TinyMcp.serve("/");
102
+ server.registerTool(
103
+ "addNote",
104
+ { inputSchema: { note: z.string() } },
105
+ async ({ note }) => {
106
+ const userId = String(getMcpAuthContext()?.props.userId ?? "anonymous");
107
+ const notes = await getAgentByName(env.Notes, userId);
108
+ const count = await notes.add(note);
109
+ return { content: [{ type: "text", text: `You have ${count} notes` }] };
110
+ }
111
+ );
174
112
  ```
175
113
 
176
- Your `wrangler.jsonc` would look something like:
114
+ Bind `Notes` as a Durable Object in `wrangler.jsonc` and export the class from your Worker. `getMcpAuthContext()` is covered in [Authentication](#authentication).
177
115
 
178
- ```jsonc
179
- {
180
- "name": "tinymcp",
181
- "main": "src/index.ts",
182
- "compatibility_date": "2026-01-28",
183
- "compatibility_flags": ["nodejs_compat"],
184
- "durable_objects": {
185
- "bindings": [
186
- {
187
- "name": "MCP_OBJECT",
188
- "class_name": "TinyMcp"
189
- }
190
- ]
116
+ ## Handler options
117
+
118
+ ```typescript
119
+ createMcpHandler(() => createServer(), {
120
+ route: "/mcp", // exact path to handle (default: "/mcp")
121
+ corsOptions: {
122
+ origin: "https://app.example.com"
191
123
  },
192
- "migrations": [
193
- {
194
- "tag": "v1",
195
- "new_sqlite_classes": ["TinyMcp"]
196
- }
197
- ]
198
- }
124
+ allowedHostnames: ["mcp.example.com"], // custom-domain Host policy
125
+ allowedOriginHostnames: ["app.example.com"], // browser Origin policy
126
+ authContext: { props: {} }, // optional application props override
127
+ legacy: "stateless", // default; use "reject" to serve only Stateless clients
128
+ responseMode: "auto" // upstream SDK response shaping
129
+ });
199
130
  ```
200
131
 
201
- ### What is going on here?
132
+ `createMcpHandler` supports the upstream SDK v2 handler options except `bus`, which the Agents SDK does not expose. Supplying `bus` throws a `TypeError`.
202
133
 
203
- `McpAgent` requires us to define 2 bits, `server` and `init()`.
134
+ ### Origin and Host validation
204
135
 
205
- `init()` is the initialization logic that runs every time our MCP server is started (each client session goes to a different Agent instance).
206
- In there you'll normally setup all your tools/resources and anything else you might need. In this case, we're only setting the tool `square`.
136
+ The handler validates every present `Origin` header before serving the request. It rejects malformed, opaque, and non-HTTP origins. Requests without `Origin` remain valid for non-browser MCP clients.
207
137
 
208
- 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's what `TinyMcp.serve(...)` is for.
209
- 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!
138
+ Its default allowlist includes localhost-class origins and the endpoint's `workers.dev` hostname. A concrete `corsOptions.origin` adds that hostname automatically. The handler also applies matching Host checks to localhost and `workers.dev` endpoints. For a custom domain with wildcard CORS, configure `allowedHostnames` and `allowedOriginHostnames` explicitly. Values are hostnames without a scheme or port, and matching ignores the Origin's scheme and port.
210
139
 
211
- ### Putting it to the test
140
+ Pass `allowedOriginHostnames: "*"` only when equivalent Origin validation runs in trusted middleware before the handler. This disables the handler's Origin check, including malformed and opaque Origin rejection. MCP HTTP servers are required to validate browser Origins, so do not use this as an unauthenticated public-server shortcut.
212
141
 
213
- It's 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. I'll try with https://playground.ai.cloudflare.com:
214
- ![model calls the square tool after connecting to our mcp](https://github.com/user-attachments/assets/1e979a82-ed3e-49e9-b9d5-a3fc9b0363a7)
142
+ ### Notify clients of changes
215
143
 
216
- ## Password-protected StorageMcp with OAuth!
144
+ Use the returned handler's `notify` methods, such as `handler.notify.toolsChanged()`, to publish list and resource changes. Notifications are isolate-local: they do not reach subscriptions in other Worker isolates. See the [handler notification API](https://developers.cloudflare.com/agents/model-context-protocol/apis/handler-api/#publish-list-and-resource-changes) for the available methods.
217
145
 
218
- 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, please do **not** use this in production)
146
+ ## Authentication
219
147
 
220
- ```typescript
221
- import { McpAgent } from "agents/mcp";
222
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
223
- import {
224
- OAuthProvider,
225
- type OAuthHelpers
226
- } from "@cloudflare/workers-oauth-provider";
227
- import { z } from "zod";
228
- import { env } from "cloudflare:workers";
148
+ Wrap the handler with `OAuthProvider` from `@cloudflare/workers-oauth-provider` to require OAuth 2.1. The provider serves the authorization, token, and registration endpoints and validates Bearer tokens on `/mcp`:
229
149
 
230
- export class StorageMcp extends McpAgent {
231
- server = new McpServer({ name: "", version: "v1.0.0" });
232
-
233
- async init() {
234
- // Helper to return text responses from our tools
235
- const textRes = (text: string) => ({
236
- content: [{ type: "text" as const, text }]
237
- });
238
-
239
- this.server.registerTool(
240
- "writeFile",
241
- {
242
- description: "Store text as a file with the given path",
243
- inputSchema: {
244
- path: z.string().describe("Absolute path of the file"),
245
- content: z.string().describe("The content to store")
246
- }
247
- },
248
- async ({ path, content }) => {
249
- try {
250
- await env.BUCKET.put(path, content);
251
- return textRes(`Successfully stored contents to ${path}`);
252
- } catch (e: unknown) {
253
- return textRes(`Couldn't save to file. Found error ${e}`);
254
- }
255
- }
256
- );
257
-
258
- this.server.registerTool(
259
- "readFile",
260
- {
261
- description: "Read the contents of a file",
262
- inputSchema: {
263
- path: z.string().describe("Absolute path of the file to read")
264
- }
265
- },
266
- async ({ path }) => {
267
- const obj = await env.BUCKET.get(path);
268
- if (!obj || !obj.body)
269
- return textRes(`Error reading file at ${path}: not found`);
270
- try {
271
- return textRes(await obj.text());
272
- } catch (e: unknown) {
273
- return textRes(`Error reading file at ${path}: ${e}`);
274
- }
275
- }
276
- );
150
+ ```typescript
151
+ import { OAuthProvider } from "@cloudflare/workers-oauth-provider";
152
+ import { createMcpHandler, getMcpAuthContext } from "agents/mcp/server";
277
153
 
278
- this.server.registerTool(
279
- "whoami",
280
- {
281
- description: "Check who the user is"
282
- },
283
- async () => {
284
- return textRes(`${this.props?.userId}`);
285
- }
286
- );
287
- }
288
- }
154
+ function createServer() {
155
+ const server = new McpServer({ name: "Authenticated", version: "1.0.0" });
289
156
 
290
- // HTML form page for users to write our password
291
- function passwordPage(opts: { query: string; error?: string }) {
292
- const err = opts.error
293
- ? `<p class="text-red-600 mb-2">${opts.error}</p>`
294
- : "";
295
- return new Response(
296
- `<!doctype html>
297
- <html lang="en">
298
- <head>
299
- <meta charset="utf-8" />
300
- <meta name="viewport" content="width=device-width, initial-scale=1" />
301
- <title>ENTER THE MAGIC WORD</title>
302
- <script src="https://cdn.tailwindcss.com"></script>
303
- </head>
304
- <body class="font-sans grid place-items-center min-h-screen bg-gray-100">
305
- <form method="POST" action="/authorize?${opts.query}"
306
- class="bg-white p-6 rounded-lg shadow-md w-full max-w-xs">
307
- <h1 class="text-lg font-semibold mb-3">ENTER THE MAGIC WORD</h1>
308
- ${err}
309
- <label class="block text-sm mb-1">Password</label>
310
- <input name="password" type="password" required autocomplete="current-password"
311
- class="w-full border rounded px-3 py-2 mb-3" />
312
- <button type="submit"
313
- class="w-full py-2 bg-black text-white rounded font-medium hover:bg-gray-800">
314
- Continue
315
- </button>
316
- </form>
317
- </body>
318
- </html>`,
319
- { headers: { "content-type": "text/html; charset=utf-8" } }
157
+ server.registerTool(
158
+ "whoami",
159
+ { description: "Returns the authenticated user" },
160
+ async (context) => {
161
+ const auth = getMcpAuthContext();
162
+ return {
163
+ content: [
164
+ {
165
+ type: "text",
166
+ text: JSON.stringify({
167
+ userId: auth?.props.userId,
168
+ clientId: context.http?.authInfo?.clientId,
169
+ scopes: context.http?.authInfo?.scopes
170
+ })
171
+ }
172
+ ]
173
+ };
174
+ }
320
175
  );
321
- }
322
176
 
323
- // This is the default handler of our worker BEFORE requests are authenticated.
324
- interface StorageEnv {
325
- OAUTH_PROVIDER: OAuthHelpers;
326
- SHARED_PASSWORD: string;
177
+ return server;
327
178
  }
328
179
 
329
- const defaultHandler = {
330
- async fetch(request: Request, env: StorageEnv) {
331
- const provider = env.OAUTH_PROVIDER;
332
- const url = new URL(request.url);
333
-
334
- // Only handle our auth UI/flow here
335
- if (url.pathname !== "/authorize") {
336
- return new Response("NOT FOUND", { status: 404 });
337
- }
338
-
339
- // Parse the OAuth request
340
- const oauthReq = await provider.parseAuthRequest(request);
341
-
342
- // We render the password page for GET requests
343
- if (request.method === "GET") {
344
- return passwordPage({ query: url.searchParams.toString() });
345
- }
346
-
347
- // We validate the password in POST requests
348
- if (request.method === "POST") {
349
- const form = await request.formData();
350
- const password = String(form.get("password") || "");
351
-
352
- const SHARED_PASSWORD = env.SHARED_PASSWORD; // Store this as a secret
353
- if (!SHARED_PASSWORD) {
354
- return new Response("Server misconfigured: missing SHARED_PASSWORD", {
355
- status: 500
356
- });
357
- }
358
- if (password !== SHARED_PASSWORD) {
359
- return passwordPage({
360
- query: url.searchParams.toString(),
361
- error: "Wrong password."
362
- });
363
- }
180
+ const mcpHandler = createMcpHandler(createServer);
364
181
 
365
- // We give everyone the same userId
366
- const userId = "friend";
367
-
368
- const { redirectTo } = await provider.completeAuthorization({
369
- request: oauthReq,
370
- userId,
371
- scope: [], // We don't care about scopes
372
-
373
- // We could add anything we wanted here so we could access it
374
- // within the MCP with `this.props`
375
- props: { userId },
376
- metadata: undefined
377
- });
378
-
379
- return Response.redirect(redirectTo, 302);
380
- }
381
-
382
- return new Response("Method Not Allowed", {
383
- status: 405,
384
- headers: { allow: "GET, POST" }
385
- });
386
- }
387
- };
388
-
389
- // OAuthProvider creates our worker handler
390
182
  export default new OAuthProvider({
391
183
  authorizeEndpoint: "/authorize",
392
- tokenEndpoint: "/token",
393
- clientRegistrationEndpoint: "/register",
394
- apiHandlers: { "/mcp": StorageMcp.serve("/mcp") },
395
- defaultHandler
396
- });
397
- ```
398
-
399
- You would also add these to your `wrangler.jsonc`:
400
-
401
- ```jsonc
402
- {
403
- // rest of your config...
404
- "r2_buckets": [{ "binding": "BUCKET", "bucket_name": "your-bucket-name" }],
405
- "kv_namespaces": [
406
- {
407
- "binding": "OAUTH_KV", // required by OAuthProvider
408
- "id": "your-kv-id"
184
+ tokenEndpoint: "/oauth/token",
185
+ clientRegistrationEndpoint: "/oauth/register",
186
+ apiRoute: "/mcp",
187
+ apiHandler: {
188
+ fetch(request: Request, env: Env, ctx: ExecutionContext) {
189
+ return mcpHandler(request, env, ctx);
409
190
  }
410
- ]
411
- }
412
- ```
413
-
414
- ### What's going on?
415
-
416
- In ~160 lines we were able to write our custom OAuth authorization flow so anyone that knows our secret password can use the MCP server.
417
-
418
- Just like before, in `init()` we set a few tools to access files in our R2 bucket. We also have the `whoami` tool to show users what `userId` we authenticated them with. It's just an example of how to access `props` from within the `McpAgent`.
419
-
420
- Most of the code here is either the HTML page to type in the password or the OAuth `/authorize` logic.
421
- The important part is to notice how in the `OAuthProvider` we expose the `StorageMcp` through the `apiHandlers` key and use the same `serve` method we were using before.
422
-
423
- ### Let's see how this looks like
424
-
425
- Once again, using https://playground.ai.cloudflare.com:
426
- ![password page](https://github.com/user-attachments/assets/8e469110-fffa-45d2-84c1-ae16a651ae41)
427
- The auth flow prompts us for the password.
428
-
429
- ![model calls all 3 tools after authorization](https://github.com/user-attachments/assets/07e22fef-93de-47c2-af7e-9c361e460186)
430
- Once we've authenticated ourselves we can use all the tools!
431
-
432
- ## Data Jurisdiction for Compliance
433
-
434
- `McpAgent` supports specifying a data jurisdiction for your MCP server, which is particularly useful for satisfying GDPR and other data residency regulations. By setting the `jurisdiction` option, you can ensure that your Durable Object instances (and their data) are created in a specific geographic region.
435
-
436
- ### Using the EU Jurisdiction for GDPR
437
-
438
- 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:
439
-
440
- ```typescript
441
- export default TinyMcp.serve("/", {
442
- jurisdiction: "eu"
443
- });
444
- ```
445
-
446
- Or with the OAuth-protected example:
447
-
448
- ```typescript
449
- export default new OAuthProvider({
450
- authorizeEndpoint: "/authorize",
451
- tokenEndpoint: "/token",
452
- clientRegistrationEndpoint: "/register",
453
- apiHandlers: {
454
- "/mcp": StorageMcp.serve("/mcp", { jurisdiction: "eu" })
455
191
  },
456
- defaultHandler
192
+ defaultHandler: AuthHandler // your consent and login flow
457
193
  });
458
194
  ```
459
195
 
460
- When you specify `jurisdiction: "eu"`, Cloudflare will create the Durable Object instances in EU data centers, ensuring that:
461
-
462
- - All MCP session data stays within the EU
463
- - User data processed by your tools remains in the EU
464
- - State stored in the Durable Object's storage API stays in the EU
196
+ `OAuthProvider` needs an object with a `fetch` method, so wrap the handler as shown. Passing the handler function directly throws at startup.
465
197
 
466
- This helps you comply with GDPR's data localization requirements without any additional configuration.
198
+ `getMcpAuthContext().props` returns the `props` you passed to `completeAuthorization()` in your auth flow. Tool callbacks also receive the standard SDK v2 `AuthInfo` at `context.http.authInfo`. Do not log or return `context.http.authInfo.token` or `authInfo.extra.props`.
467
199
 
468
- ### Available Jurisdictions
200
+ The OAuth flow needs a KV namespace bound as `OAUTH_KV`. For a complete server with a login page, see [`examples/mcp-worker-authenticated`](https://github.com/cloudflare/agents/tree/main/examples/mcp-worker-authenticated). For consent dialogs, CSRF, and proxying to a third-party identity provider, see [Securing MCP Servers](./securing-mcp-servers.md).
469
201
 
470
- The `jurisdiction` option accepts any value supported by [Cloudflare's Durable Objects jurisdiction API](https://developers.cloudflare.com/durable-objects/reference/data-location/), including:
202
+ ## Elicitation
471
203
 
472
- - `"eu"` - European Union
473
- - `"fedramp"` - FedRAMP compliant locations
474
-
475
- ## Elicitation (Human-in-the-Loop)
476
-
477
- MCP servers can request additional input during a tool call. The implementation differs by protocol generation.
478
-
479
- ### Stateless Elicitation
480
-
481
- For Stateless Elicitation, return `inputRequired(...)`. The client gathers the input and retries the request with SDK-managed `requestState` and `inputResponses`; the Worker does not suspend while a person responds.
204
+ A tool can ask the user for input by returning `inputRequired(...)`. The client collects the input and retries the request with SDK-managed `requestState` and `inputResponses`. The Worker does not wait while a person responds.
482
205
 
483
206
  ```typescript
484
207
  import {
@@ -491,7 +214,7 @@ import { z } from "zod";
491
214
 
492
215
  function createServer() {
493
216
  const server = new McpServer({
494
- name: "Stateless Elicitation Demo",
217
+ name: "Elicitation Demo",
495
218
  version: "1.0.0"
496
219
  });
497
220
  server.registerTool(
@@ -534,40 +257,24 @@ export default {
534
257
  } satisfies ExportedHandler;
535
258
  ```
536
259
 
537
- See [`examples/mcp-elicitation-mrtr`](https://github.com/cloudflare/agents/tree/main/examples/mcp-elicitation-mrtr) for a two-round Stateless Elicitation example.
538
-
539
- ### Legacy Elicitation
260
+ See [`examples/mcp-elicitation-mrtr`](https://github.com/cloudflare/agents/tree/main/examples/mcp-elicitation-mrtr) for a two-round example.
540
261
 
541
- Existing Legacy deployments send pushed `elicitation/create` requests over a session-addressed response stream. Use `McpAgent` or `createLegacyMcpHandler` with `WorkerTransport` when that behavior must be retained.
262
+ ## 2025-era clients
542
263
 
543
- See [`examples/mcp-elicitation`](https://github.com/cloudflare/agents/tree/main/examples/mcp-elicitation) for form- and URL-mode Legacy Elicitation with Durable Object state and SSE replay.
264
+ With the default `legacy: "stateless"`, the same endpoint also answers 2025-era Streamable HTTP clients for tools, resources, and prompts. Those clients have no session to receive server-to-client requests, so sampling, pushed elicitation, and roots requests fail immediately. Set `legacy: "reject"` to serve only Stateless clients.
544
265
 
545
- ## WorkerTransport
266
+ The handler does not serve the standalone SSE transport.
546
267
 
547
- `WorkerTransport` is the retained Agents transport for stateful Legacy deployments. Use it with `createLegacyMcpHandler` for persistent sessions, storage, event replay, and other explicit legacy configurations. The Stateless handler's Legacy compatibility lane uses the SDK v2 web-standard transport instead and does not import `WorkerTransport`.
268
+ ## Lower-level control
548
269
 
549
- ```typescript
550
- import { WorkerTransport, type TransportState } from "agents/mcp";
551
-
552
- const transport = new WorkerTransport({
553
- sessionIdGenerator: () => crypto.randomUUID(),
554
- enableJsonResponse: false,
555
- storage: {
556
- get: () => kv.get<TransportState>("mcp_state"),
557
- set: (state: TransportState) => kv.put<TransportState>("mcp_state", state)
558
- }
559
- });
560
- ```
270
+ If the handler lifecycle does not fit, use the SDK's `WebStandardStreamableHTTPServerTransport` from `@modelcontextprotocol/server` directly in your Worker.
561
271
 
562
- Key options:
272
+ ## Migrating from `McpAgent`
563
273
 
564
- | Option | Description |
565
- | -------------------- | ------------------------------------------------------------------------------ |
566
- | `sessionIdGenerator` | Function that returns a session ID for new sessions |
567
- | `enableJsonResponse` | Return JSON instead of SSE streams (default: `false`) |
568
- | `storage` | Optional `{ get, set }` adapter for persisting transport state across requests |
569
- | `corsOptions` | CORS configuration |
274
+ `McpAgent` is deprecated. To move an existing server to `createMcpHandler`, follow [Migrate to MCP SDK v2](https://developers.cloudflare.com/agents/model-context-protocol/guides/migrate-to-mcp-sdk-v2/).
570
275
 
571
- ### Read more
276
+ ## Examples
572
277
 
573
- For more complex examples including authentication with third-party providers, see the [examples directory](https://github.com/cloudflare/agents/tree/main/examples).
278
+ - [`examples/mcp-worker`](https://github.com/cloudflare/agents/tree/main/examples/mcp-worker): a minimal server with a browser tool tester
279
+ - [`examples/mcp-worker-authenticated`](https://github.com/cloudflare/agents/tree/main/examples/mcp-worker-authenticated): OAuth with `OAuthProvider`
280
+ - [`examples/mcp-elicitation-mrtr`](https://github.com/cloudflare/agents/tree/main/examples/mcp-elicitation-mrtr): Stateless Elicitation