Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
392 changes: 392 additions & 0 deletions src/content/docs/agents/guides/build-stateless-mcp-server.mdx
Original file line number Diff line number Diff line change
@@ -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
```

<PackageManagers type="init" />

Install the required dependencies:

<PackageManagers type="install" pkg="@modelcontextprotocol/sdk agents zod" />

### 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:

<WranglerConfig>
```jsonc
{
"compatibility_date": "2025-10-08",
"compatibility_flags": ["nodejs_compat"],
"main": "src/index.ts",
"name": "my-stateless-mcp-server",
"observability": {
"logs": {
"enabled": true
}
}
}
```
</WranglerConfig>

### 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.<your-account>.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
```

<PackageManagers type="init" />

Install the required dependencies:

<PackageManagers
type="install"
pkg="@modelcontextprotocol/sdk agents zod @cloudflare/workers-oauth-provider hono"
/>

### 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`:

<WranglerConfig>
```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": "<YOUR_KV_NAMESPACE_ID>"
}
],
"observability": {
"logs": {
"enabled": true
}
}
}
```
</WranglerConfig>

### 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 = `
<!DOCTYPE html>
<html>
<head>
<title>Authorize ${clientInfo.clientName || "MCP Client"}</title>
</head>
<body>
<h1>Authorization Request</h1>
<p><strong>${clientInfo.clientName || "An MCP Client"}</strong> is requesting access.</p>
<form method="POST" action="/authorize">
<input type="hidden" name="state" value="${btoa(JSON.stringify(oauthReqInfo))}">
<button type="submit">Approve</button>
</form>
</body>
</html>
`;

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: "[email protected]",
};

// 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.<your-account>.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)
Loading
Loading