Skip to content

feat(mcp): generate an MCP server in TypeScript (--target mcp) - #71

Open
mattzcarey wants to merge 9 commits into
mainfrom
mcp
Open

mattzcarey wants to merge 9 commits into
mainfrom
mcp

Conversation

@mattzcarey

@mattzcarey mattzcarey commented Oct 27, 2026 •

Copy link
Copy Markdown
Member

forge openapi.json --target mcp --out tools generates the tools of an MCP server in TypeScript: one tool per API method, as data a server lists and calls. Each tool has its argument schema, hints, extensions and the JSON Schemas of its responses, plus a request function that builds its HTTP request. rfcs/mcp.md describes how the target maps the API onto MCP.

import { buildRequest, listTools, toolsByName, tools } from './tools/index.js'

const listing = listTools(tools) // tools/list

const request = buildRequest(toolsByName(tools).get(name), args, baseUrl) // tools/call
const response = await fetch(request.url, { ...request, headers: { ...request.headers, authorization } })

It generates no server. Every server around the tools differs: how it authenticates, what it serves alongside them, what it does with an API error. So the runtime is one file, _/tool.ts, with only what every server needs:

  • listTools: the listing tools/list sends, built once, with output schemas only when asked
  • toolsByName: the index for tools/call
  • buildRequest: the request as plain data, { method, url, headers, body? }, which crosses RPC as it is and fetch sends

When I closed #53 the plan was to run this in production before proposing it. It now serves mcp.cloudflare.com/mcp?codemode=false&experimentalForge=true, with about 3,000 tools regenerated every six hours. Our server applies its own policy to the tools, plans calls in a separate Worker and sends them with the session's token. It checks responses against the generated response schemas so teams can fix the spec before tools advertise output schemas (cloudflare/mcp#350 moves it to this version).

One question for review: methods with x-forge-hidden get a tool. Forge says hidden methods are left out of listings, so tools/list should probably skip them too.

Tests ran, and cloudflare/mcp's suite passes on code generated by this branch.

`forge openapi.json --target mcp --out server` generates TypeScript for the
official MCP TypeScript SDK (@modelcontextprotocol/server):

- <group>/tools.ts: the tools of the group's methods, each with the JSON
  Schema of its arguments, its hints, the method's extensions, and a
  request function that builds its API request
- <group>/schemas.ts: a constant for each model's schema, shared by every
  tool whose arguments use it
- index.ts: every tool, and the API's base URL and version
- README.md: the methods without a tool, and why
- _/: the runtime, which serves the tools with low-level tools/list and
  tools/call handlers and sends their requests

Tools are named after their groups and method in snake_case. Hidden methods
get tools, since a client can only call the tools it lists; deprecated ones
don't. Methods whose required body isn't JSON, or whose tool name is taken
or too long, are listed in README.md instead.
The generated files import the tool type and pathParam from _/tool.ts, and
index.ts re-exports only that half of the runtime, so a server can import
the tools, for example lazily or as a module of its own, without pulling in
@modelcontextprotocol/server. _/server.ts still serves them.
Each tool gets responses: the JSON Schema of every response body the method
declares, by status (2XX, error ranges and default), with a $defs of the
models they reach, from shared outputs.ts constants. Response schemas keep
read-only fields and leave out descriptions and defaults: they're for a
server to check responses against, so listTools leaves them out of
tools/list.
Each tool gets an outputSchema: the JSON body schema of its successful
(2xx) responses, or anyOf the distinct ones, with $defs holding only the
models those reach. A tool with a 2xx response that has no JSON body gets
none. listTools leaves it out unless called with { outputSchema: true }.
rfcs/mcp.md says which methods get a tool, how tools and arguments are
named, how a call becomes a request, how schemas are built for arguments,
responses and output, the hints and extensions, and what the runtime does.
Comment thread src/bin/forge/mcp/lib/src/tool.ts Fixed
Only createHandler serves the tools on an McpServer, so serveTools is no
longer exported, and its extra tools, which nothing passed, are gone.
worker.ts default-exports createHandler with every tool. It calls the API
with the MCP request's Authorization header and refuses requests from
another site's Origin, so the generated server deploys as a Worker with
no code of its own.
Every server around the tools differs in how it authenticates and what it
serves alongside them, so the runtime keeps only what every server needs:
the Tool type, listTools, toolsByName and buildRequest, in _/tool.ts.
worker.ts, createHandler, serveTools and callTool are gone, and nothing
imports the MCP SDK at runtime.

buildRequest returns the request as plain data, { method, url, headers,
body? }, which crosses RPC as it is and fetch(request.url, request)
sends.
@pkg-pr-new

pkg-pr-new Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
npm i https://pkg.pr.new/cloudflare/forge/@cloudflare/forge@71

commit: 6de519b

…ession

CodeQL flagged /\/+$/ as polynomial on many slashes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants