diff --git a/apps/cloudflare-one-casb/.dev.vars.example b/apps/cloudflare-one-casb/.dev.vars.example index e6c0feb7..c087f669 100644 --- a/apps/cloudflare-one-casb/.dev.vars.example +++ b/apps/cloudflare-one-casb/.dev.vars.example @@ -1,2 +1,5 @@ CLOUDFLARE_CLIENT_ID= -CLOUDFLARE_CLIENT_SECRET= \ No newline at end of file +CLOUDFLARE_CLIENT_SECRET= +DEV_DISABLE_OAUTH= +DEV_CLOUDFLARE_API_TOKEN= +DEV_CLOUDFLARE_EMAIL= \ No newline at end of file diff --git a/apps/cloudflare-one-casb/src/index.ts b/apps/cloudflare-one-casb/src/index.ts index 550af986..17db52a3 100644 --- a/apps/cloudflare-one-casb/src/index.ts +++ b/apps/cloudflare-one-casb/src/index.ts @@ -1,10 +1,12 @@ import OAuthProvider from '@cloudflare/workers-oauth-provider' import { McpAgent } from 'agents/mcp' +import { createApiHandler } from '@repo/mcp-common/src/api-handler' import { createAuthHandlers, handleTokenExchangeCallback, } from '@repo/mcp-common/src/cloudflare-oauth-handler' +import { handleDevMode } from '@repo/mcp-common/src/dev-mode' import { getUserDetails, UserDetails } from '@repo/mcp-common/src/durable-objects/user_details' import { getEnv } from '@repo/mcp-common/src/env' import { RequiredScopes } from '@repo/mcp-common/src/scopes' @@ -14,7 +16,7 @@ import { registerAccountTools } from '@repo/mcp-common/src/tools/account' import { MetricsTracker } from '../../../packages/mcp-observability/src' import { registerIntegrationsTools } from './tools/integrations' -import type { AccountSchema, UserSchema } from '@repo/mcp-common/src/cloudflare-oauth-handler' +import type { AuthProps } from '@repo/mcp-common/src/cloudflare-oauth-handler' import type { Env } from './context' export { UserDetails } @@ -26,13 +28,11 @@ const metrics = new MetricsTracker(env.MCP_METRICS, { version: env.MCP_SERVER_VERSION, }) -export type Props = { - accessToken: string - user: UserSchema['result'] - accounts: AccountSchema['result'] -} +// Context from the auth process, encrypted & stored in the auth token +// and provided to the DurableMCP as this.props +type Props = AuthProps -export type State = { activeAccountId: string | null } +type State = { activeAccountId: string | null } export class CASBMCP extends McpAgent { _server: CloudflareMCPServer | undefined set server(server: CloudflareMCPServer) { @@ -92,17 +92,29 @@ const CloudflareOneCasbScopes = { 'teams:read': 'See Cloudflare One Resources', } as const -export default new OAuthProvider({ - apiRoute: '/sse', - // @ts-ignore - apiHandler: CASBMCP.mount('/sse'), - // @ts-ignore - defaultHandler: createAuthHandlers({ scopes: CloudflareOneCasbScopes, metrics }), - authorizeEndpoint: '/oauth/authorize', - tokenEndpoint: '/token', - tokenExchangeCallback: (options) => - handleTokenExchangeCallback(options, env.CLOUDFLARE_CLIENT_ID, env.CLOUDFLARE_CLIENT_SECRET), - // Cloudflare access token TTL - accessTokenTTL: 3600, - clientRegistrationEndpoint: '/register', -}) +export default { + fetch: async (req: Request, env: Env, ctx: ExecutionContext) => { + if (env.ENVIRONMENT === 'development' && env.DEV_DISABLE_OAUTH === 'true') { + return await handleDevMode(CASBMCP, req, env, ctx) + } + + return new OAuthProvider({ + apiRoute: ['/mcp', '/sse'], + // @ts-ignore + apiHandler: createApiHandler(CASBMCP), + // @ts-ignore + defaultHandler: createAuthHandlers({ scopes: CloudflareOneCasbScopes, metrics }), + authorizeEndpoint: '/oauth/authorize', + tokenEndpoint: '/token', + tokenExchangeCallback: (options) => + handleTokenExchangeCallback( + options, + env.CLOUDFLARE_CLIENT_ID, + env.CLOUDFLARE_CLIENT_SECRET + ), + // Cloudflare access token TTL + accessTokenTTL: 3600, + clientRegistrationEndpoint: '/register', + }).fetch(req, env, ctx) + }, +} diff --git a/apps/dex-analysis/.dev.vars.example b/apps/dex-analysis/.dev.vars.example new file mode 100644 index 00000000..c087f669 --- /dev/null +++ b/apps/dex-analysis/.dev.vars.example @@ -0,0 +1,5 @@ +CLOUDFLARE_CLIENT_ID= +CLOUDFLARE_CLIENT_SECRET= +DEV_DISABLE_OAUTH= +DEV_CLOUDFLARE_API_TOKEN= +DEV_CLOUDFLARE_EMAIL= \ No newline at end of file diff --git a/apps/dex-analysis/src/index.ts b/apps/dex-analysis/src/index.ts index ebb71e4c..03b226cb 100644 --- a/apps/dex-analysis/src/index.ts +++ b/apps/dex-analysis/src/index.ts @@ -1,11 +1,12 @@ import OAuthProvider from '@cloudflare/workers-oauth-provider' import { McpAgent } from 'agents/mcp' +import { createApiHandler } from '@repo/mcp-common/src/api-handler' import { createAuthHandlers, - getUserAndAccounts, handleTokenExchangeCallback, } from '@repo/mcp-common/src/cloudflare-oauth-handler' +import { handleDevMode } from '@repo/mcp-common/src/dev-mode' import { getUserDetails, UserDetails } from '@repo/mcp-common/src/durable-objects/user_details' import { getEnv } from '@repo/mcp-common/src/env' import { RequiredScopes } from '@repo/mcp-common/src/scopes' @@ -15,7 +16,7 @@ import { MetricsTracker } from '@repo/mcp-observability' import { registerDEXTools } from './tools/dex' -import type { AccountSchema, UserSchema } from '@repo/mcp-common/src/cloudflare-oauth-handler' +import type { AuthProps } from '@repo/mcp-common/src/cloudflare-oauth-handler' import type { Env } from './context' export { UserDetails } @@ -29,13 +30,9 @@ const metrics = new MetricsTracker(env.MCP_METRICS, { // Context from the auth process, encrypted & stored in the auth token // and provided to the DurableMCP as this.props -export type Props = { - accessToken: string - user: UserSchema['result'] - accounts: AccountSchema['result'] -} +type Props = AuthProps -export type State = { activeAccountId: string | null } +type State = { activeAccountId: string | null } export class CloudflareDEXMCP extends McpAgent { _server: CloudflareMCPServer | undefined @@ -97,29 +94,15 @@ const DexScopes = { 'dex:read': 'See Cloudflare Cloudflare DEX data for your account', } as const -// TODO: Move this in to mcp-common -async function handleDevMode(req: Request, env: Env, ctx: ExecutionContext) { - const { user, accounts } = await getUserAndAccounts(env.DEV_CLOUDFLARE_API_TOKEN, { - 'X-Auth-Email': env.DEV_CLOUDFLARE_EMAIL, - 'X-Auth-Key': env.DEV_CLOUDFLARE_API_TOKEN, - }) - ctx.props = { - accessToken: env.DEV_CLOUDFLARE_API_TOKEN, - user, - accounts, - } as Props - return CloudflareDEXMCP.mount('/sse').fetch(req, env, ctx) -} - export default { fetch: async (req: Request, env: Env, ctx: ExecutionContext) => { if (env.ENVIRONMENT === 'development' && env.DEV_DISABLE_OAUTH === 'true') { - return await handleDevMode(req, env, ctx) + return await handleDevMode(CloudflareDEXMCP, req, env, ctx) } return new OAuthProvider({ - apiRoute: '/sse', - apiHandler: CloudflareDEXMCP.mount('/sse'), + apiRoute: ['/mcp', '/sse'], + apiHandler: createApiHandler(CloudflareDEXMCP), // @ts-ignore defaultHandler: createAuthHandlers({ scopes: DexScopes, metrics }), authorizeEndpoint: '/oauth/authorize', diff --git a/apps/radar/src/index.ts b/apps/radar/src/index.ts index fdef3e56..2cfa26cb 100644 --- a/apps/radar/src/index.ts +++ b/apps/radar/src/index.ts @@ -1,11 +1,12 @@ import OAuthProvider from '@cloudflare/workers-oauth-provider' import { McpAgent } from 'agents/mcp' +import { createApiHandler } from '@repo/mcp-common/src/api-handler' import { createAuthHandlers, - getUserAndAccounts, handleTokenExchangeCallback, } from '@repo/mcp-common/src/cloudflare-oauth-handler' +import { handleDevMode } from '@repo/mcp-common/src/dev-mode' import { getEnv } from '@repo/mcp-common/src/env' import { RequiredScopes } from '@repo/mcp-common/src/scopes' import { CloudflareMCPServer } from '@repo/mcp-common/src/server' @@ -14,7 +15,7 @@ import { MetricsTracker } from '@repo/mcp-observability' import { registerRadarTools } from './tools/radar' import { registerUrlScannerTools } from './tools/url-scanner' -import type { UserSchema } from '@repo/mcp-common/src/cloudflare-oauth-handler' +import type { AuthProps } from '@repo/mcp-common/src/cloudflare-oauth-handler' import type { Env } from './context' const env = getEnv() @@ -26,12 +27,9 @@ const metrics = new MetricsTracker(env.MCP_METRICS, { // Context from the auth process, encrypted & stored in the auth token // and provided to the DurableMCP as this.props -export type Props = { - accessToken: string - user: UserSchema['result'] -} +type Props = AuthProps -export type State = never +type State = never export class RadarMCP extends McpAgent { _server: CloudflareMCPServer | undefined @@ -73,29 +71,15 @@ export class RadarMCP extends McpAgent { // Also remove URL_SCANNER_API_TOKEN env var const RadarScopes = { ...RequiredScopes } as const -// TODO: Move this in to mcp-common -async function handleDevMode(req: Request, env: Env, ctx: ExecutionContext) { - const { user, accounts } = await getUserAndAccounts(env.DEV_CLOUDFLARE_API_TOKEN, { - 'X-Auth-Email': env.DEV_CLOUDFLARE_EMAIL, - 'X-Auth-Key': env.DEV_CLOUDFLARE_API_TOKEN, - }) - ctx.props = { - accessToken: env.DEV_CLOUDFLARE_API_TOKEN, - user, - accounts, - } as Props - return RadarMCP.mount('/sse').fetch(req, env, ctx) -} - export default { fetch: async (req: Request, env: Env, ctx: ExecutionContext) => { if (env.ENVIRONMENT === 'development' && env.DEV_DISABLE_OAUTH === 'true') { - return await handleDevMode(req, env, ctx) + return await handleDevMode(RadarMCP, req, env, ctx) } return new OAuthProvider({ - apiRoute: '/sse', - apiHandler: RadarMCP.mount('/sse'), + apiRoute: ['/mcp', '/sse'], + apiHandler: createApiHandler(RadarMCP), // @ts-ignore defaultHandler: createAuthHandlers({ scopes: RadarScopes, metrics }), authorizeEndpoint: '/oauth/authorize', diff --git a/apps/sandbox-container/server/index.ts b/apps/sandbox-container/server/index.ts index 1343e4be..7d5dd806 100644 --- a/apps/sandbox-container/server/index.ts +++ b/apps/sandbox-container/server/index.ts @@ -1,10 +1,11 @@ import OAuthProvider from '@cloudflare/workers-oauth-provider' +import { createApiHandler } from '@repo/mcp-common/src/api-handler' import { createAuthHandlers, - getUserAndAccounts, handleTokenExchangeCallback, } from '@repo/mcp-common/src/cloudflare-oauth-handler' +import { handleDevMode } from '@repo/mcp-common/src/dev-mode' import { getEnv } from '@repo/mcp-common/src/env' import { RequiredScopes } from '@repo/mcp-common/src/scopes' import { MetricsTracker } from '@repo/mcp-observability' @@ -13,7 +14,7 @@ import { ContainerManager } from './containerManager' import { ContainerMcpAgent } from './containerMcp' import type { McpAgent } from 'agents/mcp' -import type { AccountSchema, UserSchema } from '@repo/mcp-common/src/cloudflare-oauth-handler' +import type { AuthProps } from '@repo/mcp-common/src/cloudflare-oauth-handler' import type { Env } from './context' export { ContainerManager, ContainerMcpAgent } @@ -27,11 +28,7 @@ const metrics = new MetricsTracker(env.MCP_METRICS, { // Context from the auth process, encrypted & stored in the auth token // and provided to the DurableMCP as this.props -export type Props = { - accessToken: string - user: UserSchema['result'] - accounts: AccountSchema['result'] -} +export type Props = AuthProps const ContainerScopes = { ...RequiredScopes, @@ -40,20 +37,6 @@ const ContainerScopes = { 'See and change Cloudflare Workers data such as zones, KV storage, namespaces, scripts, and routes.', } as const -// TODO: Move this in to mcp-common -async function handleDevMode(req: Request, env: Env, ctx: ExecutionContext) { - const { user, accounts } = await getUserAndAccounts(env.DEV_CLOUDFLARE_API_TOKEN, { - 'X-Auth-Email': env.DEV_CLOUDFLARE_EMAIL, - 'X-Auth-Key': env.DEV_CLOUDFLARE_API_TOKEN, - }) - ctx.props = { - accessToken: env.DEV_CLOUDFLARE_API_TOKEN, - user, - accounts, - } as Props - return ContainerMcpAgent.mount('/sse').fetch(req, env, ctx) -} - export default { fetch: async (req: Request, env: Env, ctx: ExecutionContext) => { // @ts-ignore @@ -74,12 +57,12 @@ export default { } if (env.ENVIRONMENT === 'dev' && env.DEV_DISABLE_OAUTH === 'true') { - return await handleDevMode(req, env, ctx) + return await handleDevMode(ContainerMcpAgent, req, env, ctx) } return new OAuthProvider({ - apiRoute: '/sse', - apiHandler: ContainerMcpAgent.mount('/sse', { binding: 'CONTAINER_MCP_AGENT' }), + apiRoute: ['/mcp', '/sse'], + apiHandler: createApiHandler(ContainerMcpAgent, { binding: 'CONTAINER_MCP_AGENT' }), // @ts-ignore defaultHandler: createAuthHandlers({ scopes: ContainerScopes, metrics }), authorizeEndpoint: '/oauth/authorize', diff --git a/apps/workers-bindings/src/index.ts b/apps/workers-bindings/src/index.ts index c799dacd..264b66cd 100644 --- a/apps/workers-bindings/src/index.ts +++ b/apps/workers-bindings/src/index.ts @@ -1,6 +1,7 @@ import OAuthProvider from '@cloudflare/workers-oauth-provider' import { McpAgent } from 'agents/mcp' +import { createApiHandler } from '@repo/mcp-common/src/api-handler' import { createAuthHandlers, getUserAndAccounts, @@ -17,7 +18,7 @@ import { registerR2BucketTools } from '@repo/mcp-common/src/tools/r2_bucket' import { registerWorkersTools } from '@repo/mcp-common/src/tools/worker' import { MetricsTracker } from '@repo/mcp-observability' -import type { AccountSchema, UserSchema } from '@repo/mcp-common/src/cloudflare-oauth-handler' +import type { AuthProps } from '@repo/mcp-common/src/cloudflare-oauth-handler' import type { Env } from './context' export { UserDetails } @@ -33,11 +34,7 @@ export type WorkersBindingsMCPState = { activeAccountId: string | null } // Context from the auth process, encrypted & stored in the auth token // and provided to the DurableMCP as this.props -export type Props = { - accessToken: string - user: UserSchema['result'] - accounts: AccountSchema['result'] -} +type Props = AuthProps export class WorkersBindingsMCP extends McpAgent { _server: CloudflareMCPServer | undefined @@ -129,8 +126,8 @@ export default { } return new OAuthProvider({ - apiRoute: '/sse', - apiHandler: WorkersBindingsMCP.mount('/sse'), + apiRoute: ['/mcp', '/sse'], + apiHandler: createApiHandler(WorkersBindingsMCP), // @ts-ignore defaultHandler: createAuthHandlers({ scopes: BindingsScopes, metrics }), authorizeEndpoint: '/oauth/authorize', diff --git a/apps/workers-observability/.dev.vars.example b/apps/workers-observability/.dev.vars.example new file mode 100644 index 00000000..c087f669 --- /dev/null +++ b/apps/workers-observability/.dev.vars.example @@ -0,0 +1,5 @@ +CLOUDFLARE_CLIENT_ID= +CLOUDFLARE_CLIENT_SECRET= +DEV_DISABLE_OAUTH= +DEV_CLOUDFLARE_API_TOKEN= +DEV_CLOUDFLARE_EMAIL= \ No newline at end of file diff --git a/apps/workers-observability/src/index.ts b/apps/workers-observability/src/index.ts index afdbe4ca..67bc97cc 100644 --- a/apps/workers-observability/src/index.ts +++ b/apps/workers-observability/src/index.ts @@ -1,11 +1,12 @@ import OAuthProvider from '@cloudflare/workers-oauth-provider' import { McpAgent } from 'agents/mcp' +import { createApiHandler } from '@repo/mcp-common/src/api-handler' import { createAuthHandlers, - getUserAndAccounts, handleTokenExchangeCallback, } from '@repo/mcp-common/src/cloudflare-oauth-handler' +import { handleDevMode } from '@repo/mcp-common/src/dev-mode' import { getUserDetails, UserDetails } from '@repo/mcp-common/src/durable-objects/user_details' import { getEnv } from '@repo/mcp-common/src/env' import { RequiredScopes } from '@repo/mcp-common/src/scopes' @@ -17,7 +18,7 @@ import { registerWorkersTools } from '@repo/mcp-common/src/tools/worker' import { MetricsTracker } from '../../../packages/mcp-observability/src' import { registerObservabilityTools } from './tools/observability' -import type { AccountSchema, UserSchema } from '@repo/mcp-common/src/cloudflare-oauth-handler' +import type { AuthProps } from '@repo/mcp-common/src/cloudflare-oauth-handler' import type { Env } from './context' export { UserDetails } @@ -31,13 +32,9 @@ const metrics = new MetricsTracker(env.MCP_METRICS, { // Context from the auth process, encrypted & stored in the auth token // and provided to the DurableMCP as this.props -export type Props = { - accessToken: string - user: UserSchema['result'] - accounts: AccountSchema['result'] -} +type Props = AuthProps -export type State = { activeAccountId: string | null } +type State = { activeAccountId: string | null } export class ObservabilityMCP extends McpAgent { _server: CloudflareMCPServer | undefined @@ -126,29 +123,15 @@ const ObservabilityScopes = { 'workers_observability:read': 'See observability logs for your account', } as const -// TODO: Move this in to mcp-common -async function handleDevMode(req: Request, env: Env, ctx: ExecutionContext) { - const { user, accounts } = await getUserAndAccounts(env.DEV_CLOUDFLARE_API_TOKEN, { - 'X-Auth-Email': env.DEV_CLOUDFLARE_EMAIL, - 'X-Auth-Key': env.DEV_CLOUDFLARE_API_TOKEN, - }) - ctx.props = { - accessToken: env.DEV_CLOUDFLARE_API_TOKEN, - user, - accounts, - } as Props - return ObservabilityMCP.mount('/sse').fetch(req, env, ctx) -} - export default { fetch: async (req: Request, env: Env, ctx: ExecutionContext) => { if (env.ENVIRONMENT === 'development' && env.DEV_DISABLE_OAUTH === 'true') { - return await handleDevMode(req, env, ctx) + return await handleDevMode(ObservabilityMCP, req, env, ctx) } return new OAuthProvider({ - apiRoute: '/sse', - apiHandler: ObservabilityMCP.mount('/sse'), + apiRoute: ['/mcp', '/sse'], + apiHandler: createApiHandler(ObservabilityMCP), // @ts-ignore defaultHandler: createAuthHandlers({ scopes: ObservabilityScopes, metrics }), authorizeEndpoint: '/oauth/authorize', diff --git a/packages/mcp-common/src/api-handler.ts b/packages/mcp-common/src/api-handler.ts new file mode 100644 index 00000000..6d473e4b --- /dev/null +++ b/packages/mcp-common/src/api-handler.ts @@ -0,0 +1,19 @@ +import type { McpAgent } from 'agents/mcp' + +// Support both SSE and Streamable HTTP +export function createApiHandler< + T extends typeof McpAgent>, +>(agent: T, opts?: { binding?: string }) { + return { + fetch: (req: Request, env: unknown, ctx: ExecutionContext) => { + const url = new URL(req.url) + if (url.pathname === '/sse' || url.pathname === '/sse/message') { + return agent.serveSSE('/sse', { binding: opts?.binding }).fetch(req, env, ctx) + } + if (url.pathname === '/mcp') { + return agent.serve('/mcp', { binding: opts?.binding }).fetch(req, env, ctx) + } + return new Response('Not found', { status: 404 }) + }, + } +} diff --git a/packages/mcp-common/src/cloudflare-oauth-handler.ts b/packages/mcp-common/src/cloudflare-oauth-handler.ts index c3afa880..404d2ff0 100644 --- a/packages/mcp-common/src/cloudflare-oauth-handler.ts +++ b/packages/mcp-common/src/cloudflare-oauth-handler.ts @@ -63,6 +63,12 @@ const AccountResponseSchema = z.object({ ), }) +export type AuthProps = { + accessToken: string + user: UserSchema['result'] + accounts: AccountSchema['result'] +} + export async function getUserAndAccounts( accessToken: string, devModeHeaders?: HeadersInit diff --git a/packages/mcp-common/src/dev-mode.ts b/packages/mcp-common/src/dev-mode.ts new file mode 100644 index 00000000..d176ad92 --- /dev/null +++ b/packages/mcp-common/src/dev-mode.ts @@ -0,0 +1,24 @@ +import { getUserAndAccounts } from './cloudflare-oauth-handler' + +import type { McpAgent } from 'agents/mcp' +import type { AuthProps } from './cloudflare-oauth-handler' + +interface RequiredEnv { + DEV_CLOUDFLARE_EMAIL: string + DEV_CLOUDFLARE_API_TOKEN: string +} + +export async function handleDevMode< + T extends typeof McpAgent>, +>(agent: T, req: Request, env: RequiredEnv, ctx: ExecutionContext) { + const { user, accounts } = await getUserAndAccounts(env.DEV_CLOUDFLARE_API_TOKEN, { + 'X-Auth-Email': env.DEV_CLOUDFLARE_EMAIL, + 'X-Auth-Key': env.DEV_CLOUDFLARE_API_TOKEN, + }) + ctx.props = { + accessToken: env.DEV_CLOUDFLARE_API_TOKEN, + user, + accounts, + } as AuthProps + return agent.mount('/sse').fetch(req, env, ctx) +}