From 0a7b32251c09dc4e1d974af3294d8d8a4dc09d77 Mon Sep 17 00:00:00 2001 From: Glen Maddern Date: Tue, 1 Jul 2025 12:21:19 +1000 Subject: [PATCH 1/2] Moving the non-authed cfagents tests to /public --- examples/servers/cf-agents/src/index.ts | 22 ++++++++++++++++++---- package.json | 3 ++- scripts/pre-commit | 24 ++++++++++++++++++++++++ test/integration/mcp-connection.test.ts | 4 ++-- 4 files changed, 46 insertions(+), 7 deletions(-) create mode 100755 scripts/pre-commit diff --git a/examples/servers/cf-agents/src/index.ts b/examples/servers/cf-agents/src/index.ts index 3d335c3..4a0dee6 100644 --- a/examples/servers/cf-agents/src/index.ts +++ b/examples/servers/cf-agents/src/index.ts @@ -58,13 +58,27 @@ export default { fetch(request: Request, env: Env, ctx: ExecutionContext) { const url = new URL(request.url) - if (url.pathname === '/sse' || url.pathname === '/sse/message') { - return MyMCP.serveSSE('/sse').fetch(request, env, ctx) + if (url.pathname === '/public/sse' || url.pathname === '/public/sse/message') { + return MyMCP.serveSSE('/public/sse').fetch(request, env, ctx) } - if (url.pathname === '/mcp') { - return MyMCP.serve('/mcp').fetch(request, env, ctx) + if (url.pathname === '/public/mcp') { + return MyMCP.serve('/public/mcp').fetch(request, env, ctx) } + // + // if (url.pathname.startsWith('/authed')) { + // return new OAuthProvider({ + // apiRoute: "/authed/mcp", + // // TODO: fix these types + // // @ts-expect-error + // apiHandler: MyMCP.mount("/sse"), + // // @ts-expect-error + // defaultHandler: app, + // authorizeEndpoint: "/authorize", + // tokenEndpoint: "/token", + // clientRegistrationEndpoint: "/register", + // }).fetch(request, env, ctx) + // } return new Response('Not found', { status: 404 }) }, diff --git a/package.json b/package.json index 9572ce2..66e5ba5 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,8 @@ "prettier:fix": "prettier --write .", "fix:oranda": "sed -i 's/```tsx/```ts/g' README.md", "build:site": "npx @axodotdev/oranda build", - "deploy:site": "npx wrangler deploy" + "deploy:site": "npx wrangler deploy", + "postinstall": "ln -sf ../../scripts/pre-commit .git/hooks/pre-commit" }, "dependencies": { "strict-url-sanitise": "^0.0.1" diff --git a/scripts/pre-commit b/scripts/pre-commit new file mode 100755 index 0000000..a0fb64f --- /dev/null +++ b/scripts/pre-commit @@ -0,0 +1,24 @@ +#!/bin/sh + +# Pre-commit hook to run prettier on staged files +# This hook is called by "git commit" and formats only the files being committed. + +echo "Running prettier on staged files..." + +# Get list of staged files that prettier can handle +STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(js|jsx|ts|tsx|json|css|scss|md)$' | tr '\n' ' ') + +if [ -z "$STAGED_FILES" ]; then + echo "No staged files need formatting." + exit 0 +fi + +echo "Formatting files: $STAGED_FILES" + +# Run prettier on staged files +pnpm prettier --write $STAGED_FILES + +# Add the formatted files back to staging +git add $STAGED_FILES + +echo "Prettier formatting completed." \ No newline at end of file diff --git a/test/integration/mcp-connection.test.ts b/test/integration/mcp-connection.test.ts index 85738c9..4c46eb0 100644 --- a/test/integration/mcp-connection.test.ts +++ b/test/integration/mcp-connection.test.ts @@ -30,12 +30,12 @@ function getMCPServers() { }, { name: 'cf-agents', - url: `http://localhost:${state.cfAgentsPort}/mcp`, + url: `http://localhost:${state.cfAgentsPort}/public/mcp`, expectedTools: 1, // Minimum expected tools count }, { name: 'cf-agents-sse', - url: `http://localhost:${state.cfAgentsPort}/sse`, + url: `http://localhost:${state.cfAgentsPort}/public/sse`, expectedTools: 1, // Minimum expected tools count }, ] From 0094000c1e2b30320484055fccd6fd0760c7076f Mon Sep 17 00:00:00 2001 From: Glen Maddern Date: Tue, 1 Jul 2025 13:16:28 +1000 Subject: [PATCH 2/2] Added OAuth CF Agents tests --- examples/servers/cf-agents/package.json | 2 + examples/servers/cf-agents/pnpm-lock.yaml | 19 ++++++ examples/servers/cf-agents/src/index.ts | 59 ++++++++++++++----- .../cf-agents/worker-configuration.d.ts | 3 +- examples/servers/cf-agents/wrangler.jsonc | 8 ++- test/integration/mcp-connection.test.ts | 32 ++++++---- 6 files changed, 96 insertions(+), 27 deletions(-) diff --git a/examples/servers/cf-agents/package.json b/examples/servers/cf-agents/package.json index b093588..180b46c 100644 --- a/examples/servers/cf-agents/package.json +++ b/examples/servers/cf-agents/package.json @@ -17,8 +17,10 @@ }, "devDependencies": { "@biomejs/biome": "^2.0.4", + "@cloudflare/workers-oauth-provider": "^0.0.5", "@types/node": "^24.0.7", "agents": "^0.0.100", + "hono": "^4.8.3", "typescript": "^5.8.3", "wrangler": "^4.22.0" } diff --git a/examples/servers/cf-agents/pnpm-lock.yaml b/examples/servers/cf-agents/pnpm-lock.yaml index 7d5e7ae..7c3d7be 100644 --- a/examples/servers/cf-agents/pnpm-lock.yaml +++ b/examples/servers/cf-agents/pnpm-lock.yaml @@ -18,12 +18,18 @@ importers: '@biomejs/biome': specifier: ^2.0.4 version: 2.0.6 + '@cloudflare/workers-oauth-provider': + specifier: ^0.0.5 + version: 0.0.5 '@types/node': specifier: ^24.0.7 version: 24.0.7 agents: specifier: ^0.0.100 version: 0.0.100(@cloudflare/workers-types@4.20250628.0)(react@19.1.0) + hono: + specifier: ^4.8.3 + version: 4.8.3 typescript: specifier: ^5.8.3 version: 5.8.3 @@ -155,6 +161,9 @@ packages: cpu: [x64] os: [win32] + '@cloudflare/workers-oauth-provider@0.0.5': + resolution: {integrity: sha512-t1x5KAzsubCvb4APnJ93z407X1x7SGj/ga5ziRnwIb/iLy4PMkT/hgd1y5z7Bbsdy5Fy6mywhCP4lym24bX66w==} + '@cloudflare/workers-types@4.20250628.0': resolution: {integrity: sha512-V4HthfhtQU2vTpwLeUic8FTLgGSjglsGZMJc9jKBNYEU/k0A1rE55UgQoTb5blKQdGtpQKfVKs3FROeY/lXmbw==} @@ -692,6 +701,10 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hono@4.8.3: + resolution: {integrity: sha512-jYZ6ZtfWjzBdh8H/0CIFfCBHaFL75k+KMzaM177hrWWm2TWL39YMYaJgB74uK/niRc866NMlH9B8uCvIo284WQ==} + engines: {node: '>=16.9.0'} + http-errors@2.0.0: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} @@ -1125,6 +1138,10 @@ snapshots: '@cloudflare/workerd-windows-64@1.20250617.0': optional: true + '@cloudflare/workers-oauth-provider@0.0.5': + dependencies: + '@cloudflare/workers-types': 4.20250628.0 + '@cloudflare/workers-types@4.20250628.0': {} '@cspotcode/source-map-support@0.8.1': @@ -1607,6 +1624,8 @@ snapshots: dependencies: function-bind: 1.1.2 + hono@4.8.3: {} + http-errors@2.0.0: dependencies: depd: 2.0.0 diff --git a/examples/servers/cf-agents/src/index.ts b/examples/servers/cf-agents/src/index.ts index 4a0dee6..417e3b9 100644 --- a/examples/servers/cf-agents/src/index.ts +++ b/examples/servers/cf-agents/src/index.ts @@ -1,6 +1,8 @@ import { McpAgent } from 'agents/mcp' import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' import { z } from 'zod' +import OAuthProvider, { OAuthHelpers } from '@cloudflare/workers-oauth-provider' +import { Hono } from 'hono' // Define our MCP agent with tools export class MyMCP extends McpAgent { @@ -54,6 +56,31 @@ export class MyMCP extends McpAgent { } } +export type Bindings = Env & { + OAUTH_PROVIDER: OAuthHelpers +} + +const app = new Hono<{ + Bindings: Bindings +}>() + +app.get('/authorize', async (c) => { + const oauthReqInfo = await c.env.OAUTH_PROVIDER.parseAuthRequest(c.req.raw) + const email = 'example@dotcom.com' + const { redirectTo } = await c.env.OAUTH_PROVIDER.completeAuthorization({ + request: oauthReqInfo, + userId: email, + metadata: { + label: 'Test User', + }, + scope: oauthReqInfo.scope, + props: { + userEmail: email, + }, + }) + return Response.redirect(redirectTo) +}) + export default { fetch(request: Request, env: Env, ctx: ExecutionContext) { const url = new URL(request.url) @@ -65,21 +92,23 @@ export default { if (url.pathname === '/public/mcp') { return MyMCP.serve('/public/mcp').fetch(request, env, ctx) } - // - // if (url.pathname.startsWith('/authed')) { - // return new OAuthProvider({ - // apiRoute: "/authed/mcp", - // // TODO: fix these types - // // @ts-expect-error - // apiHandler: MyMCP.mount("/sse"), - // // @ts-expect-error - // defaultHandler: app, - // authorizeEndpoint: "/authorize", - // tokenEndpoint: "/token", - // clientRegistrationEndpoint: "/register", - // }).fetch(request, env, ctx) - // } - return new Response('Not found', { status: 404 }) + return new OAuthProvider({ + apiRoute: ['/sse', '/mcp'], + apiHandler: { + // @ts-ignore + fetch: (request, env, ctx) => { + const { pathname } = new URL(request.url) + if (pathname.startsWith('/sse')) return MyMCP.serveSSE('/sse').fetch(request as any, env, ctx) + if (pathname === '/mcp') return MyMCP.serve('/mcp').fetch(request as any, env, ctx) + return new Response('Not found', { status: 404 }) + }, + }, + // @ts-ignore + defaultHandler: app, + authorizeEndpoint: '/authorize', + tokenEndpoint: '/token', + clientRegistrationEndpoint: '/register', + }).fetch(request, env, ctx) }, } diff --git a/examples/servers/cf-agents/worker-configuration.d.ts b/examples/servers/cf-agents/worker-configuration.d.ts index af3aa8d..3c82704 100644 --- a/examples/servers/cf-agents/worker-configuration.d.ts +++ b/examples/servers/cf-agents/worker-configuration.d.ts @@ -1,8 +1,9 @@ /* eslint-disable */ -// Generated by Wrangler by running `wrangler types` (hash: 15dd420dfccac8afafeb181790b9eddb) +// Generated by Wrangler by running `wrangler types` (hash: 71a1247d25eb5ef8b66311103d5f9180) // Runtime types generated with workerd@1.20250617.0 2025-03-10 nodejs_compat declare namespace Cloudflare { interface Env { + OAUTH_KV: KVNamespace MCP_OBJECT: DurableObjectNamespace } } diff --git a/examples/servers/cf-agents/wrangler.jsonc b/examples/servers/cf-agents/wrangler.jsonc index bc9a194..5528deb 100644 --- a/examples/servers/cf-agents/wrangler.jsonc +++ b/examples/servers/cf-agents/wrangler.jsonc @@ -24,7 +24,13 @@ }, "observability": { "enabled": true - } + }, + "kv_namespaces": [ + { + "binding": "OAUTH_KV", + "id": "" + } + ] /** * Smart Placement * Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement diff --git a/test/integration/mcp-connection.test.ts b/test/integration/mcp-connection.test.ts index 4c46eb0..bdbff27 100644 --- a/test/integration/mcp-connection.test.ts +++ b/test/integration/mcp-connection.test.ts @@ -38,6 +38,16 @@ function getMCPServers() { url: `http://localhost:${state.cfAgentsPort}/public/sse`, expectedTools: 1, // Minimum expected tools count }, + { + name: 'cf-agents-auth', + url: `http://localhost:${state.cfAgentsPort}/mcp`, + expectedTools: 1, // Minimum expected tools count + }, + { + name: 'cf-agents-auth-sse', + url: `http://localhost:${state.cfAgentsPort}/sse`, + expectedTools: 1, // Minimum expected tools count + }, ] } catch (error) { throw new Error(`Test environment not properly initialized: ${error}`) @@ -216,23 +226,25 @@ describe('MCP Connection Integration Tests', () => { }) const testScenarios = [ - // Working examples with auto transport (should pass) + // Hono examples (MCP only) { serverName: 'hono-mcp', transportType: 'auto' as const }, - { serverName: 'cf-agents', transportType: 'auto' as const }, - - // SSE endpoint with SSE transport (should pass) - { serverName: 'cf-agents-sse', transportType: 'sse' as const }, - - // Additional test cases for HTTP transport { serverName: 'hono-mcp', transportType: 'http' as const }, - { serverName: 'cf-agents', transportType: 'http' as const }, - // Failing case: SSE endpoint with auto transport (should fail) + // Agents, no auth + { serverName: 'cf-agents', transportType: 'auto' as const }, + { serverName: 'cf-agents', transportType: 'http' as const }, + { serverName: 'cf-agents-sse', transportType: 'sse' as const }, { serverName: 'cf-agents-sse', transportType: 'auto' as const }, + + // Agents, with auth + { serverName: 'cf-agents-auth', transportType: 'auto' as const }, + { serverName: 'cf-agents-auth', transportType: 'http' as const }, + { serverName: 'cf-agents-auth-sse', transportType: 'sse' as const }, + { serverName: 'cf-agents-auth-sse', transportType: 'auto' as const }, ] test.each(testScenarios)( - 'should connect to $serverName with $transportType transport (expect: $shouldPass)', + 'should connect to $serverName with $transportType transport', async ({ serverName, transportType }) => { const servers = getMCPServers() const server = servers.find((s) => s.name === serverName)