diff --git a/src/content/docs/agents/guides/build-stateless-mcp-server.mdx b/src/content/docs/agents/guides/build-stateless-mcp-server.mdx new file mode 100644 index 00000000000000..d6b184a53e593f --- /dev/null +++ b/src/content/docs/agents/guides/build-stateless-mcp-server.mdx @@ -0,0 +1,392 @@ +--- +pcx_content_type: concept +title: Build a Stateless MCP Server +tags: + - MCP +sidebar: + order: 5 +--- + +import { Details, Render, PackageManagers, WranglerConfig } from "~/components"; + +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. + +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. + +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. + +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. + +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. + +## Unauthenticated Stateless MCP Server + +An unauthenticated MCP server is the simplest way to expose MCP tools. This is ideal for public APIs and demonstration purposes. + +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). + +[![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) + +Alternatively you can follow the steps below to create a new MCP server from scratch. + +### Create your project + +Create a new directory for your MCP server and initialize it with the necessary files: + +```bash +mkdir my-stateless-mcp-server +cd my-stateless-mcp-server +``` + + + +Install the required dependencies: + + + +### MCP Server in a Worker + +Create a `src/index.ts` file with your MCP server implementation: + +```typescript +import { experimental_createMcpHandler as createMcpHandler } from "agents/mcp"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; + +type Env = {}; + +const server = new McpServer({ + name: "Hello MCP Server", + version: "1.0.0", +}); + +server.tool( + "hello", + "Returns a greeting message", + { name: z.string().optional() }, + async ({ name }) => { + return { + content: [ + { + text: `Hello, ${name ?? "World"}!`, + type: "text", + }, + ], + }; + }, +); + +export default { + fetch: async (request: Request, env: Env, ctx: ExecutionContext) => { + const handler = createMcpHandler(server); + return handler(request, env, ctx); + }, +}; +``` + +### Configure Wrangler + +Create a `wrangler.jsonc` file to configure your Worker: + + +```jsonc +{ + "compatibility_date": "2025-10-08", + "compatibility_flags": ["nodejs_compat"], + "main": "src/index.ts", + "name": "my-stateless-mcp-server", + "observability": { + "logs": { + "enabled": true + } + } +} +``` + + +### Local development + +Start the development server: + +```bash +npx wrangler dev +``` + +Your MCP server is now running on `http://localhost:8787/mcp`. + +In a new terminal, run the [MCP inspector](https://github.com/modelcontextprotocol/inspector). + +```bash +npx @modelcontextprotocol/inspector@latest +``` + +In the inspector, enter the URL of your MCP server, `http://localhost:8787/sse`, and click **Connect**. You should see the "List Tools" button, which will list the tools that your MCP server exposes. + +### Deploy your server + +Deploy your MCP server to Cloudflare: + +```bash +npx wrangler deploy +``` + +After deploying, your MCP server will be available at `https://my-stateless-mcp-server..workers.dev/sse`. + +You can now test your deployed server using the MCP inspector by entering your Worker's URL. + +## Adding Authentication to your MCP Server + +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). + +[![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) + +Alternatively, you follow the steps below to add authentication to your stateless MCP server. + +### Create your project + +Create a new directory and initialize it: + +```bash +mkdir my-auth-mcp-server +cd my-auth-mcp-server +``` + + + +Install the required dependencies: + + + +### Set up KV namespace + +The OAuth provider requires a KV namespace to store OAuth tokens and client registrations: + +```bash +npx wrangler kv namespace create OAUTH_KV +``` + +This will output a namespace ID. Add it to your `wrangler.jsonc`: + + +```jsonc +{ + "compatibility_date": "2025-10-08", + "compatibility_flags": ["nodejs_compat"], + "main": "src/index.ts", + "name": "my-auth-mcp-server", + "kv_namespaces": [ + { + "binding": "OAUTH_KV", + "id": "" + } + ], + "observability": { + "logs": { + "enabled": true + } + } +} +``` + + +### Create an authorization handler + +Create a `src/auth-handler.ts` file to handle the OAuth flow: + +```typescript +import type { + AuthRequest, + OAuthHelpers, +} from "@cloudflare/workers-oauth-provider"; +import { Hono } from "hono"; + +interface Env { + OAUTH_PROVIDER: OAuthHelpers; +} + +const app = new Hono<{ Bindings: Env }>(); + +app.get("/authorize", async (c) => { + const oauthReqInfo: AuthRequest = await c.env.OAUTH_PROVIDER.parseAuthRequest( + c.req.raw, + ); + const clientInfo = await c.env.OAUTH_PROVIDER.lookupClient( + oauthReqInfo.clientId, + ); + + if (!clientInfo) { + return c.text("Invalid client_id", 400); + } + + // Show approval page + const approvalPage = ` + + + + Authorize ${clientInfo.clientName || "MCP Client"} + + +

Authorization Request

+

${clientInfo.clientName || "An MCP Client"} is requesting access.

