Adding cf/agents tests (#11)

* Cloning demo repo

* added test for agents/mcp

---------

Co-authored-by: Glen Maddern <glen@glenmaddern.com>
This commit is contained in:
Glen Maddern 2025-06-30 21:51:49 +10:00 • committed by GitHub
commit 7f019fed7f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 10420 additions and 10 deletions

View file

@ -33,9 +33,9 @@ jobs:
run: pnpm install run: pnpm install
working-directory: examples/inspector working-directory: examples/inspector
# - name: Install dependencies - cf-agents example - name: Install dependencies - cf-agents example
# run: pnpm install run: pnpm install
# working-directory: examples/servers/cf-agents working-directory: examples/servers/cf-agents
- name: Install dependencies - hono-mcp example - name: Install dependencies - hono-mcp example
run: pnpm install run: pnpm install

172
examples/servers/cf-agents/.gitignore vendored Normal file
View file

@ -0,0 +1,172 @@
# Logs
logs
_.log
npm-debug.log_
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Runtime data
pids
_.pid
_.seed
\*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
\*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
\*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
\*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.\*
# wrangler project
.dev.vars
.wrangler/

View file

@ -0,0 +1,50 @@
# Building a Remote MCP Server on Cloudflare (Without Auth)
This example allows you to deploy a remote MCP server that doesn't require authentication on Cloudflare Workers.
## Get started:
[![Deploy to Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/ai/tree/main/demos/remote-mcp-authless)
This will deploy your MCP server to a URL like: `remote-mcp-server-authless.<your-account>.workers.dev/sse`
Alternatively, you can use the command line below to get the remote MCP Server created on your local machine:
```bash
npm create cloudflare@latest -- my-mcp-server --template=cloudflare/ai/demos/remote-mcp-authless
```
## Customizing your MCP Server
To add your own [tools](https://developers.cloudflare.com/agents/model-context-protocol/tools/) to the MCP server, define each tool inside the `init()` method of `src/index.ts` using `this.server.tool(...)`.
## Connect to Cloudflare AI Playground
You can connect to your MCP server from the Cloudflare AI Playground, which is a remote MCP client:
1. Go to https://playground.ai.cloudflare.com/
2. Enter your deployed MCP server URL (`remote-mcp-server-authless.<your-account>.workers.dev/sse`)
3. You can now use your MCP tools directly from the playground!
## Connect Claude Desktop to your MCP server
You can also connect to your remote MCP server from local MCP clients, by using the [mcp-remote proxy](https://www.npmjs.com/package/mcp-remote).
To connect to your MCP server from Claude Desktop, follow [Anthropic's Quickstart](https://modelcontextprotocol.io/quickstart/user) and within Claude Desktop go to Settings > Developer > Edit Config.
Update with this configuration:
```json
{
"mcpServers": {
"calculator": {
"command": "npx",
"args": [
"mcp-remote",
"http://localhost:8787/sse" // or remote-mcp-server-authless.your-account.workers.dev/sse
]
}
}
}
```
Restart Claude and you should see the tools become available.

View file

@ -0,0 +1,49 @@
{
"$schema": "https://biomejs.dev/schemas/2.0.4/schema.json",
"assist": {
"actions": {
"source": {
"useSortedKeys": "on"
}
},
"enabled": true
},
"files": {
"includes": ["!worker-configuration.d.ts"]
},
"formatter": {
"enabled": true,
"indentWidth": 4,
"lineWidth": 100
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"style": {
"noInferrableTypes": "error",
"noNonNullAssertion": "off",
"noParameterAssign": "error",
"noUnusedTemplateLiteral": "error",
"noUselessElse": "error",
"useAsConstAssertion": "error",
"useDefaultParameterLast": "error",
"useEnumInitializers": "error",
"useNumberNamespace": "error",
"useSelfClosingElements": "error",
"useSingleVarDeclarator": "error"
},
"suspicious": {
"noConfusingVoidType": "off",
"noDebugger": "off",
"noExplicitAny": "off"
}
}
},
"root": false,
"vcs": {
"clientKind": "git",
"enabled": true,
"useIgnoreFile": true
}
}

View file

@ -0,0 +1,25 @@
{
"name": "remote-mcp-server-authless",
"version": "0.0.0",
"private": true,
"scripts": {
"deploy": "wrangler deploy",
"dev": "wrangler dev",
"format": "biome format --write",
"lint:fix": "biome lint --fix",
"start": "wrangler dev",
"cf-typegen": "wrangler types",
"type-check": "tsc --noEmit"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.13.0",
"zod": "^3.25.67"
},
"devDependencies": {
"@biomejs/biome": "^2.0.4",
"@types/node": "^24.0.7",
"agents": "^0.0.100",
"typescript": "^5.8.3",
"wrangler": "^4.22.0"
}
}

1959
examples/servers/cf-agents/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,71 @@
import { McpAgent } from 'agents/mcp'
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { z } from 'zod'
// Define our MCP agent with tools
export class MyMCP extends McpAgent {
server = new McpServer({
name: 'Authless Calculator',
version: '1.0.0',
})
async init() {
// Simple addition tool
this.server.tool('add', { a: z.number(), b: z.number() }, async ({ a, b }) => ({
content: [{ type: 'text', text: String(a + b) }],
}))
// Calculator tool with multiple operations
this.server.tool(
'calculate',
{
operation: z.enum(['add', 'subtract', 'multiply', 'divide']),
a: z.number(),
b: z.number(),
},
async ({ operation, a, b }) => {
let result: number
switch (operation) {
case 'add':
result = a + b
break
case 'subtract':
result = a - b
break
case 'multiply':
result = a * b
break
case 'divide':
if (b === 0)
return {
content: [
{
type: 'text',
text: 'Error: Cannot divide by zero',
},
],
}
result = a / b
break
}
return { content: [{ type: 'text', text: String(result) }] }
},
)
}
}
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 === '/mcp') {
return MyMCP.serve('/mcp').fetch(request, env, ctx)
}
return new Response('Not found', { status: 404 })
},
}

View file

@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "es2021",
"lib": ["es2021"],
"jsx": "react-jsx",
"module": "es2022",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"allowJs": true,
"checkJs": false,
"noEmit": true,
"isolatedModules": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"types": ["./worker-configuration.d.ts", "node"]
},
"include": ["worker-configuration.d.ts", "src/**/*.ts"]
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,62 @@
/**
* For more details on how to configure Wrangler, refer to:
* https://developers.cloudflare.com/workers/wrangler/configuration/
*/
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "cf-agents",
"main": "src/index.ts",
"compatibility_date": "2025-03-10",
"compatibility_flags": ["nodejs_compat"],
"migrations": [
{
"new_sqlite_classes": ["MyMCP"],
"tag": "v1"
}
],
"durable_objects": {
"bindings": [
{
"class_name": "MyMCP",
"name": "MCP_OBJECT"
}
]
},
"observability": {
"enabled": true
}
/**
* Smart Placement
* Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement
*/
// "placement": { "mode": "smart" },
/**
* Bindings
* Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform, including
* databases, object storage, AI inference, real-time communication and more.
* https://developers.cloudflare.com/workers/runtime-apis/bindings/
*/
/**
* Environment Variables
* https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables
*/
// "vars": { "MY_VARIABLE": "production_value" },
/**
* Note: Use secrets to store sensitive data.
* https://developers.cloudflare.com/workers/configuration/secrets/
*/
/**
* Static Assets
* https://developers.cloudflare.com/workers/static-assets/binding/
*/
// "assets": { "directory": "./public/", "binding": "ASSETS" },
/**
* Service Bindings (communicate between multiple Workers)
* https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings
*/
// "services": [{ "binding": "MY_SERVICE", "service": "my-service" }]
}

