agents 0.30.0 → 0.31.0

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 (130) hide show
  1. package/README.md +4 -2
  2. package/dist/{agent-routing-CDbpVjYq.d.ts → agent-routing-wS1TimZ_.d.ts} +64 -603
  3. package/dist/agent-routing.d.ts +1 -1
  4. package/dist/agent-tool-types.d.ts +1 -1
  5. package/dist/{agent-tools-C8DH8Yii.d.ts → agent-tools-Bamxxb6Y.d.ts} +2 -2
  6. package/dist/agent-tools.d.ts +1 -1
  7. package/dist/{ai-iXw0uOGj.d.ts → ai-CDn3Abax.d.ts} +30 -4
  8. package/dist/{ai-sdk-DRPh2rRa.js → ai-sdk-Kon40i0q.js} +2 -36
  9. package/dist/ai-sdk-Kon40i0q.js.map +1 -0
  10. package/dist/ai-sdk-hW3vntnk.d.ts +72 -0
  11. package/dist/browser/ai.d.ts +1 -1
  12. package/dist/browser/ai.js +16 -3
  13. package/dist/browser/ai.js.map +1 -1
  14. package/dist/browser/index.d.ts +64 -16
  15. package/dist/browser/index.js +4 -4
  16. package/dist/browser/tools/ai-sdk.d.ts +7 -70
  17. package/dist/browser/tools/ai-sdk.js +4 -4
  18. package/dist/browser/tools/ai-sdk.js.map +1 -1
  19. package/dist/browser/tools/pi.d.ts +9 -15
  20. package/dist/browser/tools/pi.js +6 -14
  21. package/dist/browser/tools/pi.js.map +1 -1
  22. package/dist/browser/tools/tanstack-ai.d.ts +46 -23
  23. package/dist/browser/tools/tanstack-ai.js +8 -4
  24. package/dist/browser/tools/tanstack-ai.js.map +1 -1
  25. package/dist/{browser-DccPw_wW.js → browser-1eUbPvC2.js} +413 -14
  26. package/dist/browser-1eUbPvC2.js.map +1 -0
  27. package/dist/{browser-DlXhSAdf.d.ts → browser-D9RNpJuT.d.ts} +254 -20
  28. package/dist/browser-tool-BhjCcNVE.d.ts +71 -0
  29. package/dist/browser-tool-lGnvEBLJ.js +497 -0
  30. package/dist/browser-tool-lGnvEBLJ.js.map +1 -0
  31. package/dist/capability-id-B3J8Nok_.js +13 -0
  32. package/dist/capability-id-B3J8Nok_.js.map +1 -0
  33. package/dist/chat/index.d.ts +2 -2
  34. package/dist/chat-sdk/index.d.ts +3 -3
  35. package/dist/chat-sdk/index.js +1 -1
  36. package/dist/client-4OlMIJx-.js.map +1 -1
  37. package/dist/{client-BnYbNT3z.js → client-DMWujQJ1.js} +68 -244
  38. package/dist/client-DMWujQJ1.js.map +1 -0
  39. package/dist/{client-C27CtRZr.d.ts → client-wn4-yqGz.d.ts} +9 -7
  40. package/dist/client.d.ts +1 -1
  41. package/dist/{connector-DAXBOIIi.js → connector-CM-uuxKM.js} +96 -8
  42. package/dist/connector-CM-uuxKM.js.map +1 -0
  43. package/dist/{connector-20DJ6p4k.d.ts → connector-DZ71EjE-.d.ts} +109 -9
  44. package/dist/experimental/webmcp.js +1 -1
  45. package/dist/{handler-stateless-Bly_nSH3.js → handler-stateless-B5hU3dxK.js} +35 -2
  46. package/dist/handler-stateless-B5hU3dxK.js.map +1 -0
  47. package/dist/{handler-stateless-DxYpJ_XF.d.ts → handler-stateless-C90kD5_R.d.ts} +3 -47
  48. package/dist/harness/container/index.d.ts +15 -15
  49. package/dist/harness/opencode/index.js.map +1 -1
  50. package/dist/harness/pi/index.d.ts +7 -3
  51. package/dist/harness/pi/index.js +46 -15
  52. package/dist/harness/pi/index.js.map +1 -1
  53. package/dist/harness/think/index.d.ts +2 -1
  54. package/dist/{images-xaDv2Fqe.js → images-CfuRCjwq.js} +1 -5
  55. package/dist/images-CfuRCjwq.js.map +1 -0
  56. package/dist/{index-DCxk3OSJ.d.ts → index-8k4qsX1T.d.ts} +2 -28
  57. package/dist/index.d.ts +14 -14
  58. package/dist/index.js +2 -2
  59. package/dist/internal-hosts-Kfp5A392.d.ts +230 -0
  60. package/dist/internal-kmw70xwR.js.map +1 -1
  61. package/dist/invoker-BC1SZ3q1.js +26 -0
  62. package/dist/invoker-BC1SZ3q1.js.map +1 -0
  63. package/dist/invoker-DoLFPJZV.d.ts +21 -0
  64. package/dist/mcp/client/index.d.ts +15 -1
  65. package/dist/mcp/client/index.js +2 -2
  66. package/dist/mcp/client/x402.d.ts +7 -5
  67. package/dist/mcp/client/x402.js +34 -1
  68. package/dist/mcp/client/x402.js.map +1 -1
  69. package/dist/mcp/index.d.ts +561 -27
  70. package/dist/mcp/index.js +180 -3
  71. package/dist/mcp/index.js.map +1 -1
  72. package/dist/mcp/server/index.d.ts +1 -1
  73. package/dist/mcp/server/index.js +1 -1
  74. package/dist/models/ai-sdk/index.d.ts +2 -2
  75. package/dist/models/ai-sdk/index.js +1 -1
  76. package/dist/models/opencode/index.d.ts +2 -2
  77. package/dist/models/opencode/index.js +1 -1
  78. package/dist/models/pi-ai/index.d.ts +3 -28
  79. package/dist/models/pi-ai/index.js +31 -16
  80. package/dist/models/pi-ai/index.js.map +1 -1
  81. package/dist/protocol-BEgrJYC5.d.ts +29 -29
  82. package/dist/react.d.ts +2 -2
  83. package/dist/routing/index.d.ts +1 -1
  84. package/dist/serializable.d.ts +1 -1
  85. package/dist/{session-store-yvwq1f9x.js → session-7CDgVE0z.js} +455 -33
  86. package/dist/session-7CDgVE0z.js.map +1 -0
  87. package/dist/{session-store-_IK6dV-w.d.ts → session-store-B7Ca-u1t.d.ts} +8 -1
  88. package/dist/settings-Dk2il2Hf.js.map +1 -1
  89. package/dist/{src-DfLzdwlK.js → src-BGolXG_H.js} +18 -2
  90. package/dist/src-BGolXG_H.js.map +1 -0
  91. package/dist/sub-routing.d.ts +12 -12
  92. package/dist/tool-CTp72Jb_.d.ts +10 -10
  93. package/dist/tool-DoVberpN.d.ts +22 -22
  94. package/dist/tool-helpers-BodWi4gd.js +256 -0
  95. package/dist/tool-helpers-BodWi4gd.js.map +1 -0
  96. package/dist/{transport-CPh7YGiw.d.ts → transport-5moD2pW5.d.ts} +1 -43
  97. package/dist/types-D2naO3rA.d.ts +49 -0
  98. package/dist/voice/index.d.ts +1 -1
  99. package/dist/workflow-types.d.ts +1 -1
  100. package/dist/workflows.d.ts +1 -1
  101. package/dist/workflows.js +1 -1
  102. package/docs/browse-the-web.md +247 -413
  103. package/docs/get-current-agent.md +4 -7
  104. package/docs/harnesses/pi.md +28 -28
  105. package/docs/human-in-the-loop.md +3 -7
  106. package/docs/index.md +6 -6
  107. package/docs/mcp-client.md +4 -28
  108. package/docs/mcp-servers.md +137 -411
  109. package/docs/mcp-transports.md +30 -289
  110. package/docs/models-pi-ai.md +9 -11
  111. package/docs/models.md +1 -8
  112. package/docs/routing.md +0 -2
  113. package/docs/securing-mcp-servers.md +120 -304
  114. package/package.json +8 -3
  115. package/dist/ai-sdk-DRPh2rRa.js.map +0 -1
  116. package/dist/browser-DccPw_wW.js.map +0 -1
  117. package/dist/browser-tool-B1mhifin.js +0 -616
  118. package/dist/browser-tool-B1mhifin.js.map +0 -1
  119. package/dist/browser-tool-Cxjk5sgG.d.ts +0 -92
  120. package/dist/client-BnYbNT3z.js.map +0 -1
  121. package/dist/connector-DAXBOIIi.js.map +0 -1
  122. package/dist/handler-stateless-Bly_nSH3.js.map +0 -1
  123. package/dist/images-xaDv2Fqe.js.map +0 -1
  124. package/dist/invoker-CG0_p_Wq.d.ts +0 -20
  125. package/dist/invoker-CHMnoxIA.js +0 -57
  126. package/dist/invoker-CHMnoxIA.js.map +0 -1
  127. package/dist/session-store-yvwq1f9x.js.map +0 -1
  128. package/dist/src-DfLzdwlK.js.map +0 -1
  129. package/dist/tool-helpers-O5BDVEAY.js +0 -58
  130. package/dist/tool-helpers-O5BDVEAY.js.map +0 -1