+
+ + +
+ + + `; + + return c.html(approvalPage); +}); + +app.post("/authorize", async (c) => { + const formData = await c.req.formData(); + const state = formData.get("state"); + + if (!state || typeof state !== "string") { + return c.text("Missing state parameter", 400); + } + + const oauthReqInfo: AuthRequest = JSON.parse(atob(state)); + + // Create a user profile + const userProfile = { + userId: "demo-user", + username: "Demo User", + email: "demo@example.com", + }; + + // Complete authorization + const { redirectTo } = await c.env.OAUTH_PROVIDER.completeAuthorization({ + request: oauthReqInfo, + userId: userProfile.userId, + metadata: { + label: "MCP Server Access", + clientName: + (await c.env.OAUTH_PROVIDER.lookupClient(oauthReqInfo.clientId)) + ?.clientName || "Unknown Client", + }, + scope: oauthReqInfo.scope, + props: userProfile, + }); + + return c.redirect(redirectTo, 302); +}); + +export { app as AuthHandler }; +``` + +### Implement your authenticated MCP server + +Create a `src/index.ts` file: + +```typescript +import { + experimental_createMcpHandler as createMcpHandler, + getMcpAuthContext, +} from "agents/mcp"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { OAuthProvider } from "@cloudflare/workers-oauth-provider"; +import { AuthHandler } from "./auth-handler"; + +const server = new McpServer({ + name: "Authenticated MCP Server", + version: "1.0.0", +}); + +server.tool( + "hello", + "Returns a greeting message", + { name: z.string().optional() }, + async ({ name }) => { + const auth = getMcpAuthContext(); + const username = auth?.props?.username as string | undefined; + + return { + content: [ + { + text: `Hello, ${name ?? username ?? "World"}!`, + type: "text", + }, + ], + }; + }, +); + +// API Handler - handles authenticated MCP requests +const apiHandler = { + async fetch(request: Request, env: unknown, ctx: ExecutionContext) { + return createMcpHandler(server)(request, env, ctx); + }, +}; + +export default new OAuthProvider({ + authorizeEndpoint: "/authorize", + tokenEndpoint: "/oauth/token", + clientRegistrationEndpoint: "/oauth/register", + apiRoute: "/mcp", + apiHandler: apiHandler, + //@ts-expect-error + defaultHandler: AuthHandler, +}); +``` + +### Access authentication context in tools + +Inside your MCP tools, you can access the authenticated user's information using `getMcpAuthContext()`: + +```typescript +server.tool( + "my-tool", + "A tool that uses authentication", + { + /* ... */ + }, + async (params) => { + const auth = getMcpAuthContext(); + + if (!auth) { + // Handle unauthenticated request + return { content: [{ text: "Authentication required", type: "text" }] }; + } + + // Access user information set during oauth flow + const userId = auth.props?.userId; + const username = auth.props?.username; + const email = auth.props?.email; + + // ... + }, +); +``` + +To test your authenticated server locally, run `npx wrangler dev` and connect using the MCP inspector. You will need to complete the OAuth flow to connect to your MCP server. + +Deploy with `npx wrangler deploy`. Your authenticated MCP server will be available at `https://my-auth-mcp-server..workers.dev/mcp`. + +## How stateless MCP servers work + +Stateless MCP servers use `experimental_createMcpHandler` to wrap a standard MCP Server instance and expose it as a Cloudflare Worker. The handler: + +1. Accepts HTTP requests on the `/mcp` endpoint (or a custom route) +2. Handles the streamable-http transport for MCP +3. Routes tool calls to your MCP server implementation +4. Returns responses back to the MCP client + +For authenticated servers, the `OAuthProvider` wrapper: + +1. Handles OAuth endpoints (`/authorize`, `/token`, `/register`) +2. Validates access tokens for incoming requests +3. Injects user context into `getMcpAuthContext()` +4. Routes authenticated requests to your MCP handler + +## Next steps + +- Add [tools](/agents/model-context-protocol/tools/) to your MCP server +- Customize your MCP server's [authentication and authorization](/agents/model-context-protocol/authorization/) +- Learn about [testing remote MCP servers](/agents/guides/test-remote-mcp-server/) +- Explore the [Model Context Protocol specification](https://modelcontextprotocol.io/docs/getting-started/intro) diff --git a/src/content/docs/agents/model-context-protocol/index.mdx b/src/content/docs/agents/model-context-protocol/index.mdx index 1326b2f97a64a5..71faf095413bde 100644 --- a/src/content/docs/agents/model-context-protocol/index.mdx +++ b/src/content/docs/agents/model-context-protocol/index.mdx @@ -29,6 +29,7 @@ The MCP standard supports two modes of operation: - **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. ### Best Practices + - **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. - **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. - **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. diff --git a/src/content/docs/agents/model-context-protocol/state.mdx b/src/content/docs/agents/model-context-protocol/state.mdx new file mode 100644 index 00000000000000..4302dbdf663d49 --- /dev/null +++ b/src/content/docs/agents/model-context-protocol/state.mdx @@ -0,0 +1,23 @@ +--- +pcx_content_type: concept +title: Stateful vs. Stateless MCP Servers +tags: + - MCP +sidebar: + order: 100 +--- + +MCP was originally designed as a stateful protocol. The servers would maintain a long lived connection to a client and could maintain state between requests. + +However, since many MCP servers have been used as simply remote tools, prompts and resources, which for the most part do not require state, there was a push to make an optionally stateless version of the protocol. + +The stateless version of the protocol is the easiest to deploy and covers the majority of use cases. Users can host MCP servers on any platform that supports HTTP and Server-Sent Events (SSE), [including Cloudflare Workers](/agents/guides/build-stateless-mcp-server/), and take advantage of this stateless option. + +If you wish to maintain state inside the MCP Server you can migrate to the [McpAgent](/agents/guides/remote-mcp-server/) class which is backed by a Durable Object. + +| Feature | Stateless MCP Servers | Stateful MCP Servers | +| ------------------ | ----------------------------------------------------------------------------- | ------------------------------------------------ | +| **Transport** | `streamable-http` | `stdio`, `streamable-http` or `sse` (deprecated) | +| **Connection** | Request-response per invocation | Long-lived connection | +| **Use cases** | Remote tools, prompts, and resources | Elicitations, MCP Agents | +| **Implementation** | [`experimental_createMcpHandler`](/agents/guides/build-stateless-mcp-server/) | [`McpAgent`](/agents/guides/remote-mcp-server/) |