跳转到内容
搜索文档

构建搜索并执行 MCP 服务器

最后更新 查看 MarkdownAgent 设置

使用 openApiMcpServer() 通过两个 Model Context Protocol (MCP) tool 发布大型 OpenAPI 服务:

  • search 针对 OpenAPI 文档运行模型编写的代码。
  • execute 添加宿主提供的 codemode.request() 函数。

OpenAPI 文档保持在模型上下文之外,除非搜索代码返回其中一部分。认证保留在宿主 Worker。

前提条件

需要 Cloudflare Workers 项目、OpenAPI 3.x 文档,以及宿主侧认证 API 请求的方法。

发布服务

  1. 安装 Code Mode 与 MCP 依赖:

    npm i @cloudflare/codemode agents @modelcontextprotocol/sdk zod
  2. 添加 Worker Loader 绑定与 nodejs_compat 兼容性标志:

    {
      "$schema": "./node_modules/wrangler/config-schema.json",
      "name": "openapi-codemode-mcp",
      "main": "src/server.ts",
      // Set this to today's date
      "compatibility_date": "2026-08-17",
      "compatibility_flags": [
        "nodejs_compat"
      ],
      "worker_loaders": [
        {
          "binding": "LOADER"
        }
      ]
    }
    name = "openapi-codemode-mcp"
    main = "src/server.ts"
    # Set this to today's date
    compatibility_date = "2026-08-17"
    compatibility_flags = ["nodejs_compat"]
    
    [[worker_loaders]]
    binding = "LOADER"
  3. 在 host 加载 OpenAPI 文档。用已认证的 request 函数创建 MCP 服务器:

    src/server.jsjs
    import { DynamicWorkerExecutor } from "@cloudflare/codemode";
    import { openApiMcpServer } from "@cloudflare/codemode/mcp";
    import { createMcpHandler } from "agents/mcp";
    
    const SPEC_URL = "https://api.example.com/openapi.json";
    const API_ORIGIN = "https://api.example.com";
    
    let specCache;
    
    async function loadSpec() {
    	if (specCache) return specCache;
    
    	const response = await fetch(SPEC_URL);
    	if (!response.ok) {
    		throw new Error(`OpenAPI request failed: ${response.status}`);
    	}
    
    	specCache = await response.json();
    	return specCache;
    }
    
    export default {
    	async fetch(request, env, ctx) {
    		const authorization = request.headers.get("Authorization");
    		if (!authorization?.startsWith("Bearer ")) {
    			return new Response("Bearer token required", { status: 401 });
    		}
    
    		const server = openApiMcpServer({
    			spec: await loadSpec(),
    			executor: new DynamicWorkerExecutor({ loader: env.LOADER }),
    			name: "example-api",
    			version: "1.0.0",
    			request: async (options) => {
    				if (!options.path.startsWith("/")) {
    					throw new Error("API path must start with a slash");
    				}
    
    				const url = new URL(`${API_ORIGIN}${options.path}`);
    				for (const [key, value] of Object.entries(options.query ?? {})) {
    					if (value !== undefined) {
    						url.searchParams.set(key, String(value));
    					}
    				}
    
    				const headers = { Authorization: authorization };
    				if (options.contentType) {
    					headers["Content-Type"] = options.contentType;
    				} else if (options.body !== undefined) {
    					headers["Content-Type"] = "application/json";
    				}
    
    				const response = await fetch(url, {
    					method: options.method,
    					headers,
    					body:
    						options.body === undefined
    							? undefined
    							: options.rawBody
    								? options.body
    								: JSON.stringify(options.body),
    				});
    
    				if (response.status === 204) return null;
    
    				const responseType = response.headers.get("Content-Type") ?? "";
    				const result = responseType.includes("application/json")
    					? await response.json()
    					: await response.text();
    
    				if (!response.ok) {
    					throw new Error(`API request failed: ${response.status}`);
    				}
    				return result;
    			},
    		});
    
    		return createMcpHandler(server, { route: "/mcp" })(request, env, ctx);
    	},
    };
    src/server.tsts
    import { DynamicWorkerExecutor } from "@cloudflare/codemode";
    import { openApiMcpServer } from "@cloudflare/codemode/mcp";
    import { createMcpHandler } from "agents/mcp";
    
    const SPEC_URL = "https://api.example.com/openapi.json";
    const API_ORIGIN = "https://api.example.com";
    
    let specCache: Record<string, unknown> | undefined;
    
    async function loadSpec(): Promise<Record<string, unknown>> {
    	if (specCache) return specCache;
    
    	const response = await fetch(SPEC_URL);
    	if (!response.ok) {
    		throw new Error(`OpenAPI request failed: ${response.status}`);
    	}
    
    	specCache = (await response.json()) as Record<string, unknown>;
    	return specCache;
    }
    
    export default {
    	async fetch(request, env, ctx): Promise<Response> {
    		const authorization = request.headers.get("Authorization");
    		if (!authorization?.startsWith("Bearer ")) {
    			return new Response("Bearer token required", { status: 401 });
    		}
    
    		const server = openApiMcpServer({
    			spec: await loadSpec(),
    			executor: new DynamicWorkerExecutor({ loader: env.LOADER }),
    			name: "example-api",
    			version: "1.0.0",
    			request: async (options) => {
    				if (!options.path.startsWith("/")) {
    					throw new Error("API path must start with a slash");
    				}
    
    				const url = new URL(`${API_ORIGIN}${options.path}`);
    				for (const [key, value] of Object.entries(options.query ?? {})) {
    					if (value !== undefined) {
    						url.searchParams.set(key, String(value));
    					}
    				}
    
    				const headers: Record<string, string> = { Authorization: authorization };
    				if (options.contentType) {
    					headers["Content-Type"] = options.contentType;
    				} else if (options.body !== undefined) {
    					headers["Content-Type"] = "application/json";
    				}
    
    				const response = await fetch(url, {
    					method: options.method,
    					headers,
    					body:
    						options.body === undefined
    							? undefined
    							: options.rawBody
    								? (options.body as string)
    								: JSON.stringify(options.body),
    				});
    
    				if (response.status === 204) return null;
    
    				const responseType = response.headers.get("Content-Type") ?? "";
    				const result = responseType.includes("application/json")
    					? await response.json()
    					: await response.text();
    
    				if (!response.ok) {
    					throw new Error(`API request failed: ${response.status}`);
    				}
    				return result;
    			},
    		});
    
    		return createMcpHandler(server, { route: "/mcp" })(
    			request,
    			env,
    			ctx,
    		);
    	},
    } satisfies ExportedHandler<Env>;
  4. 部署 Worker:

    npx wrangler deploy
  5. 在 MCP client 中连接到 https://<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev/mcp。包含 Worker 所需的 bearer token。

  6. 列出 MCP tool。验证服务器暴露 searchexecute

搜索 OpenAPI 文档

execute 之前调用 search。Search 代码可检查文档而不发起 API 请求:

async () => {
	const spec = await codemode.spec();
	return Object.entries(spec.paths)
		.filter(([path]) => path.includes("/orders"))
		.map(([path, operations]) => ({
			path,
			methods: Object.keys(operations),
		}));
};

代码调用 codemode.spec() 时,本地 OpenAPI $ref 在沙箱内解析。外部引用保持未解析。

调用 API

execute tool 包含相同的 codemode.spec() 方法与 host 提供的 codemode.request() 方法:

async () => {
	const response = await codemode.request({
		method: "GET",
		path: "/orders",
		query: { status: "processing", limit: 20 },
	});

	return response.items.map(({ id, status }) => ({ id, status }));
};

Host 回调接收 methodpath、可选 query、可选 body、可选 contentType 与可选 rawBody 字段。精确类型请参阅 openApiMcpServer() API

searchexecute tool 使用固定示例 snippet。可选 description 追加到 execute tool 描述。此函数不使用 codeMcpServer() 支持的 {{types}}{{example}} 占位符。

保护 API

示例在创建 MCP 服务器前读取 bearer token。其 request 回调将该 token 加入出站请求。Token 永不进入沙箱。

openApiMcpServer() 不为 execute 内每次请求提供持久审批。在应用副作用前于宿主回调强制执行授权及任何所需的按操作审批。验证路径而非接受任意来源。

不要在 OpenAPI 文档或 API 结果中包含 secret。两者对模型编写的代码可用。

DynamicWorkerExecutor 默认阻止直接外部 fetch()connect()。生成的代码仅通过 host request 回调访问服务。

限制结果

让模型编写的代码在返回前 select、map、aggregate 或 paginate 数据。发布者将最终 MCP 响应限制在约 6,000 估计 token,截断响应标记 --- TRUNCATED ---

截断不减少已执行的 API 工作。返回支持模型下一决策的聚焦标识符、状态字段、计数与错误。

这篇文档对您有帮助吗?