@@ -1,33 +1,19 @@
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
+ `@modelcontextprotocol/server` is an optional peer dependency of `agents`, so your package manager does not install it for you. Use the exact version required by your installed Agents release.
12
+
13
+ Upgrading a server that uses `McpAgent`? See [Upgrading an existing `McpAgent` server](#upgrading-an-existing-mcpagent-server).
14
+
15
+ ## Create a server
16
+
31
17
  ```typescript
32
18
  import { McpServer } from "@modelcontextprotocol/server";
33
19
  import { createMcpHandler } from "agents/mcp/server";
@@ -53,432 +39,174 @@ function createServer() {
53
39
  return server;
54
40
  }
55
41
 
42
+ const handler = createMcpHandler(createServer);
43
+
56
44
  export default {
57
45
  fetch(request, env, ctx) {
58
- return createMcpHandler(createServer)(request, env, ctx);
46
+ return handler(request, env, ctx);
59
47
  }
60
48
  } satisfies ExportedHandler;
61
49
  ```
62
50
 
63
- `createMcpHandler` requires a factory so concurrent Worker requests receive isolated server instances. A function input is always treated as an SDK v2 factory.
64
-
65
- ### `createMcpHandler` options
51
+ The server is served at `/mcp` by default. `agents/mcp/server` uses `AsyncLocalStorage`, so enable `nodejs_compat`:
66
52
 
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
- });
53
+ ```jsonc
54
+ {
55
+ "name": "hello-mcp",
56
+ "main": "src/index.ts",
57
+ "compatibility_date": "2026-06-11",
58
+ "compatibility_flags": ["nodejs_compat"]
59
+ }
79
60
  ```
