The Model Context Protocol (MCP) specification defines two standard transport mechanisms ↗︎ for communication between clients and servers:
- stdio — Communication over standard in and standard out, designed for local MCP connections.
- Streamable HTTP — The standard transport method for remote MCP connections, introduced ↗︎ in March 2025. It uses a single HTTP endpoint for bidirectional messaging.
MCP servers built with the Agents SDK use createMcpHandler to handle Streamable HTTP transport.
Use createMcpHandler to create an MCP server that handles Streamable HTTP transport.
You can use the "Deploy to Cloudflare" button to create a remote MCP server.
Create an MCP server using createMcpHandler. View the complete example on GitHub ↗︎.
import { createMcpHandler } from "agents/mcp/server";
import { McpServer } from "@modelcontextprotocol/server";
import { z } from "zod";
function createServer() {
const server = new McpServer({
name: "My MCP Server",
version: "1.0.0",
});
server.registerTool(
"hello",
{
description: "Returns a greeting message",
inputSchema: { name: z.string().optional() },
},
async ({ name }) => {
return {
content: [{ text: `Hello, ${name ?? "World"}!`, type: "text" }],
};
},
);
return server;
}
export default {
fetch(request, env, ctx) {
return createMcpHandler(createServer)(request, env, ctx);
},
};import { createMcpHandler } from "agents/mcp/server";
import { McpServer } from "@modelcontextprotocol/server";
import { z } from "zod";
function createServer() {
const server = new McpServer({
name: "My MCP Server",
version: "1.0.0",
});
server.registerTool(
"hello",
{
description: "Returns a greeting message",
inputSchema: { name: z.string().optional() },
},
async ({ name }) => {
return {
content: [{ text: `Hello, ${name ?? "World"}!`, type: "text" }],
};
},
);
return server;
}
export default {
fetch(request, env, ctx) {
return createMcpHandler(createServer)(request, env, ctx);
},
} satisfies ExportedHandler;To require OAuth, wrap the handler in an OAuthResourceServer from Workers OAuth Provider ↗︎. It answers requests without a valid token with a 401 that points MCP clients at your authorization server. Refer to Authorization for the authorization server and the full setup.
import {
OAuthResourceServer,
insufficientScope,
} from "@cloudflare/workers-oauth-provider";
import { McpServer } from "@modelcontextprotocol/server";
import { createMcpHandler, getMcpAuthContext } from "agents/mcp/server";
import { env } from "cloudflare:workers";
function createServer() {
const server = new McpServer({ name: "my-mcp-server", version: "1.0.0" });
const props = getMcpAuthContext()?.props;
server.registerTool(
"whoami",
{ description: "Return the signed-in user" },
async () => ({
content: [{ type: "text", text: props.userId }],
}),
);
return server;
}
const mcpHandler = createMcpHandler(createServer);
export default new OAuthResourceServer({
resourceMetadata: {
resource: env.MCP_SERVER_URL, // tokens for any other resource are rejected
authorization_servers: [env.AUTH_SERVER_URL],
},
requiredScopes: ["mcp:read"],
validateToken: (env) => env.AUTH_SERVER.validateToken,
handler: {
fetch(request, env, ctx) {
// requiredScopes is advertised, not enforced: check it here.
if (!ctx.auth.scope.includes("mcp:read")) {
return insufficientScope(ctx.auth, ["mcp:read"]);
}
return mcpHandler(request, env, ctx);
},
},
});import {
OAuthResourceServer,
insufficientScope,
type AuthorizationServerBinding,
} from "@cloudflare/workers-oauth-provider";
import { McpServer } from "@modelcontextprotocol/server";
import { createMcpHandler, getMcpAuthContext } from "agents/mcp/server";
import { env } from "cloudflare:workers";
type AuthProps = { userId: string };
interface Env {
AUTH_SERVER: AuthorizationServerBinding<AuthProps>;
}
function createServer() {
const server = new McpServer({ name: "my-mcp-server", version: "1.0.0" });
const props = getMcpAuthContext()?.props as AuthProps;
server.registerTool(
"whoami",
{ description: "Return the signed-in user" },
async () => ({
content: [{ type: "text", text: props.userId }],
}),
);
return server;
}
const mcpHandler = createMcpHandler(createServer);
export default new OAuthResourceServer<Env, AuthProps>({
resourceMetadata: {
resource: env.MCP_SERVER_URL, // tokens for any other resource are rejected
authorization_servers: [env.AUTH_SERVER_URL],
},
requiredScopes: ["mcp:read"],
validateToken: (env) => env.AUTH_SERVER.validateToken,
handler: {
fetch(request, env, ctx) {
// requiredScopes is advertised, not enforced: check it here.
if (!ctx.auth.scope.includes("mcp:read")) {
return insufficientScope(ctx.auth, ["mcp:read"]);
}
return mcpHandler(request, env, ctx);
},
},
});The stateless handler has no MCP session. Keep data that must survive between requests in a Durable Object, D1, KV, or R2, keyed by the authenticated user rather than an MCP session ID.
To move an existing McpAgent server, refer to Migrate to MCP SDK v2. The deprecated API is documented in the McpAgent reference.
Test your MCP server with a client that supports Streamable HTTP connections.
Follow Test a Remote MCP Server for instructions for Claude Code, Cursor, Windsurf, and other MCP clients.