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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ The core innovation: instead of 2,500 MCP tools (~244K tokens), two tools handle
- `src/mcp-handler.ts` uses `createMcpHandler(factory)` directly from `@modelcontextprotocol/server`; this repository does not depend on the Agents SDK.
- Each authenticated request creates an upstream handler whose factory closes over validated `AuthProps`, matching the repository's pre-migration explicit data flow.
- The handler serves MCP `2026-07-28` and keeps the upstream default stateless 2025 compatibility path. Its factory creates a fresh `McpServer` for every request.
- No MCP session ID, protocol transport state, replay store, Durable Object, or Node async-context bridge is used. The handler sets the SDK's `maxSubscriptions: 0` because this server publishes no change notifications; `subscriptions/listen` is rejected immediately instead of opening a long-lived SSE stream.
- No MCP session ID, protocol transport state, replay store, Durable Object, or Node async-context bridge is used. The handler sets the SDK's `maxSubscriptions: 0` because this server publishes no change notifications; `subscriptions/listen` is rejected immediately instead of opening a long-lived SSE stream. For the same reason, `createServer` declares `tools.listChanged: false` for both tool surfaces before any tool is registered; `registerTool` would otherwise advertise `true`.
- Deployment-static Host and browser Origin allowlists cover localhost, staging, and production. Do not derive either trust list from the incoming request URL or headers.

### Worker Loader API
Expand Down
6 changes: 6 additions & 0 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ export async function createServer(
const server = new McpServer(SERVER_INFO)
const formatResult = truncateToolResult ? truncateResponse : stringifyResponse

// This server never sends notifications/tools/list_changed. Tool lists change
// only on deploy or the daily spec refresh, and mcp-handler.ts rejects
// subscriptions/listen. Declare that before registerTool runs, because it
// otherwise advertises listChanged: true.
server.server.registerCapabilities({ tools: { listChanged: false } })

if (!codemode) {
await registerNonCodemodeTools(server, props, formatResult)
return server
Expand Down
3 changes: 1 addition & 2 deletions src/tools/non-codemode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import type { AuthProps } from '../auth/types'
* schemas per HTTP request. `tools/list` serves the precomputed JSON artifact;
* `tools/call` validates and dispatches only the requested operation.
* `formatResult` turns each API response body into the tool's text output.
* `server` must already declare the `tools` capability, as `createServer` does.
*/
export async function registerNonCodemodeTools(
server: McpServer,
Expand All @@ -31,8 +32,6 @@ export async function registerNonCodemodeTools(
const tools = await getNonCodemodeTools()
const toolsByName = await getNonCodemodeToolMap()

server.server.registerCapabilities({ tools: { listChanged: false } })

server.server.setRequestHandler('tools/list', () => ({
tools: [DOCS_TOOL, ...tools.map((tool) => toWireTool(toolForAccountAccess(tool)))]
}))
Expand Down
23 changes: 23 additions & 0 deletions tests/helpers/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,29 @@ export interface McpToolResult {
error?: { code: number; message: string }
}

/** Build the legacy JSON-RPC `initialize` request a 2025-era client sends first. */
export function mcpInitializeRequest(token: string, url = MCP_URL): Request {
return new Request(url, {
method: 'POST',
headers: {
Host: MCP_HOST,
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
Accept: 'application/json, text/event-stream'
},
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'initialize',
params: {
protocolVersion: '2025-11-25',
capabilities: {},
clientInfo: { name: 'cloudflare-mcp-tests', version: '1.0.0' }
}
})
})
}

/** Build a legacy JSON-RPC `tools/list` request to the worker's `/mcp` endpoint. */
export function mcpToolListRequest(token: string, id = 1): Request {
return new Request(MCP_URL, {
Expand Down
16 changes: 16 additions & 0 deletions tests/mcp-modern.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { clearKv } from './helpers/kv'
import {
MCP_HOST,
MCP_URL,
mcpInitializeRequest,
mcpToolListRequest,
modernMcpRequest,
parseMcpResult
Expand Down Expand Up @@ -64,6 +65,21 @@ describe('MCP 2026-07-28 stateless handler', () => {
expect(body.result).not.toHaveProperty('serverInfo')
})

it.each([
['Code Mode', MCP_URL],
['non-Code-Mode', `${MCP_URL}?codemode=false`]
])('advertises no tool list change notifications in %s', async (_surface, url) => {
const discover = await parseMcpResult(
await exports.default.fetch(modernMcpRequest(API_TOKEN, 'server/discover', {}, { url }))
)
const initialize = await parseMcpResult(
await exports.default.fetch(mcpInitializeRequest(API_TOKEN, url))
)

expect(discover.result).toMatchObject({ capabilities: { tools: { listChanged: false } } })
expect(initialize.result).toMatchObject({ capabilities: { tools: { listChanged: false } } })
})

it('rejects subscriptions instead of opening a long-lived stream', async () => {
const response = await exports.default.fetch(
modernMcpRequest(API_TOKEN, 'subscriptions/listen', {
Expand Down
Loading