From c0e6700bfd98a43b918918d5c4a3aa4ae2e5eb0c Mon Sep 17 00:00:00 2001 From: Maximo Guk <62088388+Maximo-Guk@users.noreply.github.com> Date: Thu, 17 Apr 2025 12:20:48 -0500 Subject: [PATCH] Move observability server to it's own subdomain and adjust all app routes to be /sse --- README.md | 2 +- apps/sandbox-container/README.md | 2 +- apps/sandbox-container/container/index.ts | 4 +-- apps/sandbox-container/package.json | 1 + apps/sandbox-container/server/index.ts | 4 +-- .../worker-configuration.d.ts | 1 + apps/sandbox-container/wrangler.jsonc | 5 +-- apps/workers-bindings/src/index.ts | 4 +-- .../worker-configuration.d.ts | 2 ++ apps/workers-bindings/wrangler.jsonc | 5 ++- apps/workers-observability/README.md | 9 ++--- apps/workers-observability/package.json | 1 - apps/workers-observability/src/index.ts | 4 +-- .../worker-configuration.d.ts | 2 ++ apps/workers-observability/wrangler.jsonc | 28 +++++++++------ .../src/cloudflare-oauth-handler.ts | 35 +++++++++++++------ packages/mcp-common/src/config.ts | 8 +++++ pnpm-lock.yaml | 19 ++-------- 18 files changed, 80 insertions(+), 56 deletions(-) create mode 100644 packages/mcp-common/src/config.ts diff --git a/README.md b/README.md index 5e0d9d45..31ee1b71 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Replace the content with the following configuration. Once you restart Claude De "command": "npx", "args": [ "mcp-remote", - "https://mcp.cloudflare.com/workers/observability/sse" + "https://observability.mcp.cloudflare.com/sse" ] } } diff --git a/apps/sandbox-container/README.md b/apps/sandbox-container/README.md index 1f15535c..c27c5b26 100644 --- a/apps/sandbox-container/README.md +++ b/apps/sandbox-container/README.md @@ -12,7 +12,7 @@ Do the following from within the sandbox-container app: 2. Get the Cloudflare client id and secret from a team member and add them to the `.dev.vars` file. 3. Run `pnpm i` then `pnpm dev` to start the MCP server. 4. Run `pnpx @modelcontextprotocol/inspector` to start the MCP inspector client. -5. Open the inspector client in your browser and connect to the server via `http://localhost:8976/workers/sandbox/sse`. +5. Open the inspector client in your browser and connect to the server via `http://localhost:8976/sse`. Note: Temporary files created through files tool calls are stored in the workdir folder of this app. diff --git a/apps/sandbox-container/container/index.ts b/apps/sandbox-container/container/index.ts index d3b3e9e5..a37ee2e3 100644 --- a/apps/sandbox-container/container/index.ts +++ b/apps/sandbox-container/container/index.ts @@ -7,8 +7,8 @@ import { Hono } from 'hono' import { streamText } from 'hono/streaming' import mime from 'mime' -import { ExecParams, FilesWrite } from '../shared/schema.ts' -import { get_file_name_from_path } from './fileUtils.ts' +import { ExecParams, FilesWrite } from '../shared/schema' +import { get_file_name_from_path } from './fileUtils' import type { FileList } from '../shared/schema.ts' diff --git a/apps/sandbox-container/package.json b/apps/sandbox-container/package.json index 97f1cb15..ab9e29fb 100644 --- a/apps/sandbox-container/package.json +++ b/apps/sandbox-container/package.json @@ -40,6 +40,7 @@ "zod": "^3.24.2" }, "devDependencies": { + "@cloudflare/vitest-pool-workers": "0.8.14", "ai": "^4.3.6", "concurrently": "^9.1.2", "wrangler": "^4.9.1" diff --git a/apps/sandbox-container/server/index.ts b/apps/sandbox-container/server/index.ts index 86c3d76c..4c9dee80 100644 --- a/apps/sandbox-container/server/index.ts +++ b/apps/sandbox-container/server/index.ts @@ -30,9 +30,9 @@ export type Props = { } export default new OAuthProvider({ - apiRoute: '/workers/sandbox/sse', + apiRoute: '/sse', // @ts-ignore - apiHandler: ContainerMcpAgent.mount('/workers/sandbox/sse', { binding: 'CONTAINER_MCP_AGENT' }), + apiHandler: ContainerMcpAgent.mount('/sse', { binding: 'CONTAINER_MCP_AGENT' }), // @ts-ignore defaultHandler: CloudflareAuthHandler, authorizeEndpoint: '/oauth/authorize', diff --git a/apps/sandbox-container/worker-configuration.d.ts b/apps/sandbox-container/worker-configuration.d.ts index 8b80d824..b959b259 100644 --- a/apps/sandbox-container/worker-configuration.d.ts +++ b/apps/sandbox-container/worker-configuration.d.ts @@ -9,6 +9,7 @@ declare namespace Cloudflare { CONTAINER_MCP_AGENT: DurableObjectNamespace; CONTAINER_MANAGER: DurableObjectNamespace; AI: Ai; + ENVIRONMENT: string } } interface Env extends Cloudflare.Env {} diff --git a/apps/sandbox-container/wrangler.jsonc b/apps/sandbox-container/wrangler.jsonc index 124fd879..18dfb8e8 100644 --- a/apps/sandbox-container/wrangler.jsonc +++ b/apps/sandbox-container/wrangler.jsonc @@ -1,6 +1,6 @@ { "$schema": "node_modules/wrangler/config-schema.json", - "name": "sandbox-container", + "name": "sandbox-container-dev", "main": "server/index.ts", "compatibility_date": "2025-04-03", "compatibility_flags": ["nodejs_compat"], @@ -48,6 +48,7 @@ }, "vars": { "CLOUDFLARE_CLIENT_ID": "", - "CLOUDFLARE_CLIENT_SECRET": "" + "CLOUDFLARE_CLIENT_SECRET": "", + "ENVIRONMENT": "development" } } diff --git a/apps/workers-bindings/src/index.ts b/apps/workers-bindings/src/index.ts index 2ac90828..c6cc527b 100644 --- a/apps/workers-bindings/src/index.ts +++ b/apps/workers-bindings/src/index.ts @@ -62,9 +62,9 @@ export class WorkersBindingsMCP extends McpAgent; + ENVIRONMENT: string } } interface Env extends Cloudflare.Env {} diff --git a/apps/workers-bindings/wrangler.jsonc b/apps/workers-bindings/wrangler.jsonc index 6434ac1c..a26ce6be 100644 --- a/apps/workers-bindings/wrangler.jsonc +++ b/apps/workers-bindings/wrangler.jsonc @@ -4,7 +4,7 @@ */ { "$schema": "node_modules/wrangler/config-schema.json", - "name": "workers-bindings", + "name": "workers-bindings-dev", "main": "src/index.ts", "compatibility_date": "2025-03-10", "compatibility_flags": ["nodejs_compat"], @@ -33,6 +33,9 @@ }, "dev": { "port": 8976 + }, + "vars": { + "ENVIRONMENT": "development" } /** * Smart Placement diff --git a/apps/workers-observability/README.md b/apps/workers-observability/README.md index f5cd5a93..4f5f7aa9 100644 --- a/apps/workers-observability/README.md +++ b/apps/workers-observability/README.md @@ -58,7 +58,7 @@ Replace the content with the following configuration. Once you restart Claude De "command": "npx", "args": [ "mcp-remote", - "https://mcp-cloudflare-staging..workers.dev/workers/observability/sse" + "https://.workers.dev/sse" ] } } @@ -69,11 +69,8 @@ Once the Tools (under 🔨) show up in the interface, you can ask Claude to use ### For Local Development -If you'd like to iterate and test your MCP server, you can do so in local development. This will require you to create another OAuth App on GitHub: +If you'd like to iterate and test your MCP server, you can do so in local development. This will require you to create another OAuth App on Cloudflare: -- For the Homepage URL, specify `http://localhost:8788` -- For the Authorization callback URL, specify `http://localhost:8788/callback` -- Note your Client ID and generate a Client secret. - Create a `.dev.vars` file in your project root with: ``` @@ -107,7 +104,7 @@ You can connect your MCP server to other MCP clients like Windsurf by opening th The OAuth Provider library serves as a complete OAuth 2.1 server implementation for Cloudflare Workers. It handles the complexities of the OAuth flow, including token issuance, validation, and management. In this project, it plays the dual role of: - Authenticating MCP clients that connect to your server -- Managing the connection to GitHub's OAuth services +- Managing the connection to Cloudflare's OAuth services - Securely storing tokens and authentication state in KV storage #### Durable MCP diff --git a/apps/workers-observability/package.json b/apps/workers-observability/package.json index 8a7a19d1..4fdaeac5 100644 --- a/apps/workers-observability/package.json +++ b/apps/workers-observability/package.json @@ -24,7 +24,6 @@ "devDependencies": { "@cloudflare/vitest-pool-workers": "0.8.14", "@cloudflare/workers-types": "4.20250410.0", - "@types/jsonwebtoken": "9.0.9", "prettier": "3.5.3", "typescript": "5.5.4", "vitest": "3.0.9", diff --git a/apps/workers-observability/src/index.ts b/apps/workers-observability/src/index.ts index 847cdb21..7b7d68e1 100644 --- a/apps/workers-observability/src/index.ts +++ b/apps/workers-observability/src/index.ts @@ -67,9 +67,9 @@ export class MyMCP extends McpAgent { } export default new OAuthProvider({ - apiRoute: '/workers/observability/sse', + apiRoute: '/sse', // @ts-ignore - apiHandler: MyMCP.mount('/workers/observability/sse'), + apiHandler: MyMCP.mount('/sse'), // @ts-ignore defaultHandler: CloudflareAuthHandler, authorizeEndpoint: '/oauth/authorize', diff --git a/apps/workers-observability/worker-configuration.d.ts b/apps/workers-observability/worker-configuration.d.ts index 0bd82a47..7b5578e2 100644 --- a/apps/workers-observability/worker-configuration.d.ts +++ b/apps/workers-observability/worker-configuration.d.ts @@ -5,6 +5,8 @@ declare namespace Cloudflare { OAUTH_KV: KVNamespace; CLOUDFLARE_CLIENT_ID: string; CLOUDFLARE_CLIENT_SECRET: string; + ENVIRONMENT: string; + // eslint-disable-next-line @typescript-eslint/consistent-type-imports MCP_OBJECT: DurableObjectNamespace; } } diff --git a/apps/workers-observability/wrangler.jsonc b/apps/workers-observability/wrangler.jsonc index a3292f40..d3bee687 100644 --- a/apps/workers-observability/wrangler.jsonc +++ b/apps/workers-observability/wrangler.jsonc @@ -16,9 +16,6 @@ "observability": { "enabled": true }, - "dev": { - "port": 8976 - }, "durable_objects": { "bindings": [ { @@ -33,14 +30,19 @@ "id": "DEV_KV" } ], + "vars": { + "ENVIRONMENT": "development" + }, + "dev": { + "port": 8976 + }, "workers_dev": false, "preview_urls": false, "env": { "staging": { - "name": "mcp-cloudflare-staging", + "name": "mcp-cloudflare-workers-observability-staging", "account_id": "8995c0f49cdcf57eb54d2c1e52b7d2f3", - // enable workers-dev for staging - "workers_dev": true, + "routes": [{ "pattern": "observability-staging.mcp.cloudflare.com", "custom_domain": true }], "durable_objects": { "bindings": [ { @@ -54,12 +56,15 @@ "binding": "OAUTH_KV", "id": "18e839155d00407095d793dcf7e78f25" } - ] + ], + "vars": { + "ENVIRONMENT": "staging" + } }, "production": { - "name": "mcp-cloudflare-production", + "name": "mcp-cloudflare-workers-observability-production", "account_id": "8995c0f49cdcf57eb54d2c1e52b7d2f3", - "routes": ["mcp.cloudflare.com/*"], + "routes": [{ "pattern": "observability.mcp.cloudflare.com", "custom_domain": true }], "durable_objects": { "bindings": [ { @@ -73,7 +78,10 @@ "binding": "OAUTH_KV", "id": "f9782295993747df90c29c45ca89edb1" } - ] + ], + "vars": { + "ENVIRONMENT": "production" + } } } } diff --git a/packages/mcp-common/src/cloudflare-oauth-handler.ts b/packages/mcp-common/src/cloudflare-oauth-handler.ts index b0a9a1bb..555acef5 100644 --- a/packages/mcp-common/src/cloudflare-oauth-handler.ts +++ b/packages/mcp-common/src/cloudflare-oauth-handler.ts @@ -11,7 +11,6 @@ import { import { McpError } from './mcp-error' import type { - AuthRequest, OAuthHelpers, TokenExchangeCallbackOptions, TokenExchangeCallbackResult, @@ -25,9 +24,23 @@ type AuthContext = { CLOUDFLARE_CLIENT_SECRET: string } } -const app = new Hono() -const AuthQuery = z.object({ +const AuthRequestSchema = z.object({ + responseType: z.string(), + clientId: z.string(), + redirectUri: z.string(), + scope: z.array(z.string()), + state: z.string(), + codeChallenge: z.string().optional(), + codeChallengeMethod: z.string().optional(), +}) + +// AuthRequest but with extra params that we use in our authentication logic +export const AuthRequestSchemaWithExtraParams = AuthRequestSchema.merge( + z.object({ codeVerifier: z.string() }) +) + +export const AuthQuery = z.object({ code: z.string().describe('OAuth code from CF dash'), state: z.string().describe('Value of the OAuth state'), scope: z.string().describe('OAuth scopes granted'), @@ -127,16 +140,18 @@ export async function handleTokenExchangeCallback( } } -/**f +const app = new Hono() + +/** * OAuth Authorization Endpoint * - * This route initiates the GitHub OAuth flow when a user wants to log in. + * This route initiates the Cloudflare OAuth flow when a user wants to log in. * It creates a random state parameter to prevent CSRF attacks and stores the * original OAuth request information in KV storage for later retrieval. - * Then it redirects the user to GitHub's authorization page with the appropriate + * Then it redirects the user to Cloudflare's authorization page with the appropriate * parameters so the user can authenticate and grant permissions. */ -app.get('/oauth/authorize', async (c) => { +app.get(`/oauth/authorize`, async (c) => { try { const oauthReqInfo = await c.env.OAUTH_PROVIDER.parseAuthRequest(c.req.raw) oauthReqInfo.scope = Object.keys(DefaultScopes) @@ -163,15 +178,15 @@ app.get('/oauth/authorize', async (c) => { /** * OAuth Callback Endpoint * - * This route handles the callback from GitHub after user authentication. + * This route handles the callback from Cloudflare after user authentication. * It exchanges the temporary code for an access token, then stores some * user metadata & the auth token as part of the 'props' on the token passed * down to the client. It ends by redirecting the client back to _its_ callback URL */ -app.get('/oauth/callback', zValidator('query', AuthQuery), async (c) => { +app.get(`/oauth/callback`, zValidator('query', AuthQuery), async (c) => { try { const { state, code } = c.req.valid('query') - const oauthReqInfo = JSON.parse(atob(state)) as AuthRequest & { codeVerifier: string } + const oauthReqInfo = AuthRequestSchemaWithExtraParams.parse(JSON.parse(atob(state))) // Get the oathReqInfo out of KV if (!oauthReqInfo.clientId) { throw new McpError('Invalid State', 400) diff --git a/packages/mcp-common/src/config.ts b/packages/mcp-common/src/config.ts new file mode 100644 index 00000000..aefba22e --- /dev/null +++ b/packages/mcp-common/src/config.ts @@ -0,0 +1,8 @@ +import { z } from 'zod' + +export type MCPEnvironment = z.infer +export const MCPEnvironment = z.enum(['VITEST', 'development', 'staging', 'production']) + +export function getEnvironment(environment: string) { + return MCPEnvironment.parse(environment) +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3d1d53dc..b0d7c43a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -118,6 +118,9 @@ importers: specifier: ^3.24.2 version: 3.24.2 devDependencies: + '@cloudflare/vitest-pool-workers': + specifier: 0.8.14 + version: 0.8.14(@cloudflare/workers-types@4.20250410.0)(@vitest/runner@3.0.9)(@vitest/snapshot@3.0.9)(vitest@3.0.9) ai: specifier: ^4.3.6 version: 4.3.8(react@17.0.2)(zod@3.24.2) @@ -204,9 +207,6 @@ importers: '@cloudflare/workers-types': specifier: 4.20250410.0 version: 4.20250410.0 - '@types/jsonwebtoken': - specifier: 9.0.9 - version: 9.0.9 prettier: specifier: 3.5.3 version: 3.5.3 @@ -1205,9 +1205,6 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - '@types/jsonwebtoken@9.0.9': - resolution: {integrity: sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==} - '@types/linkify-it@5.0.0': resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} @@ -1217,9 +1214,6 @@ packages: '@types/mdurl@2.0.0': resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} - '@types/ms@2.1.0': - resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} - '@types/node-fetch@2.6.12': resolution: {integrity: sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==} @@ -4618,11 +4612,6 @@ snapshots: '@types/json5@0.0.29': {} - '@types/jsonwebtoken@9.0.9': - dependencies: - '@types/ms': 2.1.0 - '@types/node': 22.14.1 - '@types/linkify-it@5.0.0': {} '@types/markdown-it@14.1.2': @@ -4632,8 +4621,6 @@ snapshots: '@types/mdurl@2.0.0': {} - '@types/ms@2.1.0': {} - '@types/node-fetch@2.6.12': dependencies: '@types/node': 22.14.1