Skip to content

Commit f7f9f04

Browse files
committed
docs: expand stateless MCP server guide with deployment templates and auth setup
1 parent 579c02d commit f7f9f04

File tree

3 files changed

+165
-130
lines changed

3 files changed

+165
-130
lines changed

src/content/docs/agents/guides/build-stateless-mcp-server.mdx

Lines changed: 141 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,27 @@ sidebar:
99

1010
import { Details, Render, PackageManagers, WranglerConfig } from "~/components";
1111

12-
This guide will show you how to deploy a stateless MCP server on Cloudflare Workers using `experimental_createMcpHandler`. This is the simplest way to get started with MCP on Cloudflare.
12+
This guide will show you how to deploy a stateless Model Context Protocol Server with Cloudflare Workers using the `experimental_createMcpHandler`. This is the simplest way to get started with building MCP servers.
1313

14-
Unlike the Durable Object-based approach, stateless MCP servers run as standard Cloudflare Workers, making them simpler to deploy and manage while still providing the majority of MCP functionality.
14+
Unlike the [`McpAgent`](/agents/guides/remote-mcp-server/) which is backed by a Durable Object, this handler runs MCP servers on standard Cloudflare Workers, making them simpler to deploy and reason about while still providing the majority of MCP functionality.
1515

16-
This handler supports MCP Servers with Tools, Prompts and Resources. For more complex capabilities like Elicitations you will need to use the `McpAgent` class.
16+
The `experimental_createMcpHandler` handler supports MCP Servers with Tools, Prompts and Resources. For more complex capabilities like Elicitations you will need to use the `McpAgent` class.
1717

1818
You can start by deploying an **unauthenticated server** where anyone can connect and use the capabilities (no login required), or you can deploy an **authenticated server** where users must sign in.
1919

20+
This template includes a basic MCP server implementation that you can customize with your own tools, prompts, and resources. After deploying or creating from the template, you can follow the sections below to understand how to build and customize your stateless MCP server.
21+
2022
## Unauthenticated Stateless MCP Server
2123

