agents 0.30.0 → 0.30.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/harness/pi/index.d.ts +3 -0
- package/dist/harness/pi/index.js +15 -5
- package/dist/harness/pi/index.js.map +1 -1
- package/docs/get-current-agent.md +4 -7
- package/docs/harnesses/pi.md +1 -1
- package/docs/human-in-the-loop.md +3 -7
- package/docs/index.md +2 -2
- package/docs/mcp-client.md +2 -28
- package/docs/mcp-servers.md +121 -414
- package/docs/mcp-transports.md +30 -289
- package/docs/routing.md +0 -2
- package/docs/securing-mcp-servers.md +7 -2
- package/package.json +1 -1
package/docs/mcp-servers.md
CHANGED
|
@@ -1,33 +1,17 @@
|
|
|
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
|
+
Use the exact `@modelcontextprotocol/server` version required by your installed Agents release.
|
|
12
|
+
|
|
13
|
+
## Create a server
|
|
14
|
+
|
|
31
15
|
```typescript
|
|
32
16
|
import { McpServer } from "@modelcontextprotocol/server";
|
|
33
17
|
import { createMcpHandler } from "agents/mcp/server";
|
|
@@ -53,432 +37,171 @@ function createServer() {
|
|
|
53
37
|
return server;
|
|
54
38
|
}
|
|
55
39
|
|
|
40
|
+
const handler = createMcpHandler(createServer);
|
|
41
|
+
|
|
56
42
|
export default {
|
|
57
43
|
fetch(request, env, ctx) {
|
|
58
|
-
return
|
|
44
|
+
return handler(request, env, ctx);
|
|
59
45
|
}
|
|
60
46
|
} satisfies ExportedHandler;
|
|
61
47
|
```
|
|
62
48
|
|
|
63
|
-
|
|
49
|
+
The server is served at `/mcp` by default. `agents/mcp/server` uses `AsyncLocalStorage`, so enable `nodejs_compat`:
|
|
64
50
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
},
|
|
73
|
-
allowedHostnames: ["mcp.example.com"], // custom-domain Host policy
|
|
74
|
-
allowedOriginHostnames: ["app.example.com"], // browser Origin policy
|
|
75
|
-
authContext: { props: {} }, // optional application props override
|
|
76
|
-
legacy: "stateless", // upstream default; use "reject" for Stateless-only
|
|
77
|
-
responseMode: "auto" // upstream SDK response shaping
|
|
78
|
-
});
|
|
51
|
+
```jsonc
|
|
52
|
+
{
|
|
53
|
+
"name": "hello-mcp",
|
|
54
|
+
"main": "src/index.ts",
|
|
55
|
+
"compatibility_date": "2026-06-11",
|
|
56
|
+
"compatibility_flags": ["nodejs_compat"]
|
|
57
|
+
}
|
|
79
58
|
```
|
|
80
59
|
|
|
81
|
-
`createMcpHandler`
|
|
82
|
-
|
|
83
|
-
To publish change events, use the returned handler's `notify` methods, such as `handler.notify.toolsChanged()`. Create the handler once at module scope and reuse it for requests and notifications. Notifications are isolate-local: they do not reach subscriptions in other Worker isolates. See the [handler notification API](https://developers.cloudflare.com/agents/model-context-protocol/apis/handler-api/#publish-list-and-resource-changes) for the available methods.
|
|
84
|
-
|
|
85
|
-
Use `createLegacyMcpHandler` for SDK v1 WorkerTransport, storage, session, and event-store options. The SDK v2 `bus` option is not available on that handler either.
|
|
86
|
-
|
|
87
|
-
The handler validates every present `Origin` header before serving the request. It rejects malformed, opaque, and non-HTTP origins. Requests without `Origin` remain valid for non-browser MCP clients.
|
|
88
|
-
|
|
89
|
-
Its default allowlist includes localhost-class origins and the endpoint's `workers.dev` hostname. A concrete `corsOptions.origin` adds that hostname automatically. The handler also applies matching Host checks to localhost and `workers.dev` endpoints. For a custom domain with wildcard CORS, configure `allowedHostnames` and `allowedOriginHostnames` explicitly. Values are hostnames without a scheme or port, and matching ignores the Origin's scheme and port.
|
|
90
|
-
|
|
91
|
-
Pass `allowedOriginHostnames: "*"` only when equivalent Origin validation runs in trusted middleware before the handler. This explicitly disables the handler's Origin check, including malformed and opaque Origin rejection. MCP HTTP servers are required to validate browser Origins, so do not use this as an unauthenticated public-server shortcut.
|
|
92
|
-
|
|
93
|
-
### 2025 compatibility and elicitation
|
|
94
|
-
|
|
95
|
-
The default `legacy: "stateless"` lane supports ordinary 2025 tools, resources, and prompts. It has no session return path for push-style server-to-client requests; attempts to sample, elicit, or list roots fail immediately with guidance to use a sessionful transport.
|
|
96
|
-
|
|
97
|
-
Applications that must serve both generations can route before `createMcpHandler`: send Stateless requests to a strict handler and Legacy requests to an existing session-addressed Agent or transport. See [`examples/mcp-elicitation-mrtr`](../../examples/mcp-elicitation-mrtr/) for Stateless Elicitation and [`examples/mcp-elicitation`](../../examples/mcp-elicitation/) for Legacy Elicitation.
|
|
98
|
-
|
|
99
|
-
For an explicit Legacy handler, use the retained Legacy API:
|
|
100
|
-
|
|
101
|
-
```typescript
|
|
102
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
103
|
-
import { createLegacyMcpHandler } from "agents/mcp";
|
|
104
|
-
|
|
105
|
-
const server = new McpServer({ name: "legacy", version: "1.0.0" });
|
|
106
|
-
export default createLegacyMcpHandler(server);
|
|
107
|
-
```
|
|
60
|
+
`createMcpHandler` takes a factory, not a server instance, so concurrent requests never share one `McpServer`. Create the handler once at module scope and call it for each request.
|
|
108
61
|
|
|
109
|
-
|
|
62
|
+
Deploy the Worker and connect any MCP client to `https://<your-worker>.workers.dev/mcp`, or run `wrangler dev` and point the [MCP Inspector](https://github.com/modelcontextprotocol/inspector) at `http://localhost:8787/mcp`.
|
|
110
63
|
|
|
111
|
-
|
|
64
|
+
## Keep state in bindings
|
|
112
65
|
|
|
113
|
-
|
|
66
|
+
Nothing survives between requests inside the handler. Keep data in bindings and read them from your tools with `env` from `cloudflare:workers`:
|
|
114
67
|
|
|
115
68
|
```typescript
|
|
116
|
-
import {
|
|
69
|
+
import { env } from "cloudflare:workers";
|
|
117
70
|
|
|
118
71
|
server.registerTool(
|
|
119
|
-
"
|
|
120
|
-
{
|
|
121
|
-
|
|
122
|
-
|
|
72
|
+
"readFile",
|
|
73
|
+
{
|
|
74
|
+
description: "Read a file from the shared bucket",
|
|
75
|
+
inputSchema: { path: z.string() }
|
|
76
|
+
},
|
|
77
|
+
async ({ path }) => {
|
|
78
|
+
const object = await env.BUCKET.get(path);
|
|
123
79
|
return {
|
|
124
80
|
content: [
|
|
125
|
-
{
|
|
126
|
-
type: "text",
|
|
127
|
-
text: JSON.stringify({
|
|
128
|
-
clientId: context.http?.authInfo?.clientId,
|
|
129
|
-
scopes: context.http?.authInfo?.scopes,
|
|
130
|
-
props: auth?.props
|
|
131
|
-
})
|
|
132
|
-
}
|
|
81
|
+
{ type: "text", text: object ? await object.text() : "Not found" }
|
|
133
82
|
]
|
|
134
83
|
};
|
|
135
84
|
}
|
|
136
85
|
);
|
|
137
86
|
```
|
|
138
87
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
## Stateful MCP Server with `McpAgent` (legacy)
|
|
142
|
-
|
|
143
|
-
`McpAgent` gives each client session its own Durable Object with persistent state. It remains available for existing SDK v1 deployments but is deprecated and feature-frozen; new servers should use the stateless handler or explicitly compose a separate legacy route where a session is required.
|
|
144
|
-
|
|
145
|
-
### Writing TinyMCP
|
|
146
|
-
|
|
147
|
-
Prototyping is very easy! If you want to quickly deploy an MCP, it only takes ~20 lines of code:
|
|
88
|
+
For per-user state, call an Agent from the tool. The Agent owns the data, and the MCP server stays stateless:
|
|
148
89
|
|
|
149
90
|
```typescript
|
|
150
|
-
import {
|
|
151
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
152
|
-
import { z } from "zod";
|
|
91
|
+
import { Agent, getAgentByName } from "agents";
|
|
153
92
|
|
|
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
|
-
);
|
|
93
|
+
export class Notes extends Agent<Env, { notes: string[] }> {
|
|
94
|
+
initialState = { notes: [] };
|
|
95
|
+
|
|
96
|
+
add(note: string) {
|
|
97
|
+
this.setState({ notes: [...this.state.notes, note] });
|
|
98
|
+
return this.state.notes.length;
|
|
169
99
|
}
|
|
170
100
|
}
|
|
171
101
|
|
|
172
|
-
|
|
173
|
-
|
|
102
|
+
server.registerTool(
|
|
103
|
+
"addNote",
|
|
104
|
+
{ inputSchema: { note: z.string() } },
|
|
105
|
+
async ({ note }) => {
|
|
106
|
+
const userId = String(getMcpAuthContext()?.props.userId ?? "anonymous");
|
|
107
|
+
const notes = await getAgentByName(env.Notes, userId);
|
|
108
|
+
const count = await notes.add(note);
|
|
109
|
+
return { content: [{ type: "text", text: `You have ${count} notes` }] };
|
|
110
|
+
}
|
|
111
|
+
);
|
|
174
112
|
```
|
|
175
113
|
|
|
176
|
-
|
|
114
|
+
Bind `Notes` as a Durable Object in `wrangler.jsonc` and export the class from your Worker. `getMcpAuthContext()` is covered in [Authentication](#authentication).
|
|
177
115
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
"
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
"bindings": [
|
|
186
|
-
{
|
|
187
|
-
"name": "MCP_OBJECT",
|
|
188
|
-
"class_name": "TinyMcp"
|
|
189
|
-
}
|
|
190
|
-
]
|
|
116
|
+
## Handler options
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
createMcpHandler(() => createServer(), {
|
|
120
|
+
route: "/mcp", // exact path to handle (default: "/mcp")
|
|
121
|
+
corsOptions: {
|
|
122
|
+
origin: "https://app.example.com"
|
|
191
123
|
},
|
|
192
|
-
"
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
124
|
+
allowedHostnames: ["mcp.example.com"], // custom-domain Host policy
|
|
125
|
+
allowedOriginHostnames: ["app.example.com"], // browser Origin policy
|
|
126
|
+
authContext: { props: {} }, // optional application props override
|
|
127
|
+
legacy: "stateless", // default; use "reject" to serve only Stateless clients
|
|
128
|
+
responseMode: "auto" // upstream SDK response shaping
|
|
129
|
+
});
|
|
199
130
|
```
|
|
200
131
|
|
|
201
|
-
|
|
132
|
+
`createMcpHandler` supports the upstream SDK v2 handler options except `bus`, which the Agents SDK does not expose. Supplying `bus` throws a `TypeError`.
|
|
202
133
|
|
|
203
|
-
|
|
134
|
+
### Origin and Host validation
|
|
204
135
|
|
|
205
|
-
`
|
|
206
|
-
In there you'll normally setup all your tools/resources and anything else you might need. In this case, we're only setting the tool `square`.
|
|
136
|
+
The handler validates every present `Origin` header before serving the request. It rejects malformed, opaque, and non-HTTP origins. Requests without `Origin` remain valid for non-browser MCP clients.
|
|
207
137
|
|
|
208
|
-
|
|
209
|
-
It returns an object with a `fetch` handler that can act as our Worker entrypoint and deal with the Streamable HTTP transport for us, so we can deploy our MCP directly!
|
|
138
|
+
Its default allowlist includes localhost-class origins and the endpoint's `workers.dev` hostname. A concrete `corsOptions.origin` adds that hostname automatically. The handler also applies matching Host checks to localhost and `workers.dev` endpoints. For a custom domain with wildcard CORS, configure `allowedHostnames` and `allowedOriginHostnames` explicitly. Values are hostnames without a scheme or port, and matching ignores the Origin's scheme and port.
|
|
210
139
|
|
|
211
|
-
|
|
140
|
+
Pass `allowedOriginHostnames: "*"` only when equivalent Origin validation runs in trusted middleware before the handler. This disables the handler's Origin check, including malformed and opaque Origin rejection. MCP HTTP servers are required to validate browser Origins, so do not use this as an unauthenticated public-server shortcut.
|
|
212
141
|
|
|
213
|
-
|
|
214
|
-

|
|
142
|
+
### Notify clients of changes
|
|
215
143
|
|
|
216
|
-
|
|
144
|
+
Use the returned handler's `notify` methods, such as `handler.notify.toolsChanged()`, to publish list and resource changes. Notifications are isolate-local: they do not reach subscriptions in other Worker isolates. See the [handler notification API](https://developers.cloudflare.com/agents/model-context-protocol/apis/handler-api/#publish-list-and-resource-changes) for the available methods.
|
|
217
145
|
|
|
218
|
-
|
|
146
|
+
## Authentication
|
|
219
147
|
|
|
220
|
-
|
|
221
|
-
import { McpAgent } from "agents/mcp";
|
|
222
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
223
|
-
import {
|
|
224
|
-
OAuthProvider,
|
|
225
|
-
type OAuthHelpers
|
|
226
|
-
} from "@cloudflare/workers-oauth-provider";
|
|
227
|
-
import { z } from "zod";
|
|
228
|
-
import { env } from "cloudflare:workers";
|
|
148
|
+
Wrap the handler with `OAuthProvider` from `@cloudflare/workers-oauth-provider` to require OAuth 2.1. The provider serves the authorization, token, and registration endpoints and validates Bearer tokens on `/mcp`:
|
|
229
149
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
async init() {
|
|
234
|
-
// Helper to return text responses from our tools
|
|
235
|
-
const textRes = (text: string) => ({
|
|
236
|
-
content: [{ type: "text" as const, text }]
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
this.server.registerTool(
|
|
240
|
-
"writeFile",
|
|
241
|
-
{
|
|
242
|
-
description: "Store text as a file with the given path",
|
|
243
|
-
inputSchema: {
|
|
244
|
-
path: z.string().describe("Absolute path of the file"),
|
|
245
|
-
content: z.string().describe("The content to store")
|
|
246
|
-
}
|
|
247
|
-
},
|
|
248
|
-
async ({ path, content }) => {
|
|
249
|
-
try {
|
|
250
|
-
await env.BUCKET.put(path, content);
|
|
251
|
-
return textRes(`Successfully stored contents to ${path}`);
|
|
252
|
-
} catch (e: unknown) {
|
|
253
|
-
return textRes(`Couldn't save to file. Found error ${e}`);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
);
|
|
257
|
-
|
|
258
|
-
this.server.registerTool(
|
|
259
|
-
"readFile",
|
|
260
|
-
{
|
|
261
|
-
description: "Read the contents of a file",
|
|
262
|
-
inputSchema: {
|
|
263
|
-
path: z.string().describe("Absolute path of the file to read")
|
|
264
|
-
}
|
|
265
|
-
},
|
|
266
|
-
async ({ path }) => {
|
|
267
|
-
const obj = await env.BUCKET.get(path);
|
|
268
|
-
if (!obj || !obj.body)
|
|
269
|
-
return textRes(`Error reading file at ${path}: not found`);
|
|
270
|
-
try {
|
|
271
|
-
return textRes(await obj.text());
|
|
272
|
-
} catch (e: unknown) {
|
|
273
|
-
return textRes(`Error reading file at ${path}: ${e}`);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
);
|
|
150
|
+
```typescript
|
|
151
|
+
import { OAuthProvider } from "@cloudflare/workers-oauth-provider";
|
|
152
|
+
import { createMcpHandler, getMcpAuthContext } from "agents/mcp/server";
|
|
277
153
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
{
|
|
281
|
-
description: "Check who the user is"
|
|
282
|
-
},
|
|
283
|
-
async () => {
|
|
284
|
-
return textRes(`${this.props?.userId}`);
|
|
285
|
-
}
|
|
286
|
-
);
|
|
287
|
-
}
|
|
288
|
-
}
|
|
154
|
+
function createServer() {
|
|
155
|
+
const server = new McpServer({ name: "Authenticated", version: "1.0.0" });
|
|
289
156
|
|
|
290
|
-
|
|
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" } }
|
|
157
|
+
server.registerTool(
|
|
158
|
+
"whoami",
|
|
159
|
+
{ description: "Returns the authenticated user" },
|
|
160
|
+
async (context) => {
|
|
161
|
+
const auth = getMcpAuthContext();
|
|
162
|
+
return {
|
|
163
|
+
content: [
|
|
164
|
+
{
|
|
165
|
+
type: "text",
|
|
166
|
+
text: JSON.stringify({
|
|
167
|
+
userId: auth?.props.userId,
|
|
168
|
+
clientId: context.http?.authInfo?.clientId,
|
|
169
|
+
scopes: context.http?.authInfo?.scopes
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
]
|
|
173
|
+
};
|
|
174
|
+
}
|
|
320
175
|
);
|
|
321
|
-
}
|
|
322
176
|
|
|
323
|
-
|
|
324
|
-
interface StorageEnv {
|
|
325
|
-
OAUTH_PROVIDER: OAuthHelpers;
|
|
326
|
-
SHARED_PASSWORD: string;
|
|
177
|
+
return server;
|
|
327
178
|
}
|
|
328
179
|
|
|
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
|
-
}
|
|
338
|
-
|
|
339
|
-
// Parse the OAuth request
|
|
340
|
-
const oauthReq = await provider.parseAuthRequest(request);
|
|
341
|
-
|
|
342
|
-
// We render the password page for GET requests
|
|
343
|
-
if (request.method === "GET") {
|
|
344
|
-
return passwordPage({ query: url.searchParams.toString() });
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// We validate the password in POST requests
|
|
348
|
-
if (request.method === "POST") {
|
|
349
|
-
const form = await request.formData();
|
|
350
|
-
const password = String(form.get("password") || "");
|
|
351
|
-
|
|
352
|
-
const SHARED_PASSWORD = env.SHARED_PASSWORD; // Store this as a secret
|
|
353
|
-
if (!SHARED_PASSWORD) {
|
|
354
|
-
return new Response("Server misconfigured: missing SHARED_PASSWORD", {
|
|
355
|
-
status: 500
|
|
356
|
-
});
|
|
357
|
-
}
|
|
358
|
-
if (password !== SHARED_PASSWORD) {
|
|
359
|
-
return passwordPage({
|
|
360
|
-
query: url.searchParams.toString(),
|
|
361
|
-
error: "Wrong password."
|
|
362
|
-
});
|
|
363
|
-
}
|
|
180
|
+
const mcpHandler = createMcpHandler(createServer);
|
|
364
181
|
|
|
365
|
-
// We give everyone the same userId
|
|
366
|
-
const userId = "friend";
|
|
367
|
-
|
|
368
|
-
const { redirectTo } = await provider.completeAuthorization({
|
|
369
|
-
request: oauthReq,
|
|
370
|
-
userId,
|
|
371
|
-
scope: [], // We don't care about scopes
|
|
372
|
-
|
|
373
|
-
// We could add anything we wanted here so we could access it
|
|
374
|
-
// within the MCP with `this.props`
|
|
375
|
-
props: { userId },
|
|
376
|
-
metadata: undefined
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
return Response.redirect(redirectTo, 302);
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
return new Response("Method Not Allowed", {
|
|
383
|
-
status: 405,
|
|
384
|
-
headers: { allow: "GET, POST" }
|
|
385
|
-
});
|
|
386
|
-
}
|
|
387
|
-
};
|
|
388
|
-
|
|
389
|
-
// OAuthProvider creates our worker handler
|
|
390
182
|
export default new OAuthProvider({
|
|
391
183
|
authorizeEndpoint: "/authorize",
|
|
392
|
-
tokenEndpoint: "/token",
|
|
393
|
-
clientRegistrationEndpoint: "/register",
|
|
394
|
-
|
|
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"
|
|
184
|
+
tokenEndpoint: "/oauth/token",
|
|
185
|
+
clientRegistrationEndpoint: "/oauth/register",
|
|
186
|
+
apiRoute: "/mcp",
|
|
187
|
+
apiHandler: {
|
|
188
|
+
fetch(request: Request, env: Env, ctx: ExecutionContext) {
|
|
189
|
+
return mcpHandler(request, env, ctx);
|
|
409
190
|
}
|
|
410
|
-
]
|
|
411
|
-
}
|
|
412
|
-
```
|
|
413
|
-
|
|
414
|
-
### What's going on?
|
|
415
|
-
|
|
416
|
-
In ~160 lines we were able to write our custom OAuth authorization flow so anyone that knows our secret password can use the MCP server.
|
|
417
|
-
|
|
418
|
-
Just like before, in `init()` we set a few tools to access files in our R2 bucket. We also have the `whoami` tool to show users what `userId` we authenticated them with. It's just an example of how to access `props` from within the `McpAgent`.
|
|
419
|
-
|
|
420
|
-
Most of the code here is either the HTML page to type in the password or the OAuth `/authorize` logic.
|
|
421
|
-
The important part is to notice how in the `OAuthProvider` we expose the `StorageMcp` through the `apiHandlers` key and use the same `serve` method we were using before.
|
|
422
|
-
|
|
423
|
-
### Let's see how this looks like
|
|
424
|
-
|
|
425
|
-
Once again, using https://playground.ai.cloudflare.com:
|
|
426
|
-

|
|
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
191
|
},
|
|
456
|
-
defaultHandler
|
|
192
|
+
defaultHandler: AuthHandler // your consent and login flow
|
|
457
193
|
});
|
|
458
194
|
```
|
|
459
195
|
|
|
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
|
|
196
|
+
`OAuthProvider` needs an object with a `fetch` method, so wrap the handler as shown. Passing the handler function directly throws at startup.
|
|
465
197
|
|
|
466
|
-
|
|
198
|
+
`getMcpAuthContext().props` returns the `props` you passed to `completeAuthorization()` in your auth flow. Tool callbacks also receive the standard SDK v2 `AuthInfo` at `context.http.authInfo`. Do not log or return `context.http.authInfo.token` or `authInfo.extra.props`.
|
|
467
199
|
|
|
468
|
-
|
|
200
|
+
The OAuth flow needs a KV namespace bound as `OAUTH_KV`. For a complete server with a login page, see [`examples/mcp-worker-authenticated`](https://github.com/cloudflare/agents/tree/main/examples/mcp-worker-authenticated). For consent dialogs, CSRF, and proxying to a third-party identity provider, see [Securing MCP Servers](./securing-mcp-servers.md).
|
|
469
201
|
|
|
470
|
-
|
|
202
|
+
## Elicitation
|
|
471
203
|
|
|
472
|
-
- `
|
|
473
|
-
- `"fedramp"` - FedRAMP compliant locations
|
|
474
|
-
|
|
475
|
-
## Elicitation (Human-in-the-Loop)
|
|
476
|
-
|
|
477
|
-
MCP servers can request additional input during a tool call. The implementation differs by protocol generation.
|
|
478
|
-
|
|
479
|
-
### Stateless Elicitation
|
|
480
|
-
|
|
481
|
-
For Stateless Elicitation, return `inputRequired(...)`. The client gathers the input and retries the request with SDK-managed `requestState` and `inputResponses`; the Worker does not suspend while a person responds.
|
|
204
|
+
A tool can ask the user for input by returning `inputRequired(...)`. The client collects the input and retries the request with SDK-managed `requestState` and `inputResponses`. The Worker does not wait while a person responds.
|
|
482
205
|
|
|
483
206
|
```typescript
|
|
484
207
|
import {
|
|
@@ -491,7 +214,7 @@ import { z } from "zod";
|
|
|
491
214
|
|
|
492
215
|
function createServer() {
|
|
493
216
|
const server = new McpServer({
|
|
494
|
-
name: "
|
|
217
|
+
name: "Elicitation Demo",
|
|
495
218
|
version: "1.0.0"
|
|
496
219
|
});
|
|
497
220
|
server.registerTool(
|
|
@@ -534,40 +257,24 @@ export default {
|
|
|
534
257
|
} satisfies ExportedHandler;
|
|
535
258
|
```
|
|
536
259
|
|
|
537
|
-
See [`examples/mcp-elicitation-mrtr`](https://github.com/cloudflare/agents/tree/main/examples/mcp-elicitation-mrtr) for a two-round
|
|
538
|
-
|
|
539
|
-
### Legacy Elicitation
|
|
260
|
+
See [`examples/mcp-elicitation-mrtr`](https://github.com/cloudflare/agents/tree/main/examples/mcp-elicitation-mrtr) for a two-round example.
|
|
540
261
|
|
|
541
|
-
|
|
262
|
+
## 2025-era clients
|
|
542
263
|
|
|
543
|
-
|
|
264
|
+
With the default `legacy: "stateless"`, the same endpoint also answers 2025-era Streamable HTTP clients for tools, resources, and prompts. Those clients have no session to receive server-to-client requests, so sampling, pushed elicitation, and roots requests fail immediately. Set `legacy: "reject"` to serve only Stateless clients.
|
|
544
265
|
|
|
545
|
-
|
|
266
|
+
The handler does not serve the standalone SSE transport.
|
|
546
267
|
|
|
547
|
-
|
|
268
|
+
## Lower-level control
|
|
548
269
|
|
|
549
|
-
|
|
550
|
-
import { WorkerTransport, type TransportState } from "agents/mcp";
|
|
551
|
-
|
|
552
|
-
const transport = new WorkerTransport({
|
|
553
|
-
sessionIdGenerator: () => crypto.randomUUID(),
|
|
554
|
-
enableJsonResponse: false,
|
|
555
|
-
storage: {
|
|
556
|
-
get: () => kv.get<TransportState>("mcp_state"),
|
|
557
|
-
set: (state: TransportState) => kv.put<TransportState>("mcp_state", state)
|
|
558
|
-
}
|
|
559
|
-
});
|
|
560
|
-
```
|
|
270
|
+
If the handler lifecycle does not fit, use the SDK's `WebStandardStreamableHTTPServerTransport` from `@modelcontextprotocol/server` directly in your Worker.
|
|
561
271
|
|
|
562
|
-
|
|
272
|
+
## Migrating from `McpAgent`
|
|
563
273
|
|
|
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 |
|
|
274
|
+
`McpAgent` is deprecated. To move an existing server to `createMcpHandler`, follow [Migrate to MCP SDK v2](https://developers.cloudflare.com/agents/model-context-protocol/guides/migrate-to-mcp-sdk-v2/).
|
|
570
275
|
|
|
571
|
-
|
|
276
|
+
## Examples
|
|
572
277
|
|
|
573
|
-
|
|
278
|
+
- [`examples/mcp-worker`](https://github.com/cloudflare/agents/tree/main/examples/mcp-worker): a minimal server with a browser tool tester
|
|
279
|
+
- [`examples/mcp-worker-authenticated`](https://github.com/cloudflare/agents/tree/main/examples/mcp-worker-authenticated): OAuth with `OAuthProvider`
|
|
280
|
+
- [`examples/mcp-elicitation-mrtr`](https://github.com/cloudflare/agents/tree/main/examples/mcp-elicitation-mrtr): Stateless Elicitation
|