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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,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. This server publishes no change notifications, so both tool modes advertise `tools.listChanged: false`. For `subscriptions/listen`, the handler lets the SDK send the acknowledgment with an empty honored filter and then closes the per-request handler. That ends the subscription gracefully with a `complete` result rather than an error, and no SSE stream stays open.
- No MCP session ID, protocol transport state, replay store, Durable Object, or Node async-context bridge is used. This server publishes no change notifications, so both tool modes advertise `tools.listChanged: false`. For `subscriptions/listen`, the SDK acknowledges with an empty honored filter and, since it honours none of the requested types, ends the stream straight away with a `complete` result (SDK 2.2.0+). No SSE stream stays open.
- 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
48 changes: 24 additions & 24 deletions package-lock.json

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

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@
"seed:prod": "tsx scripts/seed-r2.ts production"
},
"dependencies": {
"@cloudflare/workers-oauth-provider": "^1.2.1",
"@modelcontextprotocol/server": "2.0.0",
"@cloudflare/workers-oauth-provider": "^1.2.2",
"@modelcontextprotocol/server": "2.3.1",
"hono": "^4.13.5",
"zod": "^4.3.5"
},
"devDependencies": {
"@cloudflare/vitest-pool-workers": "^0.16.18",
"@modelcontextprotocol/client": "2.0.0",
"@modelcontextprotocol/client": "2.3.1",
"@types/node": "^25.0.6",
"msw": "^2.14.6",
"oxfmt": "^0.31.0",
Expand Down
14 changes: 0 additions & 14 deletions src/mcp-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,6 @@ function createAuthenticatedHandler(props: AuthProps) {
})
}

const SUBSCRIPTIONS_LISTEN = 'subscriptions/listen'

// Handler options are intentionally omitted. The SDK defaults to:
// - stateless 2025 compatibility, with a fresh server and no protocol session
// - automatic JSON/SSE response shaping (ordinary requests here remain JSON)
Expand Down Expand Up @@ -109,18 +107,6 @@ export async function handleAuthenticatedMcpRequest(
const handler = createAuthenticatedHandler(props)
const response = await handler.fetch(request)

// This server publishes no change notifications and keeps no long-lived
// request state. The SDK only serves subscriptions/listen after checking that
// the Mcp-Method header matches the body. It acknowledges the subscription
// with every unsupported notification type left out. Closing this
// per-request handler then ends the subscription gracefully, as the spec
// describes: it writes a `complete` result and closes the stream, so no
// isolate stays pinned. Clients get an empty subscription instead of an
// error.
if (request.headers.get('Mcp-Method') === SUBSCRIPTIONS_LISTEN) {
await handler.close()
}

return withCors(response, request)
}

Expand Down
38 changes: 28 additions & 10 deletions tests/mcp-modern.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,14 +111,17 @@ describe('MCP 2026-07-28 stateless handler', () => {
expect(body.result?.capabilities?.tools?.listChanged).not.toBe(true)
})

it('acknowledges an empty subscription and ends it gracefully', async () => {
// The server declares no list-changed or subscribe capability, so the SDK honours none of
// these and ends the stream itself (SDK 2.2+). An open stream would hang the read below.
it.each([
{ toolsListChanged: true },
{ promptsListChanged: true },
{ resourcesListChanged: true },
{ resourceSubscriptions: ['file:///a'] },
{}
])('acknowledges an empty subscription for %j and ends it gracefully', async (notifications) => {
const response = await exports.default.fetch(
modernMcpRequest(
API_TOKEN,
'subscriptions/listen',
{ notifications: { toolsListChanged: true } },
{ id: 7 }
)
modernMcpRequest(API_TOKEN, 'subscriptions/listen', { notifications }, { id: 7 })
)

expect(response.status).toBe(200)
Expand Down Expand Up @@ -156,7 +159,12 @@ describe('MCP 2026-07-28 stateless handler', () => {

expect(response.status).toBe(200)
expect(body.result?.resultType).toBe('complete')
expect(body.result?.tools?.map((tool) => tool.name)).toEqual(['docs', 'search', 'execute', 'whoami'])
expect(body.result?.tools?.map((tool) => tool.name)).toEqual([
'docs',
'search',
'execute',
'whoami'
])
})

it('serves a modern Code Mode tools/call', async () => {
Expand Down Expand Up @@ -271,7 +279,12 @@ describe('MCP 2026-07-28 stateless handler', () => {

expect(codemodeResponse.status).toBe(200)
expect(endpointResponse.status).toBe(200)
expect(codemode.result?.tools?.map((tool) => tool.name)).toEqual(['docs', 'search', 'execute', 'whoami'])
expect(codemode.result?.tools?.map((tool) => tool.name)).toEqual([
'docs',
'search',
'execute',
'whoami'
])
expect(endpoints.result?.tools?.map((tool) => tool.name)).toEqual([
'docs',
'whoami',
Expand Down Expand Up @@ -308,7 +321,12 @@ describe('MCP 2026-07-28 stateless handler', () => {
expect(response.status).toBe(200)
expect(response.headers.get('content-type')).toContain('text/event-stream')
expect(response.headers.get('mcp-session-id')).toBeNull()
expect(body.result?.tools?.map((tool) => tool.name)).toEqual(['docs', 'search', 'execute', 'whoami'])
expect(body.result?.tools?.map((tool) => tool.name)).toEqual([
'docs',
'search',
'execute',
'whoami'
])
})

it.each(['GET', 'DELETE'])('rejects session-only %s requests', async (method) => {
Expand Down
Loading