-
Notifications
You must be signed in to change notification settings - Fork 71
Expand file tree
/
Copy pathindex.ts
More file actions
146 lines (129 loc) · 5.2 KB
/
Copy pathindex.ts
File metadata and controls
146 lines (129 loc) · 5.2 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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
import OAuthProvider, { getOAuthApi } from '@cloudflare/workers-oauth-provider'
import { Hono } from 'hono'
import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js'
import { createServer } from './server'
import { createAuthHandlers, handleTokenExchangeCallback } from './auth/oauth-handler'
import { isDirectApiToken, handleApiTokenRequest } from './auth/api-token-mode'
import { processSpec, extractProducts } from './spec-processor'
import { buildNonCodemodeTools, type OperationInfo } from './openapi'
import type { AuthProps } from './auth/types'
// GlobalOutbound lives with the execute tool (its only caller); wrangler
// resolves the GLOBAL_OUTBOUND worker-loader entrypoint from this entry module,
// so it must be re-exported here.
export { GlobalOutbound } from './tools/execute'
type McpContext = {
Bindings: Env
}
/**
* Create an MCP response for the authenticated session described by `props`.
*/
async function createMcpResponse(
request: Request,
ctx: ExecutionContext,
props: AuthProps
): Promise<Response> {
const url = new URL(request.url)
const codemode = url.searchParams.get('codemode') !== 'false'
const server = await createServer(props, codemode)
const transport = new WebStandardStreamableHTTPServerTransport({
sessionIdGenerator: undefined,
enableJsonResponse: true,
retryInterval: 1000
})
await server.connect(transport)
const response = await transport.handleRequest(request)
ctx.waitUntil(transport.close())
return response
}
/**
* Create MCP API handler using Hono
*/
function createMcpHandler() {
const app = new Hono<McpContext>()
app.post('/mcp', async (c) => {
// Props are passed via ExecutionContext by workers-oauth-provider
const ctx = c.executionCtx as ExecutionContext & { props?: AuthProps }
const props = ctx.props
if (!props || !props.accessToken) {
return new Response(JSON.stringify({ error: 'Not authenticated' }), {
status: 401,
headers: { 'Content-Type': 'application/json' }
})
}
return createMcpResponse(c.req.raw, ctx, props)
})
return app
}
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
// Check for direct API token first (like GitHub MCP's PAT support)
if (isDirectApiToken(request)) {
const response = await handleApiTokenRequest(request, (props) =>
createMcpResponse(request, ctx, props)
)
if (response) return response
}
// OAuth mode - handle via workers-oauth-provider
const oauthOptions: ConstructorParameters<typeof OAuthProvider>[0] = {
apiHandlers: {
// @ts-ignore - Hono apps are compatible with ExportedHandler at runtime
'/mcp': createMcpHandler()
},
// @ts-ignore - Hono apps are compatible with ExportedHandler at runtime
defaultHandler: createAuthHandlers(),
authorizeEndpoint: '/authorize',
tokenEndpoint: '/token',
clientRegistrationEndpoint: '/register',
tokenExchangeCallback: (options) =>
handleTokenExchangeCallback(
options,
env.CLOUDFLARE_CLIENT_ID,
env.CLOUDFLARE_CLIENT_SECRET,
// Lazily build helpers (only invoked on terminal invalid_grant) so we
// can revoke the dead grant. env.OAUTH_PROVIDER is NOT injected during
// the token endpoint, so we must construct the API explicitly here.
() => getOAuthApi(oauthOptions, env)
),
resourceMetadata: {
resource_name: 'Cloudflare API MCP Server'
},
accessTokenTTL: 3600,
refreshTokenTTL: 2592000, // 30 days
// TODO: Remove after 2026-05-01 — all pre-0.4.0 grants will have expired by then
resourceMatchOriginOnly: true
}
return new OAuthProvider(oauthOptions).fetch(request, env, ctx)
},
async scheduled(
_controller: ScheduledController,
env: Env,
_ctx: ExecutionContext
): Promise<void> {
console.log('Fetching OpenAPI spec from:', env.OPENAPI_SPEC_URL)
const response = await fetch(env.OPENAPI_SPEC_URL)
if (!response.ok) {
throw new Error(`Failed to fetch OpenAPI spec: ${response.status}`)
}
const rawSpec = (await response.json()) as Record<string, unknown>
console.log('Processing spec, resolving $refs...')
const processed = processSpec(rawSpec)
const specJson = JSON.stringify(processed)
const products = extractProducts(rawSpec)
const productsJson = JSON.stringify(products)
const paths = (processed as { paths: Record<string, Record<string, OperationInfo>> }).paths
const nonCodemodeToolsJson = JSON.stringify(buildNonCodemodeTools(paths))
console.log(`Writing spec to R2 (${(specJson.length / 1024).toFixed(0)} KB)`)
await Promise.all([
env.SPEC_BUCKET.put('spec.json', specJson, {
httpMetadata: { contentType: 'application/json' }
}),
env.SPEC_BUCKET.put('products.json', productsJson, {
httpMetadata: { contentType: 'application/json' }
}),
env.SPEC_BUCKET.put('non-codemode-tools.json', nonCodemodeToolsJson, {
httpMetadata: { contentType: 'application/json' }
})
])
console.log(`Spec updated successfully (${products.length} products)`)
}
}