Demonstrates how to turn any OpenAPI spec into a pair of MCP tools (search + execute) using openApiMcpServer.
openApiMcpServer takes a raw OpenAPI spec and creates two tools:
search— the LLM queries the spec as a JavaScript object to find endpoints, parameters, and schemasexecute— the LLM calls the API via a host-siderequest()function you provide
Auth tokens and base URLs live in your request() function on the host. The sandbox has no outbound network access and never sees secrets. The callback's second argument is the MCP context for the outer execute tool call. Use it when an elicitation, sampling request, roots request, or notification belongs to that call.
This example connects to the live Cloudflare API using the official OpenAPI spec. Pass a Cloudflare API token via the Authorization header.
npm install
npm startThen connect an MCP client with your Cloudflare API token:
Authorization: Bearer <your-cf-api-token>
The Worker reads the spec from GitHub on first request and caches it for the lifetime of the isolate.
import { openApiMcpServer } from "@cloudflare/codemode/mcp";
const server = openApiMcpServer({
spec,
executor,
request: async (opts, context) => {
// Runs on the host. Put auth, base URL, and headers here.
// The sandbox sees neither the token nor the MCP context.
const url = new URL(`https://api.example.com${opts.path}`);
const res = await fetch(url, {
method: opts.method,
headers: { Authorization: `Bearer ${token}` },
body: opts.body ? JSON.stringify(opts.body) : undefined
});
return res.json();
}
});The second argument is the MCP SDK's request-scoped context. For example, a host callback can elicit confirmation through the same response stream as the outer tool call:
request: async (opts, context) => {
const result = await server.server.elicitInput(
{
message: `Allow ${opts.method} ${opts.path}?`,
requestedSchema: {
type: "object",
properties: { approved: { type: "boolean" } },
required: ["approved"]
}
},
{
relatedRequestId: context.requestId,
signal: context.signal
}
);
if (result.action !== "accept" || !result.content?.approved) {
throw new Error("Request declined");
}
return callApi(opts);
};The context stays in trusted host code and should only be used while the outer tool call is active. Existing callbacks that only accept opts continue to work.
The request() callback runs while the sandbox is still suspended on its
codemode.request() call — that call is a blocking RPC round-trip, so the
executor's timeout covers the whole wait, including the time a human spends
answering the elicitation. The default DynamicWorkerExecutor timeout is
60s, which matches the MCP elicitation timeout (both the SDK's
DEFAULT_REQUEST_TIMEOUT_MSEC and McpAgent.elicitInput default to 60s), so
the default config already lets an elicitation run to completion.
If you lower the executor timeout, a tool that elicits user input (or samples,
or lists roots) will abort with Execution timed out once the sandbox budget
expires, before the user can respond. Keep the executor timeout at least as
long as the elicitation timeout:
const executor = new DynamicWorkerExecutor({
loader: env.LOADER,
timeout: 60_000 // do not drop below the 60s elicitation timeout
});The LLM first searches the spec:
async () => {
const spec = await codemode.spec();
return Object.entries(spec.paths)
.filter(([, item]) => item.get?.tags?.includes("zones"))
.map(([path, item]) => ({ path, summary: item.get?.summary }));
};Then executes calls:
async () => {
return await codemode.request({ method: "GET", path: "/zones" });
};wrangler.jsonc needs a worker_loaders binding for the executor:
- codemode-mcp — wrapping an existing MCP server instead of an OpenAPI spec
@cloudflare/codemodedocs
{ "worker_loaders": [{ "binding": "LOADER" }] }