View file

@ -18,18 +18,21 @@ function getMCPServers() {
throw new Error('hono-mcp port not found in test state') throw new Error('hono-mcp port not found in test state')
} }
if (!state.cfAgentsPort) {
throw new Error('cf-agents port not found in test state')
}
return [ return [
{ {
name: 'hono-mcp', name: 'hono-mcp',
url: `http://localhost:${state.honoPort}/mcp`, url: `http://localhost:${state.honoPort}/mcp`,
expectedTools: 1, // Minimum expected tools count expectedTools: 1, // Minimum expected tools count
}, },
// Add more servers here as they become available {
// { name: 'cf-agents',
// name: 'another-server', url: `http://localhost:${state.cfAgentsPort}/mcp`,
// url: 'http://localhost:9902/mcp', expectedTools: 1, // Minimum expected tools count
// expectedTools: 1, },
// },
] ]
} catch (error) { } catch (error) {
throw new Error(`Test environment not properly initialized: ${error}`) throw new Error(`Test environment not properly initialized: ${error}`)
@ -161,6 +164,15 @@ describe('MCP Connection Integration Tests', () => {
// Ignore errors - process might already be dead // Ignore errors - process might already be dead
} }
try {
if (state?.cfAgentsServer && !state.cfAgentsServer.killed) {
console.log('🔥 Force cleanup cf-agents server...')
state.cfAgentsServer.kill('SIGKILL')
}
} catch (e) {
// Ignore errors - process might already be dead
}
try { try {
if (state?.staticServer) { if (state?.staticServer) {
state.staticServer.close() state.staticServer.close()

View file

@ -14,9 +14,11 @@ const testStateFile = join(cacheDir, 'test-state.json')
interface GlobalState { interface GlobalState {
honoServer?: ChildProcess honoServer?: ChildProcess
cfAgentsServer?: ChildProcess
staticServer?: Server staticServer?: Server
staticPort?: number staticPort?: number
honoPort?: number honoPort?: number
cfAgentsPort?: number
processGroupId?: number processGroupId?: number
allChildProcesses?: Set<number> allChildProcesses?: Set<number>
} }
@ -252,7 +254,48 @@ export default async function globalSetup() {
await waitForOutput(honoServer, 'Ready on') await waitForOutput(honoServer, 'Ready on')
state.honoServer = honoServer state.honoServer = honoServer
// Step 4: Start simple static file server for inspector // Step 4: Find available port and start cf-agents server
console.log('🔍 Finding available port starting from 9902...')
const cfAgentsPort = await findAvailablePortFromBase(9902)
console.log(`📍 Using port ${cfAgentsPort} for cf-agents server`)
console.log('🚀 Starting cf-agents server...')
const cfAgentsDir = join(rootDir, 'examples/servers/cf-agents')
const cfAgentsServer = spawn('pnpm', ['dev', `--port=${cfAgentsPort}`], {
cwd: cfAgentsDir,
stdio: ['ignore', 'pipe', 'pipe'],
shell: true,
detached: false,
})
// Track all child processes
if (cfAgentsServer.pid) {
state.allChildProcesses!.add(cfAgentsServer.pid)
}
state.cfAgentsPort = cfAgentsPort
state.cfAgentsServer = cfAgentsServer
cfAgentsServer.stdout?.on('data', (data) => {
console.log(`[cf-agents] ${data.toString()}`)
})
cfAgentsServer.stderr?.on('data', (data) => {
console.log(`[cf-agents] ${data.toString()}`)
})
// Track when the process exits to remove it from our tracking
cfAgentsServer.on('exit', () => {
if (cfAgentsServer.pid) {
state.allChildProcesses!.delete(cfAgentsServer.pid)
}
})
// Wait for cf-agents server to be ready
await waitForOutput(cfAgentsServer, 'Ready on')
state.cfAgentsServer = cfAgentsServer
// Step 5: Start simple static file server for inspector
console.log('🌐 Starting static file server for inspector...') console.log('🌐 Starting static file server for inspector...')
const inspectorDistDir = join(inspectorDir, 'dist') const inspectorDistDir = join(inspectorDir, 'dist')
const staticPort = await findAvailablePort(8000) const staticPort = await findAvailablePort(8000)
@ -315,6 +358,7 @@ export default async function globalSetup() {
JSON.stringify( JSON.stringify(
{ {
honoPort: state.honoPort, honoPort: state.honoPort,
cfAgentsPort: state.cfAgentsPort,
staticPort: state.staticPort, staticPort: state.staticPort,
}, },
null, null,
@ -330,6 +374,9 @@ export default async function globalSetup() {
if (state.honoServer) { if (state.honoServer) {
state.honoServer.kill() state.honoServer.kill()
} }
if (state.cfAgentsServer) {
state.cfAgentsServer.kill()
}
if (state.staticServer) { if (state.staticServer) {
state.staticServer.close() state.staticServer.close()
} }