agents 0.16.2 → 0.17.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 (122) hide show
  1. package/README.md +11 -8
  2. package/dist/{agent-tool-types-CTw3UJUP.d.ts → agent-tool-types-Cd1TZPfB.d.ts} +587 -137
  3. package/dist/agent-tool-types.d.ts +34 -18
  4. package/dist/agent-tool-types.js +20 -1
  5. package/dist/agent-tool-types.js.map +1 -0
  6. package/dist/agent-tools-BXlsuX0d.js +304 -0
  7. package/dist/agent-tools-BXlsuX0d.js.map +1 -0
  8. package/dist/agent-tools-_E8wxUIK.d.ts +119 -0
  9. package/dist/agent-tools.d.ts +24 -18
  10. package/dist/agent-tools.js.map +1 -1
  11. package/dist/ai-chat-agent.d.ts +1 -1
  12. package/dist/ai-chat-agent.js +1 -2
  13. package/dist/ai-chat-agent.js.map +1 -1
  14. package/dist/ai-chat-v5-migration.d.ts +1 -1
  15. package/dist/ai-chat-v5-migration.js +1 -2
  16. package/dist/ai-chat-v5-migration.js.map +1 -1
  17. package/dist/ai-react.d.ts +1 -1
  18. package/dist/ai-react.js +1 -2
  19. package/dist/ai-react.js.map +1 -1
  20. package/dist/ai-types.d.ts +1 -7
  21. package/dist/ai-types.js +2 -8
  22. package/dist/ai-types.js.map +1 -1
  23. package/dist/browser/ai.js +1 -1
  24. package/dist/browser/index.js +1 -1
  25. package/dist/chat/index.d.ts +1918 -72
  26. package/dist/chat/index.js +1730 -245
  27. package/dist/chat/index.js.map +1 -1
  28. package/dist/chat/react.d.ts +602 -0
  29. package/dist/chat/react.js +1506 -0
  30. package/dist/chat/react.js.map +1 -0
  31. package/dist/chat-sdk/index.d.ts +4 -4
  32. package/dist/{classPrivateFieldGet2-CZ7QjTXN.js → classPrivateFieldGet2-DZBYAB34.js} +5 -5
  33. package/dist/{classPrivateMethodInitSpec-D-0__zd9.js → classPrivateMethodInitSpec-qMjJ6sHQ.js} +2 -2
  34. package/dist/{client-BXJ9n2f7.js → client-BZ-B3NhC.js} +2 -2
  35. package/dist/client-BZ-B3NhC.js.map +1 -0
  36. package/dist/client-tools-aIBO0Fk7.d.ts +53 -0
  37. package/dist/client.d.ts +76 -57
  38. package/dist/client.js +33 -5
  39. package/dist/client.js.map +1 -1
  40. package/dist/{connector-CrKhowfD.js → connector-CdldGF3h.js} +5 -5
  41. package/dist/{connector-CrKhowfD.js.map → connector-CdldGF3h.js.map} +1 -1
  42. package/dist/email.js +1 -1
  43. package/dist/email.js.map +1 -1
  44. package/dist/{index-B7IbEeze.d.ts → index-CcbnKkNh.d.ts} +188 -14
  45. package/dist/index.d.ts +91 -71
  46. package/dist/index.js +562 -24
  47. package/dist/index.js.map +1 -1
  48. package/dist/mcp/client.d.ts +18 -14
  49. package/dist/mcp/client.js +1 -1
  50. package/dist/mcp/index.d.ts +30 -30
  51. package/dist/mcp/index.js +7 -7
  52. package/dist/mcp/index.js.map +1 -1
  53. package/dist/{agent-tools-3zLG7MgA.js → message-builder-BymO4N_D.js} +45 -126
  54. package/dist/message-builder-BymO4N_D.js.map +1 -0
  55. package/dist/observability/index.d.ts +1 -1
  56. package/dist/observability/index.js +4 -2
  57. package/dist/observability/index.js.map +1 -1
  58. package/dist/react.d.ts +123 -110
  59. package/dist/react.js +44 -11
  60. package/dist/react.js.map +1 -1
  61. package/dist/serializable.d.ts +1 -1
  62. package/dist/skills/compile.js +1 -1
  63. package/dist/skills/compile.js.map +1 -1
  64. package/dist/skills/index.js +5 -5
  65. package/dist/skills/index.js.map +1 -1
  66. package/dist/sub-routing.d.ts +6 -6
  67. package/dist/utils.js +1 -1
  68. package/dist/utils.js.map +1 -1
  69. package/dist/vite.js +5 -5
  70. package/dist/vite.js.map +1 -1
  71. package/dist/wire-types-nflOzNuU.js +240 -0
  72. package/dist/wire-types-nflOzNuU.js.map +1 -0
  73. package/dist/{workflow-types-SrZK_o9p.d.ts → workflow-types-Baz_PO5v.d.ts} +42 -22
  74. package/dist/workflow-types.d.ts +25 -21
  75. package/dist/workflow-types.js.map +1 -1
  76. package/dist/workflows.d.ts +22 -21
  77. package/dist/workflows.js +31 -7
  78. package/dist/workflows.js.map +1 -1
  79. package/docs/adding-to-existing-project.md +450 -0
  80. package/docs/agent-class.md +503 -0
  81. package/docs/agent-tools.md +552 -0
  82. package/docs/browse-the-web.md +430 -0
  83. package/docs/callable-methods.md +627 -0
  84. package/docs/chat-agents.md +1687 -0
  85. package/docs/chat-sdk.md +181 -0
  86. package/docs/client-sdk.md +520 -0
  87. package/docs/client-tools-continuation.md +177 -0
  88. package/docs/codemode.md +440 -0
  89. package/docs/configuration.md +775 -0
  90. package/docs/cross-domain-authentication.md +171 -0
  91. package/docs/durable-execution.md +537 -0
  92. package/docs/email.md +663 -0
  93. package/docs/get-current-agent.md +204 -0
  94. package/docs/getting-started.md +305 -0
  95. package/docs/http-websockets.md +668 -0
  96. package/docs/human-in-the-loop.md +661 -0
  97. package/docs/index.md +151 -0
  98. package/docs/long-running-agents.md +730 -0
  99. package/docs/mcp-client.md +620 -0
  100. package/docs/mcp-servers.md +526 -0
  101. package/docs/mcp-transports.md +308 -0
  102. package/docs/migration-to-ai-sdk-v5.md +96 -0
  103. package/docs/migration-to-ai-sdk-v6.md +163 -0
  104. package/docs/observability.md +261 -0
  105. package/docs/push-notifications.md +367 -0
  106. package/docs/queue.md +329 -0
  107. package/docs/readonly-connections.md +278 -0
  108. package/docs/resumable-streaming.md +127 -0
  109. package/docs/retries.md +444 -0
  110. package/docs/routing.md +749 -0
  111. package/docs/scheduling.md +898 -0
  112. package/docs/securing-mcp-servers.md +359 -0
  113. package/docs/server-driven-messages.md +477 -0
  114. package/docs/sessions.md +1024 -0
  115. package/docs/state.md +512 -0
  116. package/docs/sub-agents.md +389 -0
  117. package/docs/webhooks.md +604 -0
  118. package/docs/workflows.md +877 -0
  119. package/package.json +41 -14
  120. package/dist/agent-tools-3zLG7MgA.js.map +0 -1
  121. package/dist/agent-tools-DZhI5F6Q.d.ts +0 -14
  122. package/dist/client-BXJ9n2f7.js.map +0 -1