80
61
 
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
- ```
62
+ `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
63
 
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.
64
+ 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
65
 
111
- ### Accessing Authenticated User Context
66
+ ## Keep state in bindings
112
67
 
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:
68
+ Nothing survives between requests inside the handler. Keep data in bindings and read them from your tools with `env` from `cloudflare:workers`:
114
69
 
115
70
  ```typescript
116
- import { createMcpHandler, getMcpAuthContext } from "agents/mcp/server";
71
+ import { env } from "cloudflare:workers";
117
72
 
118
73
  server.registerTool(
119
- "whoami",
120
- { description: "Returns the authenticated user" },
121
- async (context) => {
122
- const auth = getMcpAuthContext();
74
+ "readFile",
75
+ {
76
+ description: "Read a file from the shared bucket",
77
+ inputSchema: { path: z.string() }
78
+ },
79
+ async ({ path }) => {
80
+ const object = await env.BUCKET.get(path);
123
81
  return {
124
82
  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
- }
83
+ { type: "text", text: object ? await object.text() : "Not found" }
133
84
  ]
134
85
  };
135
86
  }
136
87
  );
137
88
  ```
138
89
 
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:
90
+ For per-user state, call an Agent from the tool. The Agent owns the data, and the MCP server stays stateless:
148
91
 
149
92
  ```typescript
150
- import { McpAgent } from "agents/mcp";
151
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
152
- import { z } from "zod";
93
+ import { Agent, getAgentByName } from "agents";
153
94
 
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
- );
95
+ export class Notes extends Agent<Env, { notes: string[] }> {
96
+ initialState = { notes: [] };
97
+
98
+ add(note: string) {
99
+ this.setState({ notes: [...this.state.notes, note] });
100
+ return this.state.notes.length;
169
101
  }
170
102
  }
171
103
 
172
- // This is literally all there is to our Worker
173
- export default TinyMcp.serve("/");
104
+ server.registerTool(
105
+ "addNote",
106
+ { inputSchema: { note: z.string() } },
107
+ async ({ note }) => {
108
+ const userId = String(getMcpAuthContext()?.props.userId ?? "anonymous");
109
+ const notes = await getAgentByName(env.Notes, userId);
110
+ const count = await notes.add(note);
111
+ return { content: [{ type: "text", text: `You have ${count} notes` }] };
112
+ }
113
+ );
174
114
  ```
