-
Notifications
You must be signed in to change notification settings - Fork 90
Expand file tree
/
Copy pathmcp-handler.ts
More file actions
111 lines (95 loc) · 3.62 KB
/
Copy pathmcp-handler.ts
File metadata and controls
111 lines (95 loc) · 3.62 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
import {
createMcpHandler,
hostHeaderValidationResponse,
localhostAllowedHostnames,
localhostAllowedOrigins,
originValidationResponse
} from '@modelcontextprotocol/server'
import { createServer } from './server'
import { AuthProps as AuthPropsSchema, type AuthProps } from './auth/types'
export const MCP_ROUTE = '/mcp'
const ALLOWED_MCP_HOSTNAMES = [
...localhostAllowedHostnames(),
'staging.mcp.cloudflare.com',
'mcp.cloudflare.com'
]
const ALLOWED_MCP_ORIGIN_HOSTNAMES = [
...localhostAllowedOrigins(),
'staging.mcp.cloudflare.com',
'mcp.cloudflare.com'
]
function createAuthenticatedHandler(props: AuthProps) {
return createMcpHandler(
({ requestInfo }) => {
if (!requestInfo) {
throw new Error('The Cloudflare MCP server requires an HTTP request')
}
const codemode = new URL(requestInfo.url).searchParams.get('codemode') !== 'false'
return createServer(props, codemode)
},
// This server publishes no change notifications and intentionally keeps no
// long-lived request state. Reject subscriptions/listen before the SDK opens
// an SSE stream and pins an isolate.
{ maxSubscriptions: 0 }
)
}
// 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)
/** Validate the deployment boundary before authentication or MCP dispatch. */
export function rejectInvalidMcpRequest(request: Request): Response | undefined {
return (
hostHeaderValidationResponse(request, ALLOWED_MCP_HOSTNAMES) ??
originValidationResponse(request, ALLOWED_MCP_ORIGIN_HOSTNAMES)
)
}
function corsHeaders(request: Request): Headers | undefined {
const origin = request.headers.get('Origin')
if (!origin) return undefined
const requestedHeaders = request.headers.get('Access-Control-Request-Headers')
const headers = new Headers({
'Access-Control-Allow-Origin': origin,
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers':
requestedHeaders ??
'Content-Type, Accept, Authorization, MCP-Protocol-Version, Mcp-Method, Mcp-Name',
'Access-Control-Max-Age': '86400',
Vary: 'Origin'
})
return headers
}
function withCors(response: Response, request: Request): Response {
const cors = corsHeaders(request)
if (!cors) return response
const headers = new Headers(response.headers)
for (const [name, value] of cors) headers.set(name, value)
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers
})
}
/** Serve an allowed browser preflight without invoking authentication or a server factory. */
export function handleMcpPreflight(request: Request): Response {
return new Response(null, { status: 204, headers: corsHeaders(request) })
}
/** Serve one authenticated MCP exchange with a fresh SDK v2 server instance. */
export async function handleAuthenticatedMcpRequest(
request: Request,
rawProps: unknown
): Promise<Response> {
if (new URL(request.url).pathname !== MCP_ROUTE) {
return new Response('Not Found', { status: 404 })
}
const rejected = rejectInvalidMcpRequest(request)
if (rejected) return rejected
const props = AuthPropsSchema.parse(rawProps)
const handler = createAuthenticatedHandler(props)
return withCors(await handler.fetch(request), request)
}
/** ExportedHandler adapter required by workers-oauth-provider 0.8.x. */
export const oauthMcpHandler = {
fetch(request: Request, _env: Env, ctx: ExecutionContext) {
return handleAuthenticatedMcpRequest(request, ctx.props)
}
}