@@ -0,0 +1,526 @@
1
+ # Creating MCP Servers
2
+
3
+ This guide covers the different ways to create MCP servers with the Agents SDK and helps you choose the right approach.
4
+
5
+ ## Choosing an Approach
6
+
7
+ | Approach | Stateful? | Requires Durable Objects? | Best for |
8
+ | ---------------------------------------------- | --------- | ------------------------- | ---------------------------------------------- |
9
+ | `createMcpHandler()` | No | No | Stateless tools, simplest setup |
10
+ | `McpAgent` | Yes | Yes | Stateful tools, per-session state, elicitation |
11
+ | Raw `WebStandardStreamableHTTPServerTransport` | No | No | Full control, no SDK dependency |
12
+
13
+ - **`createMcpHandler()`** is the fastest way to get a stateless MCP server running. Use it when your tools do not need per-session state.
14
+ - **`McpAgent`** gives you a Durable Object per session with built-in state management, elicitation support, and both SSE and Streamable HTTP transports.
15
+ - **Raw transport** gives you full control if you want to use the `@modelcontextprotocol/sdk` directly without the Agents SDK helpers.
16
+
17
+ ## Stateless MCP Server with `createMcpHandler()`
18
+
19
+ The simplest way to create an MCP server. No Durable Objects or bindings required:
20
+
21
+ ```typescript
22
+ import { createMcpHandler } from "agents/mcp";
23
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
24
+ import { z } from "zod";
25
+
26
+ function createServer() {
27
+ const server = new McpServer({
28
+ name: "Hello MCP Server",
29
+ version: "1.0.0"
30
+ });
31
+
32
+ server.registerTool(
33
+ "hello",
34
+ {
35
+ description: "Returns a greeting message",
36
+ inputSchema: { name: z.string().optional() }
37
+ },
38
+ async ({ name }) => ({
39
+ content: [{ text: `Hello, ${name ?? "World"}!`, type: "text" }]
40
+ })
41
+ );
42
+
43
+ return server;
44
+ }
45
+
46
+ export default {
47
+ fetch: async (request: Request, env: Env, ctx: ExecutionContext) => {
48
+ const server = createServer();
49
+ return createMcpHandler(server)(request, env, ctx);
50
+ }
51
+ };
52
+ ```
53
+
54
+ > **Important:** Create a new `McpServer` instance per request. The MCP SDK does not allow connecting an already-connected server to a new transport.
55
+
56
+ ### `createMcpHandler` Options
57
+
58
+ ```typescript
59
+ createMcpHandler(server, {
60
+ route: "/mcp", // path to handle (default: "/mcp")
61
+ enableJsonResponse: true, // use JSON responses instead of SSE streaming
62
+ sessionIdGenerator: () => crypto.randomUUID(),
63
+ corsOptions: { ... }, // CORS configuration
64
+ authContext: { props: {} }, // manually set auth context
65
+ transport: workerTransport // provide your own WorkerTransport instance
66
+ });
67
+ ```
68
+
69
+ ### Accessing Authenticated User Context
70
+
71
+ When your MCP server is wrapped with `OAuthProvider` from `@cloudflare/workers-oauth-provider`, authenticated user information is available inside tools via `getMcpAuthContext()`:
72
+
73
+ ```typescript
74
+ import { createMcpHandler, getMcpAuthContext } from "agents/mcp";
75
+
76
+ server.registerTool(
77
+ "whoami",
78
+ { description: "Returns the authenticated user" },
79
+ async () => {
80
+ const auth = getMcpAuthContext();
81
+ return {
82
+ content: [
83
+ {
84
+ type: "text",
85
+ text: auth ? JSON.stringify(auth.props) : "Not authenticated"
86
+ }
87
+ ]
88
+ };
89
+ }
90
+ );
91
+ ```
92
+
93
+ The `OAuthProvider` sets `ctx.props` on the execution context, which `createMcpHandler` automatically picks up and makes available via `getMcpAuthContext()`.
94
+
95
+ ## Stateful MCP Server with `McpAgent`
96
+
97
+ `McpAgent` gives each client session its own Durable Object with persistent state. Use this when your tools need to track per-session data.
98
+
99
+ ### Writing TinyMCP
100
+
101
+ Prototyping is very easy! If you want to quickly deploy an MCP, it only takes ~20 lines of code:
102
+
103
+ ```typescript
104
+ import { McpAgent } from "agents/mcp";
105
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
106
+ import { z } from "zod";
107
+
108
+ // Our MCP server!
109
+ export class TinyMcp extends McpAgent {
110
+ server = new McpServer({ name: "", version: "v1.0.0" });
111
+
112
+ async init() {
113
+ this.server.registerTool(
114
+ "square",
115
+ {
116
+ description: "Squares a number",
117
+ inputSchema: { number: z.number() }
118
+ },
119
+ async ({ number }) => ({
120
+ content: [{ type: "text", text: String(number ** 2) }]
121
+ })
122
+ );
123
+ }
124
+ }
125
+
126
+ // This is literally all there is to our Worker
127
+ export default TinyMcp.serve("/");
128
+ ```
129
+
130
+ Your `wrangler.jsonc` would look something like:
131
+
132
+ ```jsonc
133
+ {
134
+ "name": "tinymcp",
135
+ "main": "src/index.ts",
136
+ "compatibility_date": "2026-01-28",
137
+ "compatibility_flags": ["nodejs_compat"],
138
+ "durable_objects": {
139
+ "bindings": [
140
+ {
141
+ "name": "MCP_OBJECT",
142
+ "class_name": "TinyMcp"
143
+ }
144
+ ]
145
+ },
146
+ "migrations": [
147
+ {
148
+ "tag": "v1",
149
+ "new_sqlite_classes": ["TinyMcp"]
150
+ }
151
+ ]
152
+ }
153
+ ```
154
+
155
+ ### What is going on here?
156
+
157
+ `McpAgent` requires us to define 2 bits, `server` and `init()`.
158
+
159
+ `init()` is the initialization logic that runs every time our MCP server is started (each client session goes to a different Agent instance).
160
+ 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`.
161
+
162
+ 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.
163
+ 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!
164
+
165
+ ### Putting it to the test
166
+
167
+ 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:
168
+ ![model calls the square tool after connecting to our mcp](https://github.com/user-attachments/assets/1e979a82-ed3e-49e9-b9d5-a3fc9b0363a7)
169
+
170
+ ## Password-protected StorageMcp with OAuth!
171
+
172
+ 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)
173
+
174
+ ```typescript
175
+ import { McpAgent } from "agents/mcp";
176
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
177
+ import {
178
+ OAuthProvider,
179
+ type OAuthHelpers
180
+ } from "@cloudflare/workers-oauth-provider";
181
+ import { z } from "zod";
182
+ import { env } from "cloudflare:workers";
183
+
184
+ export class StorageMcp extends McpAgent {
185
+ server = new McpServer({ name: "", version: "v1.0.0" });
186
+
187
+ async init() {
188
+ // Helper to return text responses from our tools
189
+ const textRes = (text: string) => ({
190
+ content: [{ type: "text" as const, text }]
191
+ });
192
+
193
+ this.server.registerTool(
194
+ "writeFile",
195
+ {
196
+ description: "Store text as a file with the given path",
197
+ inputSchema: {
198
+ path: z.string().describe("Absolute path of the file"),
199
+ content: z.string().describe("The content to store")
200
+ }
201
+ },
202
+ async ({ path, content }) => {
203
+ try {
204
+ await env.BUCKET.put(path, content);
205
+ return textRes(`Successfully stored contents to ${path}`);
206
+ } catch (e: unknown) {
207
+ return textRes(`Couldn't save to file. Found error ${e}`);
208
+ }
209
+ }
210
+ );
211
+
212
+ this.server.registerTool(
213
+ "readFile",
214
+ {
215
+ description: "Read the contents of a file",
216
+ inputSchema: {
217
+ path: z.string().describe("Absolute path of the file to read")
218
+ }
219
+ },
220
+ async ({ path }) => {
221
+ const obj = await env.BUCKET.get(path);
222
+ if (!obj || !obj.body)
223
+ return textRes(`Error reading file at ${path}: not found`);
224
+ try {
225
+ return textRes(await obj.text());
226
+ } catch (e: unknown) {
227
+ return textRes(`Error reading file at ${path}: ${e}`);
228
+ }
229
+ }
230
+ );
231
+
232
+ this.server.registerTool(
233
+ "whoami",
234
+ {
235
+ description: "Check who the user is"
236
+ },
237
+ async () => {
238
+ return textRes(`${this.props?.userId}`);
239
+ }
240
+ );
241
+ }
242
+ }
243
+
244
+ // HTML form page for users to write our password
245
+ function passwordPage(opts: { query: string; error?: string }) {
246
+ const err = opts.error
247
+ ? `<p class="text-red-600 mb-2">${opts.error}</p>`
248
+ : "";
249
+ return new Response(
250
+ `<!doctype html>
251
+ <html lang="en">
252
+ <head>
253
+ <meta charset="utf-8" />
254
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
255
+ <title>ENTER THE MAGIC WORD</title>
256
+ <script src="https://cdn.tailwindcss.com"></script>
257
+ </head>
258
+ <body class="font-sans grid place-items-center min-h-screen bg-gray-100">
259
+ <form method="POST" action="/authorize?${opts.query}"
260
+ class="bg-white p-6 rounded-lg shadow-md w-full max-w-xs">
261
+ <h1 class="text-lg font-semibold mb-3">ENTER THE MAGIC WORD</h1>
262
+ ${err}
263
+ <label class="block text-sm mb-1">Password</label>
264
+ <input name="password" type="password" required autocomplete="current-password"
265
+ class="w-full border rounded px-3 py-2 mb-3" />
266
+ <button type="submit"
267
+ class="w-full py-2 bg-black text-white rounded font-medium hover:bg-gray-800">
268
+ Continue
269
+ </button>
270
+ </form>
271
+ </body>
272
+ </html>`,
273
+ { headers: { "content-type": "text/html; charset=utf-8" } }
274
+ );
275
+ }
276
+
277
+ // This is the default handler of our worker BEFORE requests are authenticated.
278
+ interface StorageEnv {
279
+ OAUTH_PROVIDER: OAuthHelpers;
280
+ SHARED_PASSWORD: string;
281
+ }
282
+
283
+ const defaultHandler = {
284
+ async fetch(request: Request, env: StorageEnv) {
285
+ const provider = env.OAUTH_PROVIDER;
286
+ const url = new URL(request.url);
287
+
288
+ // Only handle our auth UI/flow here
289
+ if (url.pathname !== "/authorize") {
290
+ return new Response("NOT FOUND", { status: 404 });
291
+ }
292
+
293
+ // Parse the OAuth request
294
+ const oauthReq = await provider.parseAuthRequest(request);
295
+
296
+ // We render the password page for GET requests
297
+ if (request.method === "GET") {
298
+ return passwordPage({ query: url.searchParams.toString() });
299
+ }
300
+
301
+ // We validate the password in POST requests
302
+ if (request.method === "POST") {
303
+ const form = await request.formData();
304
+ const password = String(form.get("password") || "");
305
+
306
+ const SHARED_PASSWORD = env.SHARED_PASSWORD; // Store this as a secret
307
+ if (!SHARED_PASSWORD) {
308
+ return new Response("Server misconfigured: missing SHARED_PASSWORD", {
309
+ status: 500
310
+ });
311
+ }
312
+ if (password !== SHARED_PASSWORD) {
313
+ return passwordPage({
314
+ query: url.searchParams.toString(),
315
+ error: "Wrong password."
316
+ });
317
+ }
318
+
319
+ // We give everyone the same userId
320
+ const userId = "friend";
321
+
322
+ const { redirectTo } = await provider.completeAuthorization({
323
+ request: oauthReq,
324
+ userId,
325
+ scope: [], // We don't care about scopes
326
+
327
+ // We could add anything we wanted here so we could access it
328
+ // within the MCP with `this.props`
329
+ props: { userId },
330
+ metadata: undefined
331
+ });
332
+
333
+ return Response.redirect(redirectTo, 302);
334
+ }
335
+
336
+ return new Response("Method Not Allowed", {
337
+ status: 405,
338
+ headers: { allow: "GET, POST" }
339
+ });
340
+ }
341
+ };
342
+
343
+ // OAuthProvider creates our worker handler
344
+ export default new OAuthProvider({
345
+ authorizeEndpoint: "/authorize",
346
+ tokenEndpoint: "/token",
347
+ clientRegistrationEndpoint: "/register",
348
+ apiHandlers: { "/mcp": StorageMcp.serve("/mcp") },
349
+ defaultHandler
350
+ });
351
+ ```
352
+
353
+ You would also add these to your `wrangler.jsonc`:
354
+
355
+ ```jsonc
356
+ {
357
+ // rest of your config...
358
+ "r2_buckets": [{ "binding": "BUCKET", "bucket_name": "your-bucket-name" }],
359
+ "kv_namespaces": [
360
+ {
361
+ "binding": "OAUTH_KV", // required by OAuthProvider
362
+ "id": "your-kv-id"
363
+ }
364
+ ]
365
+ }
366
+ ```
367
+
368
+ ### What's going on?
369
+
370
+ 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.
371
+
372
+ 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`.
373
+
374
+ Most of the code here is either the HTML page to type in the password or the OAuth `/authorize` logic.
375
+ 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.
376
+
377
+ ### Let's see how this looks like
378
+
379
+ Once again, using https://playground.ai.cloudflare.com:
380
+ ![password page](https://github.com/user-attachments/assets/8e469110-fffa-45d2-84c1-ae16a651ae41)
381
+ The auth flow prompts us for the password.
382
+
383
+ ![model calls all 3 tools after authorization](https://github.com/user-attachments/assets/07e22fef-93de-47c2-af7e-9c361e460186)
384
+ Once we've authenticated ourselves we can use all the tools!
385
+
386
+ ## Data Jurisdiction for Compliance
387
+
388
+ `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.
389
+
390
+ ### Using the EU Jurisdiction for GDPR
391
+
392
+ 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:
393
+
394
+ ```typescript
395
+ export default TinyMcp.serve("/", {
396
+ jurisdiction: "eu"
397
+ });
398
+ ```
399
+
400
+ Or with the OAuth-protected example:
401
+
402
+ ```typescript
403
+ export default new OAuthProvider({
404
+ authorizeEndpoint: "/authorize",
405
+ tokenEndpoint: "/token",
406
+ clientRegistrationEndpoint: "/register",
407
+ apiHandlers: {
408
+ "/mcp": StorageMcp.serve("/mcp", { jurisdiction: "eu" })
409
+ },
410
+ defaultHandler
411
+ });
412
+ ```
413
+
414
+ When you specify `jurisdiction: "eu"`, Cloudflare will create the Durable Object instances in EU data centers, ensuring that:
415
+
416
+ - All MCP session data stays within the EU
417
+ - User data processed by your tools remains in the EU
418
+ - State stored in the Durable Object's storage API stays in the EU
419
+
420
+ This helps you comply with GDPR's data localization requirements without any additional configuration.
421
+
422
+ ### Available Jurisdictions
423
+
424
+ The `jurisdiction` option accepts any value supported by [Cloudflare's Durable Objects jurisdiction API](https://developers.cloudflare.com/durable-objects/reference/data-location/), including:
425
+
426
+ - `"eu"` - European Union
427
+ - `"fedramp"` - FedRAMP compliant locations
428
+
429
+ ## Elicitation (Human-in-the-Loop)
430
+
431
+ 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.
432
+
433
+ Elicitation is supported via `McpAgent` (which manages the request/response lifecycle through Durable Object storage) or via `WorkerTransport` (for stateful non-McpAgent setups).
434
+
435
+ ```typescript
436
+ import { McpAgent } from "agents/mcp";
437
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
438
+ import { z } from "zod";
439
+
440
+ export class MyMCP extends McpAgent<Env, { counter: number }> {
441
+ server = new McpServer({ name: "Elicitation Demo", version: "1.0.0" });
442
+
443
+ initialState = { counter: 0 };
444
+
445
+ async init() {
446
+ this.server.registerTool(
447
+ "increase-counter",
448
+ {
449
+ description: "Increase the counter",
450
+ inputSchema: {
451
+ confirm: z.boolean().describe("Do you want to increase the counter?")
452
+ }
453
+ },
454
+ async ({ confirm }, extra) => {
455
+ if (!confirm) {
456
+ return { content: [{ type: "text", text: "Cancelled." }] };
457
+ }
458
+
459
+ const result = await this.server.server.elicitInput(
460
+ {
461
+ message: "By how much?",
462
+ requestedSchema: {
463
+ type: "object",
464
+ properties: {
465
+ amount: { type: "number", title: "Amount" }
466
+ },
467
+ required: ["amount"]
468
+ }
469
+ },
470
+ { relatedRequestId: extra.requestId }
471
+ );
472
+
473
+ if (result.action !== "accept" || !result.content?.amount) {
474
+ return { content: [{ type: "text", text: "Cancelled." }] };
475
+ }
476
+
477
+ const amount = Number(result.content.amount);
478
+ this.setState({ counter: this.state.counter + amount });
479
+
480
+ return {
481
+ content: [
482
+ {
483
+ type: "text",
484
+ text: `Counter increased by ${amount}, now ${this.state.counter}`
485
+ }
486
+ ]
487
+ };
488
+ }
489
+ );
490
+ }
491
+ }
492
+
493
+ export default MyMCP.serve("/mcp");
494
+ ```
495
+
496
+ See the [`examples/mcp-elicitation`](https://github.com/cloudflare/agents/tree/main/examples/mcp-elicitation) example for a full working demo.
497
+
498
+ ## WorkerTransport
499
+
500
+ `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`.
501
+
502
+ ```typescript
503
+ import { WorkerTransport, type TransportState } from "agents/mcp";
504
+
505
+ const transport = new WorkerTransport({
506
+ sessionIdGenerator: () => crypto.randomUUID(),
507
+ enableJsonResponse: false,
508
+ storage: {
509
+ get: () => kv.get<TransportState>("mcp_state"),
510
+ set: (state: TransportState) => kv.put<TransportState>("mcp_state", state)
511
+ }
512
+ });
513
+ ```
514
+
515
+ Key options:
516
+
517
+ | Option | Description |
518
+ | -------------------- | ------------------------------------------------------------------------------ |
519
+ | `sessionIdGenerator` | Function that returns a session ID for new sessions |
520
+ | `enableJsonResponse` | Return JSON instead of SSE streams (default: `false`) |
521
+ | `storage` | Optional `{ get, set }` adapter for persisting transport state across requests |
522
+ | `corsOptions` | CORS configuration |
523
+
524
+ ### Read more
525
+
526
+ For more complex examples including authentication with third-party providers, see the [examples directory](https://github.com/cloudflare/agents/tree/main/examples).