Skip to content

Move observability server to it's own subdomain #52

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 17, 2025
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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
}
}
Expand Down
2 changes: 1 addition & 1 deletion apps/sandbox-container/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
4 changes: 2 additions & 2 deletions apps/sandbox-container/container/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down
1 change: 1 addition & 0 deletions apps/sandbox-container/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 2 additions & 2 deletions apps/sandbox-container/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions apps/sandbox-container/worker-configuration.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ declare namespace Cloudflare {
CONTAINER_MCP_AGENT: DurableObjectNamespace<import("./server/index").ContainerMcpAgent>;
CONTAINER_MANAGER: DurableObjectNamespace<import("./server/index").ContainerManager>;
AI: Ai;
ENVIRONMENT: string
}
}
interface Env extends Cloudflare.Env {}
Expand Down
5 changes: 3 additions & 2 deletions apps/sandbox-container/wrangler.jsonc
Original file line number Diff line number Diff line change
@@ -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"],
Expand Down Expand Up @@ -48,6 +48,7 @@
},
"vars": {
"CLOUDFLARE_CLIENT_ID": "<PLACEHOLDER>",
"CLOUDFLARE_CLIENT_SECRET": "<PLACEHOLDER>"
"CLOUDFLARE_CLIENT_SECRET": "<PLACEHOLDER>",
"ENVIRONMENT": "development"
}
}
4 changes: 2 additions & 2 deletions apps/workers-bindings/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,9 @@ export class WorkersBindingsMCP extends McpAgent<Env, WorkersBindingsMCPState, P

// Export the OAuth handler as the default
export default new OAuthProvider({
apiRoute: '/workers/bindings/sse',
apiRoute: '/sse',
// @ts-ignore
apiHandler: WorkersBindingsMCP.mount('/workers/bindings/sse'),
apiHandler: WorkersBindingsMCP.mount('/sse'),
// @ts-ignore
defaultHandler: CloudflareAuthHandler,
authorizeEndpoint: '/oauth/authorize',
Expand Down
2 changes: 2 additions & 0 deletions apps/workers-bindings/worker-configuration.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ declare namespace Cloudflare {
OAUTH_KV: KVNamespace;
CLOUDFLARE_CLIENT_ID: string;
CLOUDFLARE_CLIENT_SECRET: string;
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
MCP_OBJECT: DurableObjectNamespace<import("./src/index").WorkersBindingsMCP>;
ENVIRONMENT: string
}
}
interface Env extends Cloudflare.Env {}
Expand Down
5 changes: 4 additions & 1 deletion apps/workers-bindings/wrangler.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down Expand Up @@ -33,6 +33,9 @@
},
"dev": {
"port": 8976
},
"vars": {
"ENVIRONMENT": "development"
}
/**
* Smart Placement
Expand Down
9 changes: 3 additions & 6 deletions apps/workers-observability/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.<your-subdomain>.workers.dev/workers/observability/sse"
"https://<your-subdomain>.workers.dev/sse"
]
}
}
Expand All @@ -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:

```
Expand Down Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion apps/workers-observability/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions apps/workers-observability/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,9 @@ export class MyMCP extends McpAgent<Env, State, Props> {
}

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',
Expand Down
2 changes: 2 additions & 0 deletions apps/workers-observability/worker-configuration.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<import("./src/index").MyMCP>;
}
}
Expand Down
28 changes: 18 additions & 10 deletions apps/workers-observability/wrangler.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,6 @@
"observability": {
"enabled": true
},
"dev": {
"port": 8976
},
"durable_objects": {
"bindings": [
{
Expand All @@ -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": [
{
Expand All @@ -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": [
{
Expand All @@ -73,7 +78,10 @@
"binding": "OAUTH_KV",
"id": "f9782295993747df90c29c45ca89edb1"
}
]
],
"vars": {
"ENVIRONMENT": "production"
}
}
}
}
35 changes: 25 additions & 10 deletions packages/mcp-common/src/cloudflare-oauth-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
import { McpError } from './mcp-error'

import type {
AuthRequest,
OAuthHelpers,
TokenExchangeCallbackOptions,
TokenExchangeCallbackResult,
Expand All @@ -25,9 +24,23 @@ type AuthContext = {
CLOUDFLARE_CLIENT_SECRET: string
}
}
const app = new Hono<AuthContext>()

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'),
Expand Down Expand Up @@ -127,16 +140,18 @@ export async function handleTokenExchangeCallback(
}
}

/**f
const app = new Hono<AuthContext>()

/**
* 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)
Expand All @@ -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)
Expand Down
8 changes: 8 additions & 0 deletions packages/mcp-common/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { z } from 'zod'

export type MCPEnvironment = z.infer<typeof MCPEnvironment>
export const MCPEnvironment = z.enum(['VITEST', 'development', 'staging', 'production'])

export function getEnvironment(environment: string) {
return MCPEnvironment.parse(environment)
}
19 changes: 3 additions & 16 deletions pnpm-lock.yaml

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

Loading