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.
- package/README.md +4 -2
- package/dist/{agent-routing-CDbpVjYq.d.ts → agent-routing-wS1TimZ_.d.ts} +64 -603
- package/dist/agent-routing.d.ts +1 -1
- package/dist/agent-tool-types.d.ts +1 -1
- package/dist/{agent-tools-C8DH8Yii.d.ts → agent-tools-Bamxxb6Y.d.ts} +2 -2
- package/dist/agent-tools.d.ts +1 -1
- package/dist/{ai-iXw0uOGj.d.ts → ai-CDn3Abax.d.ts} +30 -4
- package/dist/{ai-sdk-DRPh2rRa.js → ai-sdk-Kon40i0q.js} +2 -36
- package/dist/ai-sdk-Kon40i0q.js.map +1 -0
- package/dist/ai-sdk-hW3vntnk.d.ts +72 -0
- package/dist/browser/ai.d.ts +1 -1
- package/dist/browser/ai.js +16 -3
- package/dist/browser/ai.js.map +1 -1
- package/dist/browser/index.d.ts +64 -16
- package/dist/browser/index.js +4 -4
- package/dist/browser/tools/ai-sdk.d.ts +7 -70
- package/dist/browser/tools/ai-sdk.js +4 -4
- package/dist/browser/tools/ai-sdk.js.map +1 -1
- package/dist/browser/tools/pi.d.ts +9 -15
- package/dist/browser/tools/pi.js +6 -14
- package/dist/browser/tools/pi.js.map +1 -1
- package/dist/browser/tools/tanstack-ai.d.ts +46 -23
- package/dist/browser/tools/tanstack-ai.js +8 -4
- package/dist/browser/tools/tanstack-ai.js.map +1 -1
- package/dist/{browser-DccPw_wW.js → browser-1eUbPvC2.js} +413 -14
- package/dist/browser-1eUbPvC2.js.map +1 -0
- package/dist/{browser-DlXhSAdf.d.ts → browser-D9RNpJuT.d.ts} +254 -20
- package/dist/browser-tool-BhjCcNVE.d.ts +71 -0
- package/dist/browser-tool-lGnvEBLJ.js +497 -0
- package/dist/browser-tool-lGnvEBLJ.js.map +1 -0
- package/dist/capability-id-B3J8Nok_.js +13 -0
- package/dist/capability-id-B3J8Nok_.js.map +1 -0
- package/dist/chat/index.d.ts +2 -2
- package/dist/chat-sdk/index.d.ts +3 -3
- package/dist/chat-sdk/index.js +1 -1
- package/dist/client-4OlMIJx-.js.map +1 -1
- package/dist/{client-BnYbNT3z.js → client-DMWujQJ1.js} +68 -244
- package/dist/client-DMWujQJ1.js.map +1 -0
- package/dist/{client-C27CtRZr.d.ts → client-wn4-yqGz.d.ts} +9 -7
- package/dist/client.d.ts +1 -1
- package/dist/{connector-DAXBOIIi.js → connector-CM-uuxKM.js} +96 -8
- package/dist/connector-CM-uuxKM.js.map +1 -0
- package/dist/{connector-20DJ6p4k.d.ts → connector-DZ71EjE-.d.ts} +109 -9
- package/dist/experimental/webmcp.js +1 -1
- package/dist/{handler-stateless-Bly_nSH3.js → handler-stateless-B5hU3dxK.js} +35 -2
- package/dist/handler-stateless-B5hU3dxK.js.map +1 -0
- package/dist/{handler-stateless-DxYpJ_XF.d.ts → handler-stateless-C90kD5_R.d.ts} +3 -47
- package/dist/harness/container/index.d.ts +15 -15
- package/dist/harness/opencode/index.js.map +1 -1
- package/dist/harness/pi/index.d.ts +7 -3
- package/dist/harness/pi/index.js +46 -15
- package/dist/harness/pi/index.js.map +1 -1
- package/dist/harness/think/index.d.ts +2 -1
- package/dist/{images-xaDv2Fqe.js → images-CfuRCjwq.js} +1 -5
- package/dist/images-CfuRCjwq.js.map +1 -0
- package/dist/{index-DCxk3OSJ.d.ts → index-8k4qsX1T.d.ts} +2 -28
- package/dist/index.d.ts +14 -14
- package/dist/index.js +2 -2
- package/dist/internal-hosts-Kfp5A392.d.ts +230 -0
- package/dist/internal-kmw70xwR.js.map +1 -1
- package/dist/invoker-BC1SZ3q1.js +26 -0
- package/dist/invoker-BC1SZ3q1.js.map +1 -0
- package/dist/invoker-DoLFPJZV.d.ts +21 -0
- package/dist/mcp/client/index.d.ts +15 -1
- package/dist/mcp/client/index.js +2 -2
- package/dist/mcp/client/x402.d.ts +7 -5
- package/dist/mcp/client/x402.js +34 -1
- package/dist/mcp/client/x402.js.map +1 -1
- package/dist/mcp/index.d.ts +561 -27
- package/dist/mcp/index.js +180 -3
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/server/index.d.ts +1 -1
- package/dist/mcp/server/index.js +1 -1
- package/dist/models/ai-sdk/index.d.ts +2 -2
- package/dist/models/ai-sdk/index.js +1 -1
- package/dist/models/opencode/index.d.ts +2 -2
- package/dist/models/opencode/index.js +1 -1
- package/dist/models/pi-ai/index.d.ts +3 -28
- package/dist/models/pi-ai/index.js +31 -16
- package/dist/models/pi-ai/index.js.map +1 -1
- package/dist/protocol-BEgrJYC5.d.ts +29 -29
- package/dist/react.d.ts +2 -2
- package/dist/routing/index.d.ts +1 -1
- package/dist/serializable.d.ts +1 -1
- package/dist/{session-store-yvwq1f9x.js → session-7CDgVE0z.js} +455 -33
- package/dist/session-7CDgVE0z.js.map +1 -0
- package/dist/{session-store-_IK6dV-w.d.ts → session-store-B7Ca-u1t.d.ts} +8 -1
- package/dist/settings-Dk2il2Hf.js.map +1 -1
- package/dist/{src-DfLzdwlK.js → src-BGolXG_H.js} +18 -2
- package/dist/src-BGolXG_H.js.map +1 -0
- package/dist/sub-routing.d.ts +12 -12
- package/dist/tool-CTp72Jb_.d.ts +10 -10
- package/dist/tool-DoVberpN.d.ts +22 -22
- package/dist/tool-helpers-BodWi4gd.js +256 -0
- package/dist/tool-helpers-BodWi4gd.js.map +1 -0
- package/dist/{transport-CPh7YGiw.d.ts → transport-5moD2pW5.d.ts} +1 -43
- package/dist/types-D2naO3rA.d.ts +49 -0
- package/dist/voice/index.d.ts +1 -1
- package/dist/workflow-types.d.ts +1 -1
- package/dist/workflows.d.ts +1 -1
- package/dist/workflows.js +1 -1
- package/docs/browse-the-web.md +247 -413
- package/docs/get-current-agent.md +4 -7
- package/docs/harnesses/pi.md +28 -28
- package/docs/human-in-the-loop.md +3 -7
- package/docs/index.md +6 -6
- package/docs/mcp-client.md +4 -28
- package/docs/mcp-servers.md +137 -411
- package/docs/mcp-transports.md +30 -289
- package/docs/models-pi-ai.md +9 -11
- package/docs/models.md +1 -8
- package/docs/routing.md +0 -2
- package/docs/securing-mcp-servers.md +120 -304
- package/package.json +8 -3
- package/dist/ai-sdk-DRPh2rRa.js.map +0 -1
- package/dist/browser-DccPw_wW.js.map +0 -1
- package/dist/browser-tool-B1mhifin.js +0 -616
- package/dist/browser-tool-B1mhifin.js.map +0 -1
- package/dist/browser-tool-Cxjk5sgG.d.ts +0 -92
- package/dist/client-BnYbNT3z.js.map +0 -1
- package/dist/connector-DAXBOIIi.js.map +0 -1
- package/dist/handler-stateless-Bly_nSH3.js.map +0 -1
- package/dist/images-xaDv2Fqe.js.map +0 -1
- package/dist/invoker-CG0_p_Wq.d.ts +0 -20
- package/dist/invoker-CHMnoxIA.js +0 -57
- package/dist/invoker-CHMnoxIA.js.map +0 -1
- package/dist/session-store-yvwq1f9x.js.map +0 -1
- package/dist/src-DfLzdwlK.js.map +0 -1
- package/dist/tool-helpers-O5BDVEAY.js +0 -58
- package/dist/tool-helpers-O5BDVEAY.js.map +0 -1
package/docs/mcp-servers.md
CHANGED
|
@@ -1,33 +1,19 @@
|
|
|
1
1
|
# Creating MCP Servers
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
##
|
|
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
|
|
46
|
+
return handler(request, env, ctx);
|
|
59
47
|
}
|
|
60
48
|
} satisfies ExportedHandler;
|
|
61
49
|
```
|
|
62
50
|
|
|
63
|
-
|
|
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
|
-
```
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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`
|
|
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
|
-
|
|
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
|
-
|
|
66
|
+
## Keep state in bindings
|
|
112
67
|
|
|
113
|
-
|
|
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 {
|
|
71
|
+
import { env } from "cloudflare:workers";
|
|
117
72
|
|
|
118
73
|
server.registerTool(
|
|
119
|
-
"
|
|
120
|
-
{
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
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 {
|
|
151
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
152
|
-
import { z } from "zod";
|
|
93
|
+
import { Agent, getAgentByName } from "agents";
|
|
153
94
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
this.
|
|
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
|
-
|
|
173
|
-
|
|
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
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
"
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
"
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
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
|
-
|
|
136
|
+
### Origin and Host validation
|
|
204
137
|
|
|
205
|
-
`
|
|
206
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
144
|
+
### Notify clients of changes
|
|
212
145
|
|
|
213
|
-
|
|
214
|
-