175
115
 
176
- Your `wrangler.jsonc` would look something like:
116
+ Bind `Notes` as a Durable Object in `wrangler.jsonc` and export the class from your Worker. `getMcpAuthContext()` is covered in [Authentication](#authentication).
177
117
 
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
- ]
118
+ ## Handler options
119
+
120
+ ```typescript
121
+ createMcpHandler(() => createServer(), {
122
+ route: "/mcp", // exact path to handle (default: "/mcp")
123
+ corsOptions: {
124
+ origin: "https://app.example.com"
191
125
  },
192
- "migrations": [
193
- {
194
- "tag": "v1",
195
- "new_sqlite_classes": ["TinyMcp"]
196
- }
197
- ]
198
- }
126
+ allowedHostnames: ["mcp.example.com"], // custom-domain Host policy
127
+ allowedOriginHostnames: ["app.example.com"], // browser Origin policy
128
+ authContext: { props: {} }, // optional application props override
129
+ legacy: "stateless", // default; use "reject" to serve only Stateless clients
130
+ responseMode: "auto" // upstream SDK response shaping
131
+ });
199
132
  ```
200
133
 
201
- ### What is going on here?
134
+ `createMcpHandler` supports the upstream SDK v2 handler options except `bus`, which the Agents SDK does not expose. Supplying `bus` throws a `TypeError`.
202
135
 
203
- `McpAgent` requires us to define 2 bits, `server` and `init()`.
136
+ ### Origin and Host validation
204
137
 
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`.
138
+ 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.
139
+
140
+ 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.
207
141
 
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!
142
+ 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.
210
143
 
211
- ### Putting it to the test
144
+ ### Notify clients of changes
212
145
 
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)
146
+ 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.
215
147
 
216
- ## Password-protected StorageMcp with OAuth!
148
+ ## Authentication
217
149
 
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)
150
+ 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`:
219
151
 
220
152
  ```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";
153
+ import { OAuthProvider } from "@cloudflare/workers-oauth-provider";
154
+ import { createMcpHandler, getMcpAuthContext } from "agents/mcp/server";
228
155
  import { env } from "cloudflare:workers";
229
156
 
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
- );
277
-
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
- }
157
+ function createServer() {
158
+ const server = new McpServer({ name: "Authenticated", version: "1.0.0" });
289
159
 
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" } }
160
+ server.registerTool(
161
+ "whoami",
162
+ { description: "Returns the authenticated user" },
163
+ async (context) => {
164
+ const auth = getMcpAuthContext();
165
+ return {
166
+ content: [
167
+ {
168
+ type: "text",
169
+ text: JSON.stringify({
170
+ userId: auth?.props.userId,
171
+ clientId: context.http?.authInfo?.clientId,
172
+ scopes: context.http?.authInfo?.scopes
173
+ })
174
+ }
175
+ ]
176
+ };
177
+ }
320
178
  );
321
- }
322
179
 
323
- // This is the default handler of our worker BEFORE requests are authenticated.
324
- interface StorageEnv {
325
- OAUTH_PROVIDER: OAuthHelpers;
326
- SHARED_PASSWORD: string;
180
+ return server;
327
181
  }
328
182
 
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
- }
183
+ const mcpHandler = createMcpHandler(createServer);
338
184
 
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
- }
364
-
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
185
  export default new OAuthProvider({
391
186
  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"
187
+ tokenEndpoint: "/oauth/token",
188
+ clientRegistrationEndpoint: "/oauth/register",
189
+ apiRoute: "/mcp",
190
+ apiHandler: {
191
+ fetch(request: Request, env: Env, ctx: ExecutionContext) {
192
+ return mcpHandler(request, env, ctx);
409
193
  }
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
194
  },
456
- defaultHandler
195
+ defaultHandler: AuthHandler, // your consent and login flow
196
+ // The URL MCP clients connect to. Every token is bound to it.
197
+ resourceMetadata: { resource: env.MCP_RESOURCE }
457
198
  });
458
199
  ```
459
200
 
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
465
-
466
- This helps you comply with GDPR's data localization requirements without any additional configuration.
467
-
468
- ### Available Jurisdictions
469
-
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:
471
-
472
- - `"eu"` - European Union
473
- - `"fedramp"` - FedRAMP compliant locations
201
+ `OAuthProvider` needs an object with a `fetch` method, so wrap the handler as shown. Passing the handler function directly throws at startup.
474
202
 
