Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/angry-beds-cheat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"agents": patch
---

bump mcp sdk version to 1.25.2. changes error handling for not found see: https://github.com/cloudflare/agents/pull/752/changes#diff-176ef2d2154e76a8eb7862efb323210f8f1b434f6a9ff3f06abc87d8616855c9R25-R31
22 changes: 22 additions & 0 deletions examples/mcp-server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# MCP Server Example

This example demonstrates how to use `WebStandardStreamableHTTPServerTransport` to create an unauthenticated stateless MCP server.

In this example we do not use the `agents` package, but instead use the `@modelcontextprotocol/sdk` package directly to create an MCP server that "just works" on Cloudflare Workers.

This is THE simplest way to get started with MCP on Cloudflare.

## Usage

```bash
npm install
npm run dev
```

## Testing

You can test the MCP server using the MCP Inspector or any MCP client that supports the `streamable-http` transport.

## Adding State

To create a stateful MCP server, you can use an `Agent` to keep the state of the session/transport. See the [`mcp-elicitation`](../mcp-elicitation) example for more information.
9 changes: 9 additions & 0 deletions examples/mcp-server/env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/* eslint-disable */
// Generated by Wrangler by running `wrangler types env.d.ts --include-runtime false` (hash: b739a9c19cff1463949c4db47674ed86)
declare namespace Cloudflare {
interface GlobalProps {
mainModule: typeof import("./src/index");
}
interface Env {}
}
interface Env extends Cloudflare.Env {}
13 changes: 13 additions & 0 deletions examples/mcp-server/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "@cloudflare/agents-mcp-server",
"description": "zero config stateless MCP Server on Cloudflare",
"author": "Matt Carey <[email protected]>",
"version": "0.0.1",
"private": true,
"type": "module",
"scripts": {
"dev": "wrangler dev",
"deploy": "wrangler deploy",
"types": "wrangler types env.d.ts --include-runtime false"
}
}
54 changes: 54 additions & 0 deletions examples/mcp-server/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";

const server = new McpServer({
name: "Hello 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"
}
]
};
}
);

const transport = new WebStandardStreamableHTTPServerTransport();
server.connect(transport);

const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
"Access-Control-Allow-Headers":
"Content-Type, Accept, mcp-session-id, mcp-protocol-version",
"Access-Control-Expose-Headers": "mcp-session-id",
"Access-Control-Max-Age": "86400"
};

function withCors(response: Response): Response {
for (const [key, value] of Object.entries(corsHeaders)) {
response.headers.set(key, value);
}
return response;
}

export default {
fetch: async (request: Request, _env: Env, _ctx: ExecutionContext) => {
if (request.method === "OPTIONS") {
return new Response(null, { headers: corsHeaders });
}
return withCors(await transport.handleRequest(request));
}
};
3 changes: 3 additions & 0 deletions examples/mcp-server/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "../../tsconfig.base.json"
}
11 changes: 11 additions & 0 deletions examples/mcp-server/wrangler.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"compatibility_date": "2025-10-08",
"compatibility_flags": ["nodejs_compat"],
"main": "src/index.ts",
"name": "mcp-server",
"observability": {
"logs": {
"enabled": true
}
}
}
2 changes: 0 additions & 2 deletions examples/mcp-worker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

This example demonstrates how to use `createMcpHandler` to create an unauthenticated stateless MCP server.

This is THE simplest way to get started with MCP on Cloudflare.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dethroned. Maybe worth a mention here that this is now supported directly in the mcp library and a link to the new example?

Just a nit, don't have to do this now


## Usage

```bash
Expand Down
68 changes: 17 additions & 51 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"@cloudflare/vite-plugin": "^1.19.0",
"@cloudflare/vitest-pool-workers": "^0.11.1",
"@cloudflare/workers-types": "^4.20260103.0",
"@modelcontextprotocol/sdk": "1.23.0",
"@modelcontextprotocol/sdk": "1.25.2",
"@openai/agents": "^0.3.7",
"@openai/agents-extensions": "^0.3.7",
"@types/node": "^25.0.3",
Expand Down
2 changes: 1 addition & 1 deletion packages/agents/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
},
"dependencies": {
"@cfworker/json-schema": "^4.1.1",
"@modelcontextprotocol/sdk": "1.23.0",
"@modelcontextprotocol/sdk": "1.25.2",
"cron-schedule": "^6.0.0",
"json-schema": "^0.4.0",
"json-schema-to-typescript": "^15.0.4",
Expand Down
22 changes: 21 additions & 1 deletion packages/agents/src/mcp/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,34 @@ export function toErrorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}

function getErrorCode(error: unknown): number | undefined {
if (
error &&
typeof error === "object" &&
"code" in error &&
typeof (error as { code: unknown }).code === "number"
) {
return (error as { code: number }).code;
}
return undefined;
}

export function isUnauthorized(error: unknown): boolean {
const code = getErrorCode(error);
if (code === 401) return true;

const msg = toErrorMessage(error);
return msg.includes("Unauthorized") || msg.includes("401");
}

// MCP SDK change (v1.24.0, commit 6b90e1a):
// - Old: Error POSTing to endpoint (HTTP 404): Not Found
// - New: StreamableHTTPError with code: 404 and message Error POSTing to endpoint: Not Found
export function isTransportNotImplemented(error: unknown): boolean {
const code = getErrorCode(error);
if (code === 404 || code === 405) return true;

const msg = toErrorMessage(error);
// Treat common "not implemented" surfaces as transport not supported
return (
msg.includes("404") ||
msg.includes("405") ||
Expand Down
8 changes: 4 additions & 4 deletions packages/agents/src/mcp/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import type {
} from "@modelcontextprotocol/sdk/types.js";
import {
JSONRPCMessageSchema,
isJSONRPCError,
isJSONRPCResponse,
isJSONRPCErrorResponse,
isJSONRPCResultResponse,
type ElicitResult
} from "@modelcontextprotocol/sdk/types.js";
import type { Connection, ConnectionContext } from "../";
Expand Down Expand Up @@ -312,7 +312,7 @@ export abstract class McpAgent<
message: JSONRPCMessage
): Promise<boolean> {
// Check if this is a response to an elicitation request
if (isJSONRPCResponse(message) && message.result) {
if (isJSONRPCResultResponse(message) && message.result) {
const requestId = message.id?.toString();
if (!requestId || !requestId.startsWith("elicit_")) return false;

Expand All @@ -331,7 +331,7 @@ export abstract class McpAgent<
}

// Check if this is an error response to an elicitation request
if (isJSONRPCError(message)) {
if (isJSONRPCErrorResponse(message)) {
const requestId = message.id?.toString();
if (!requestId || !requestId.startsWith("elicit_")) return false;

Expand Down
Loading
Loading