|
|
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
|
-
##
|
|
148
|
+
## Authentication
|
|
217
149
|
|
|
218
|
-
|
|
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 {
|
|
222
|
-
import {
|
|
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
|
-
|
|
231
|
-
server = new McpServer({ name: "", version: "
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
324
|
-
interface StorageEnv {
|
|
325
|
-
OAUTH_PROVIDER: OAuthHelpers;
|
|
326
|
-
SHARED_PASSWORD: string;
|
|
180
|
+
return server;
|
|
327
181
|
}
|
|
328
182
|
|
|
329
|
-
const
|
|
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
|
-
|
|
395
|
-
|
|
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
|
-

|
|
427
|
-
The auth flow prompts us for the password.
|
|
428
|
-
|
|
429
|
-

|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
207
|
+
## Elicitation
|
|
480
208
|
|
|
481
|
-
|
|
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: "
|
|
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
|
|
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
|
-
|
|
267
|
+
## 2025-era clients
|
|
540
268
|
|
|
541
|
-
|
|
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
|
-
|
|
271
|
+
The handler does not serve the standalone SSE transport.
|
|
544
272
|
|
|
545
|
-
##
|
|
273
|
+
## Lower-level control
|
|
546
274
|
|
|
547
|
-
|
|
275
|
+
If the handler lifecycle does not fit, use the SDK's `WebStandardStreamableHTTPServerTransport` from `@modelcontextprotocol/server` directly in your Worker.
|
|
548
276
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
295
|
+
## Examples
|
|
572
296
|
|
|
573
|
-
|
|
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
|