475
- ## Elicitation (Human-in-the-Loop)
203
+ `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`. `authInfo` is set only when the token has a client ID and `props` is a plain object. Tokens from `resolveExternalToken` have no client ID, so for those only `getMcpAuthContext().props` is available. Do not log or return `context.http.authInfo.token` or `authInfo.extra.props`.
476
204
 
477
- MCP servers can request additional input during a tool call. The implementation differs by protocol generation.
205
+ Set `MCP_RESOURCE` in `wrangler.jsonc` to the URL clients connect to: Workers OAuth Provider 1.x binds every token to it. The OAuth flow also 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 the consent page and signing in through a third-party provider, see [Securing MCP Servers](./securing-mcp-servers.md).
478
206
 
479
- ### Stateless Elicitation
207
+ ## Elicitation
480
208
 
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.
209
+ 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
210
 
483
211
  ```typescript
484
212
  import {
@@ -491,7 +219,7 @@ import { z } from "zod";
491
219
 
492
220
  function createServer() {
493
221
  const server = new McpServer({
494
- name: "Stateless Elicitation Demo",
222
+ name: "Elicitation Demo",
495
223
  version: "1.0.0"
496
224
  });
497
225
  server.registerTool(
@@ -534,40 +262,38 @@ export default {
534
262
  } satisfies ExportedHandler;
535
263
  ```
536
264
 
537
- See [`examples/mcp-elicitation-mrtr`](https://github.com/cloudflare/agents/tree/main/examples/mcp-elicitation-mrtr) for a two-round Stateless Elicitation example.
265
+ See [`examples/mcp-elicitation-mrtr`](https://github.com/cloudflare/agents/tree/main/examples/mcp-elicitation-mrtr) for a two-round example.
538
266
 
539
- ### Legacy Elicitation
267
+ ## 2025-era clients
540
268
 
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.
269
+ 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.
542
270
 
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.
271
+ The handler does not serve the standalone SSE transport.
544
272
 
545
- ## WorkerTransport
273
+ ## Lower-level control
546
274
 
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`.
275
+ If the handler lifecycle does not fit, use the SDK's `WebStandardStreamableHTTPServerTransport` from `@modelcontextprotocol/server` directly in your Worker.
548
276
 
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
- });
277
+ ## Upgrading an existing `McpAgent` server
278
+
279
+ `McpAgent` is deprecated, but existing servers keep working. It is built on MCP SDK v1, which is an optional peer dependency of `agents`, so your package manager no longer installs it for you. Add the exact version your Agents release expects before you upgrade:
280
+
281
+ ```sh
282
+ pnpm add agents @modelcontextprotocol/sdk@1.32.1 zod
560
283
  ```
561
284
 
562
- Key options:
285
+ Keep importing `McpServer` from `@modelcontextprotocol/sdk/server/mcp.js` in an `McpAgent`. An SDK v2 server from `@modelcontextprotocol/server` cannot run inside `McpAgent`. The same applies to `WorkerTransport` and `createLegacyMcpHandler` from `agents/mcp`.
286
+
287
+ `agents/x402` also needs `@modelcontextprotocol/sdk`. It does not load the package at runtime, but its types refer to SDK v1 clients and tools.
288
+
289
+ If your server imported `@modelcontextprotocol/sdk` without listing it in `package.json`, it only worked because `agents` installed the package. After you upgrade, the build fails with an unresolved `@modelcontextprotocol/sdk` import until you add it.
290
+
291
+ Client code does not need the v1 SDK. If you import client helpers such as `RPCClientTransport` or `normalizeServerId` from `agents/mcp`, import them from `agents/mcp/client` instead.
563
292
 
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 |
293
+ To move the server itself to `createMcpHandler`, follow [Migrate to MCP SDK v2](https://developers.cloudflare.com/agents/model-context-protocol/guides/migrate-to-mcp-sdk-v2/).
570
294
 
571
- ### Read more
295
+ ## Examples
572
296
 
573
- For more complex examples including authentication with third-party providers, see the [examples directory](https://github.com/cloudflare/agents/tree/main/examples).
297
+ - [`examples/mcp-worker`](https://github.com/cloudflare/agents/tree/main/examples/mcp-worker): a minimal server with a browser tool tester
298
+ - [`examples/mcp-worker-authenticated`](https://github.com/cloudflare/agents/tree/main/examples/mcp-worker-authenticated): OAuth with `OAuthProvider`
299
+ - [`examples/mcp-elicitation-mrtr`](https://github.com/cloudflare/agents/tree/main/examples/mcp-elicitation-mrtr): Stateless Elicitation