diff --git a/.changeset/cold-teeth-repeat.md b/.changeset/cold-teeth-repeat.md new file mode 100644 index 00000000..aa63785a --- /dev/null +++ b/.changeset/cold-teeth-repeat.md @@ -0,0 +1,5 @@ +--- +'docs-vectorize': minor +--- + +Moved Docs MCP server to be stateless for /mcp diff --git a/apps/docs-vectorize/src/docs-vectorize.app.ts b/apps/docs-vectorize/src/docs-vectorize.app.ts index 2de9e84f..7887391a 100644 --- a/apps/docs-vectorize/src/docs-vectorize.app.ts +++ b/apps/docs-vectorize/src/docs-vectorize.app.ts @@ -1,6 +1,5 @@ -import { McpAgent } from 'agents/mcp' +import { createMcpHandler, McpAgent } from 'agents/mcp' -import { createApiHandler } from '@repo/mcp-common/src/api-handler' import { getEnv } from '@repo/mcp-common/src/env' import { registerPrompts } from '@repo/mcp-common/src/prompts/docs-vectorize.prompts' import { initSentry } from '@repo/mcp-common/src/sentry' @@ -11,12 +10,7 @@ import type { Env } from './docs-vectorize.context' const env = getEnv() -// The docs MCP server isn't stateful, so we don't have state/props -export type Props = never - -export type State = never - -export class CloudflareDocumentationMCP extends McpAgent { +export class CloudflareDocumentationMCP extends McpAgent { _server: CloudflareMCPServer | undefined set server(server: CloudflareMCPServer) { this._server = server @@ -25,7 +19,6 @@ export class CloudflareDocumentationMCP extends McpAgent { if (!this._server) { throw new Error('Tried to access server before it was initialized') } - return this._server } @@ -37,20 +30,47 @@ export class CloudflareDocumentationMCP extends McpAgent { } async init() { - const sentry = initSentry(env, this.ctx) - - this.server = new CloudflareMCPServer({ - wae: env.MCP_METRICS, - serverInfo: { - name: env.MCP_SERVER_NAME, - version: env.MCP_SERVER_VERSION, - }, - sentry, - }) - - registerDocsTools(this, this.env) - registerPrompts(this) + this.server = createMcpServer(env, this.ctx) } } -export default createApiHandler(CloudflareDocumentationMCP) +const sseHandler = CloudflareDocumentationMCP.serveSSE('/sse') + +export default { + fetch: async (req: Request, env: Env, ctx: ExecutionContext) => { + const url = new URL(req.url) + if (url.pathname === '/sse' || url.pathname === '/sse/message') { + return sseHandler.fetch(req, env, ctx) + } + if (url.pathname === '/mcp') { + const server = createMcpServer(env, ctx, req) + const mcpHandler = createMcpHandler(server) + return mcpHandler(req, env, ctx) + } + return new Response('Not found', { status: 404 }) + }, +} + +function createMcpServer( + env: Env, + ctx: { + waitUntil: ExecutionContext['waitUntil'] + }, + req?: Request +) { + const sentry = initSentry(env, ctx, req) + + const server = new CloudflareMCPServer({ + wae: env.MCP_METRICS, + serverInfo: { + name: env.MCP_SERVER_NAME, + version: env.MCP_SERVER_VERSION, + }, + sentry, + }) + + registerDocsTools(server, env) + registerPrompts(server) + + return server +} diff --git a/apps/docs-vectorize/wrangler.jsonc b/apps/docs-vectorize/wrangler.jsonc index 8a131fd7..7177efb2 100644 --- a/apps/docs-vectorize/wrangler.jsonc +++ b/apps/docs-vectorize/wrangler.jsonc @@ -8,6 +8,7 @@ "compatibility_date": "2025-03-10", "compatibility_flags": ["nodejs_compat"], "name": "mcp-cloudflare-docs-vectorize-dev", + "minify": true, "migrations": [ { "new_sqlite_classes": ["CloudflareDocumentationMCP"], diff --git a/apps/workers-bindings/src/bindings.app.ts b/apps/workers-bindings/src/bindings.app.ts index 9fc69862..67469104 100644 --- a/apps/workers-bindings/src/bindings.app.ts +++ b/apps/workers-bindings/src/bindings.app.ts @@ -83,8 +83,8 @@ export class WorkersBindingsMCP extends McpAgent { registerObservabilityTools(this) // Add docs tools - registerDocsTools(this, this.env) - registerPrompts(this) + registerDocsTools(this.server, this.env) + registerPrompts(this.server) } async getActiveAccountId() { diff --git a/packages/mcp-common/src/prompts/docs-vectorize.prompts.ts b/packages/mcp-common/src/prompts/docs-vectorize.prompts.ts index d8ff45d3..594e071b 100644 --- a/packages/mcp-common/src/prompts/docs-vectorize.prompts.ts +++ b/packages/mcp-common/src/prompts/docs-vectorize.prompts.ts @@ -1,11 +1,11 @@ -import type { CloudflareMcpAgentNoAccount } from '../types/cloudflare-mcp-agent.types' +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' /** * Registers developer-platform-related prompts with the MCP server - * @param agent The MCP server instance + * @param server The MCP server instance */ -export function registerPrompts(agent: CloudflareMcpAgentNoAccount) { - agent.server.prompt( +export function registerPrompts(server: McpServer) { + server.prompt( 'workers-prompt-full', 'Detailed prompt for generating Cloudflare Workers code (and other developer platform products) from https://developers.cloudflare.com/workers/prompt.txt', async () => ({ diff --git a/packages/mcp-common/src/tools/docs-vectorize.tools.ts b/packages/mcp-common/src/tools/docs-vectorize.tools.ts index 0395a777..1084be46 100644 --- a/packages/mcp-common/src/tools/docs-vectorize.tools.ts +++ b/packages/mcp-common/src/tools/docs-vectorize.tools.ts @@ -1,6 +1,6 @@ import { z } from 'zod' -import type { CloudflareMcpAgentNoAccount } from '../types/cloudflare-mcp-agent.types' +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' interface RequiredEnv { AI: Ai @@ -12,10 +12,10 @@ const TOP_K = 10 /** * Registers the docs search tool with the MCP server - * @param agent The MCP server instance + * @param server The MCP server instance */ -export function registerDocsTools(agent: CloudflareMcpAgentNoAccount, env: RequiredEnv) { - agent.server.tool( +export function registerDocsTools(server: McpServer, env: RequiredEnv) { + server.tool( 'search_cloudflare_documentation', `Search the Cloudflare documentation. @@ -57,7 +57,7 @@ ${result.text} // Note: this is a tool instead of a prompt because // prompt support is much less common than tools. - agent.server.tool( + server.tool( 'migrate_pages_to_workers_guide', `ALWAYS read this guide before migrating Pages projects to Workers.`, {},