22-
An unauthenticated MCP server is the simplest way to expose MCP tools. This is ideal for public APIs, demonstration purposes, or internal tools that don't require user authentication.
24+
An unauthenticated MCP server is the simplest way to expose MCP tools. This is ideal for public APIs and demonstration purposes.
25+
26+
The fastest way to get started is to use the MCP Worker template from the [cloudflare/agents repository](https://github.com/cloudflare/agents/tree/main/examples/mcp-worker).
27+
28+
[![Deploy to Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/agents/tree/main/examples/mcp-worker)
2329

24-
### Create your server
30+
Alternatively you can follow the steps below to create a new MCP server from scratch.
31+
32+
### Create your project
2533

2634
Create a new directory for your MCP server and initialize it with the necessary files:
2735

@@ -34,12 +42,9 @@ cd my-stateless-mcp-server
3442

3543
Install the required dependencies:
3644

37-
<PackageManagers
38-
type="install"
39-
pkg="@modelcontextprotocol/sdk agents zod"
40-
/>
45+
<PackageManagers type="install" pkg="@modelcontextprotocol/sdk agents zod" />
4146

42-
### Implement your MCP server
47+
### MCP Server in a Worker
4348

4449
Create a `src/index.ts` file with your MCP server implementation:
4550

@@ -51,31 +56,31 @@ import { z } from "zod";
5156
type Env = {};
5257

5358
const server = new McpServer({
54-
name: "Hello MCP Server",
55-
version: "1.0.0"
59+
name: "Hello MCP Server",
60+
version: "1.0.0",
5661
});
5762

5863
server.tool(
59-
"hello",
60-
"Returns a greeting message",
61-
{ name: z.string().optional() },
62-
async ({ name }) => {
63-
return {
64-
content: [
65-
{
66-
text: `Hello, ${name ?? "World"}!`,
67-
type: "text"
68-
}
69-
]
70-
};
71-
}
64+
"hello",
65+
"Returns a greeting message",
66+
{ name: z.string().optional() },
67+
async ({ name }) => {
68+
return {
69+
content: [
70+
{
71+
text: `Hello, ${name ?? "World"}!`,
72+
type: "text",
73+
},
74+
],
75+
};
76+
},
7277
);
7378

7479
export default {
75-
fetch: async (request: Request, env: Env, ctx: ExecutionContext) => {
76-
const handler = createMcpHandler(server);
77-
return handler(request, env, ctx);
78-
}
80+
fetch: async (request: Request, env: Env, ctx: ExecutionContext) => {
81+
const handler = createMcpHandler(server);
82+
return handler(request, env, ctx);
83+
},
7984
};
8085
```
8186

@@ -107,7 +112,7 @@ Start the development server:
107112
npx wrangler dev
108113
```
109114

110-
Your MCP server is now running on `http://localhost:8787/mcp`.
115+
Your MCP server is now running on `http://localhost:8787/mcp`.
111116

112117
In a new terminal, run the [MCP inspector](https://github.com/modelcontextprotocol/inspector).
113118

@@ -129,11 +134,15 @@ After deploying, your MCP server will be available at `https://my-stateless-mcp-
129134

130135
You can now test your deployed server using the MCP inspector by entering your Worker's URL.
131136

132-
## Authenticated stateless MCP server
137+
## Adding Authentication to your MCP Server
138+
139+
OAuth-based user authentication allows you to control access and identify users. It uses the [Cloudflare Workers OAuth Provider](https://github.com/cloudflare/workers-oauth-provider) to handle the OAuth flow. Get started with the Authenticated MCP Worker template from the [cloudflare/agents repository](https://github.com/cloudflare/agents/tree/main/examples/mcp-worker-authenticated).
140+
141+
[![Deploy to Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/agents/tree/main/examples/mcp-worker-authenticated)
133142

134-
An authenticated MCP server adds OAuth-based user authentication, allowing you to control access and identify users. It uses the [Cloudflare Workers OAuth Provider](https://github.com/cloudflare/workers-oauth-provider) to handle the OAuth flow.
143+
Alternatively, you follow the steps below to add authentication to your stateless MCP server.
135144

136-
### Create your authenticated server
145+
### Create your project
137146

138147
Create a new directory and initialize it:
139148

@@ -153,7 +162,7 @@ Install the required dependencies:
153162

154163
### Set up KV namespace
155164

156-
Create a KV namespace to store OAuth tokens and client registrations:
165+
The OAuth provider requires a KV namespace to store OAuth tokens and client registrations:
157166

158167
```bash
159168
npx wrangler kv:namespace create OAUTH_KV
@@ -189,31 +198,31 @@ Create a `src/auth-handler.ts` file to handle the OAuth flow:
189198

190199
```typescript
191200
import type {
192-
AuthRequest,
193-
OAuthHelpers
201+
AuthRequest,
202+
OAuthHelpers,
194203
} from "@cloudflare/workers-oauth-provider";
195204
import { Hono } from "hono";
196205

197206
interface Env {
198-
OAUTH_PROVIDER: OAuthHelpers;
207+
OAUTH_PROVIDER: OAuthHelpers;
199208
}
200209

201210
const app = new Hono<{ Bindings: Env }>();
202211

203212
app.get("/authorize", async (c) => {
204-
const oauthReqInfo: AuthRequest = await c.env.OAUTH_PROVIDER.parseAuthRequest(
205-
c.req.raw
206-
);
207-
const clientInfo = await c.env.OAUTH_PROVIDER.lookupClient(
208-
oauthReqInfo.clientId
209-
);
210-
211-
if (!clientInfo) {
212-
return c.text("Invalid client_id", 400);
213-
}
214-
215-
// Show approval page
216-
const approvalPage = `
213+
const oauthReqInfo: AuthRequest = await c.env.OAUTH_PROVIDER.parseAuthRequest(
214+
c.req.raw,
215+
);
216+
const clientInfo = await c.env.OAUTH_PROVIDER.lookupClient(
217+
oauthReqInfo.clientId,
218+
);
219+
220+
if (!clientInfo) {
221+
return c.text("Invalid client_id", 400);
222+
}
223+
224+
// Show approval page
225+
const approvalPage = `
217226
<!DOCTYPE html>
218227
<html>
219228
<head>
@@ -230,41 +239,41 @@ app.get("/authorize", async (c) => {
230239
</html>
231240
`;
232241

233-
return c.html(approvalPage);
242+
return c.html(approvalPage);
234243
});
235244

236245
app.post("/authorize", async (c) => {
237-
const formData = await c.req.formData();
238-
const state = formData.get("state");
239-
240-
if (!state || typeof state !== "string") {
241-
return c.text("Missing state parameter", 400);
242-
}
243-
244-
const oauthReqInfo: AuthRequest = JSON.parse(atob(state));
245-
246-
// Create a user profile
247-
const userProfile = {
248-
userId: "demo-user",
249-
username: "Demo User",
250-
251-
};
252-
253-
// Complete authorization
254-
const { redirectTo } = await c.env.OAUTH_PROVIDER.completeAuthorization({
255-
request: oauthReqInfo,
256-
userId: userProfile.userId,
257-
metadata: {
258-
label: "MCP Server Access",
259-
clientName:
260-
(await c.env.OAUTH_PROVIDER.lookupClient(oauthReqInfo.clientId))
261-
?.clientName || "Unknown Client"
262-
},
263-
scope: oauthReqInfo.scope,
264-
props: userProfile
265-
});
266-
267-
return c.redirect(redirectTo, 302);
246+
const formData = await c.req.formData();
247+
const state = formData.get("state");
248+
249+
if (!state || typeof state !== "string") {
250+
return c.text("Missing state parameter", 400);
251+
}
252+
253+
const oauthReqInfo: AuthRequest = JSON.parse(atob(state));
254+
255+
// Create a user profile
256+
const userProfile = {
257+
userId: "demo-user",
258+
username: "Demo User",
259+
260+
};
261+
262+
// Complete authorization
263+
const { redirectTo } = await c.env.OAUTH_PROVIDER.completeAuthorization({
264+
request: oauthReqInfo,
265+
userId: userProfile.userId,
266+
metadata: {
267+
label: "MCP Server Access",
268+
clientName:
269+
(await c.env.OAUTH_PROVIDER.lookupClient(oauthReqInfo.clientId))
270+
?.clientName || "Unknown Client",
271+
},
272+
scope: oauthReqInfo.scope,
273+
props: userProfile,
274+
});
275+
276+
return c.redirect(redirectTo, 302);
268277
});
269278

270279
export { app as AuthHandler };
@@ -276,53 +285,53 @@ Create a `src/index.ts` file:
276285

277286
```typescript
278287
import {
279-
experimental_createMcpHandler as createMcpHandler,
280-
getMcpAuthContext
288+
experimental_createMcpHandler as createMcpHandler,
289+
getMcpAuthContext,
281290
} from "agents/mcp";
282291
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
283292
import { z } from "zod";
284293
import { OAuthProvider } from "@cloudflare/workers-oauth-provider";
285294
import { AuthHandler } from "./auth-handler";
286295

287296
const server = new McpServer({
288-
name: "Authenticated MCP Server",
289-
version: "1.0.0"
297+
name: "Authenticated MCP Server",
298+
version: "1.0.0",
290299
});
291300

292301
server.tool(
293-
"hello",
294-
"Returns a greeting message",
295-
{ name: z.string().optional() },
296-
async ({ name }) => {
297-
const auth = getMcpAuthContext();
298-
const username = auth?.props?.username as string | undefined;
299-
300-
return {
301-
content: [
302-
{
303-
text: `Hello, ${name ?? username ?? "World"}!`,
304-
type: "text"
305-
}
306-
]
307-
};
308-
}
302+
"hello",
303+
"Returns a greeting message",
304+
{ name: z.string().optional() },
305+
async ({ name }) => {
306+
const auth = getMcpAuthContext();
307+
const username = auth?.props?.username as string | undefined;
308+
309+
return {
310+
content: [
311+
{
312+
text: `Hello, ${name ?? username ?? "World"}!`,
313+
type: "text",
314+
},
315+
],
316+
};
317+
},
309318
);
310319

311320
// API Handler - handles authenticated MCP requests
312321
const apiHandler = {
313-
async fetch(request: Request, env: unknown, ctx: ExecutionContext) {
314-
return createMcpHandler(server)(request, env, ctx);
315-
}
322+
async fetch(request: Request, env: unknown, ctx: ExecutionContext) {
323+
return createMcpHandler(server)(request, env, ctx);
324+
},
316325
};
317326

318327
export default new OAuthProvider({
319-
authorizeEndpoint: "/authorize",
320-
tokenEndpoint: "/oauth/token",
321-
clientRegistrationEndpoint: "/oauth/register",
322-
apiRoute: "/mcp",
323-
apiHandler: apiHandler,
324-
//@ts-expect-error
325-
defaultHandler: AuthHandler
328+
authorizeEndpoint: "/authorize",
329+
tokenEndpoint: "/oauth/token",
330+
clientRegistrationEndpoint: "/oauth/register",
331+
apiRoute: "/mcp",
332+
apiHandler: apiHandler,
333+
//@ts-expect-error
334+
defaultHandler: AuthHandler,
326335
});
327336
```
328337

@@ -332,24 +341,26 @@ Inside your MCP tools, you can access the authenticated user's information using
332341

333342
```typescript
334343
server.tool(
335-
"my-tool",
336-
"A tool that uses authentication",
337-
{ /* ... */ },
338-
async (params) => {
339-
const auth = getMcpAuthContext();
340-
341-
if (!auth) {
342-
// Handle unauthenticated request
343-
return { content: [{ text: "Authentication required", type: "text" }] };
344-
}
345-
346-
// Access user information set during oauth flow
347-
const userId = auth.props?.userId;
348-
const username = auth.props?.username;
349-
const email = auth.props?.email;
350-
351-
// ...
352-
}
344+
"my-tool",
345+
"A tool that uses authentication",
346+
{
347+
/* ... */
348+
},
349+
async (params) => {
350+
const auth = getMcpAuthContext();
351+
352+
if (!auth) {
353+
// Handle unauthenticated request
354+
return { content: [{ text: "Authentication required", type: "text" }] };
355+
}
356+
357+
// Access user information set during oauth flow
358+
const userId = auth.props?.userId;
359+
const username = auth.props?.username;
360+
const email = auth.props?.email;
361+
362+
// ...
363+
},
353364
);
354365
```
355366

src/content/docs/agents/model-context-protocol/index.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ The MCP standard supports two modes of operation:
2929
- **Local MCP connections**: MCP clients connect to MCP servers on the same machine, using [stdio](https://spec.modelcontextprotocol.io/specification/draft/basic/transports/#stdio) as a local transport method.
3030

3131
### Best Practices
32+
3233
- **Tool design**: Do not treat your MCP server as a wrapper around your full API schema. Instead, build tools that are optimized for specific user goals and reliable outcomes. Fewer, well-designed tools often outperform many granular ones, especially for agents with small context windows or tight latency budgets.
3334
- **Scoped permissions**: Deploying several focused MCP servers, each with narrowly scoped permissions, reduces the risk of over-privileged access and makes it easier to manage and audit what each server is allowed to do.
3435
- **Tool descriptions**: Detailed parameter descriptions help agents understand how to use your tools correctly — including what values are expected, how they affect behavior, and any important constraints. This reduces errors and improves reliability.

0 commit comments

Comments
 (0)