Skip to content

feat(mcp): generate a Rust MCP server - #49

Draft
dimitropoulos wants to merge 1 commit into
mainfrom
dimitri/mcp-rust
Draft

dimitropoulos wants to merge 1 commit into
mainfrom
dimitri/mcp-rust

Conversation

@dimitropoulos

Copy link
Copy Markdown
Collaborator

This is the first step toward generating MCP servers from OpenAPI documents. It adds an mcp target:

forge openapi.json --target mcp --out server
cd server && cargo build --release

The output is a standalone Rust crate. The server exposes every method as an MCP tool and calls the API over HTTP when a client calls one.

What gets generated

File Contents
Cargo.toml Fixed manifest. It declares an empty [workspace] so the crate builds wherever it's generated.
src/main.rs Fixed runtime, embedded with include_str! from src/bin/forge/mcp/runtime/main.rs. Implements ServerHandler with list_tools and call_tool.
src/tools.rs Generated. Contains BASE_URL, INSTRUCTIONS and TOOLS: &[Tool].

Each tool gets:

  • Name: the group path plus the method name in snake_case, e.g. accounts_members_list. If two methods produce the same name, generation fails.
  • Title and description: the summary and description. Deprecation and the x-forge-require-confirmation sentence are appended to the description.
  • HTTP details: the HTTP method, the path template, and the path and query parameters (keyed by wire name, with explode respected).
  • Body: either flat (the body's fields sit next to the parameters, following Method::request) or a single body argument.
  • Input schema: a JSON Schema built from the IR. Models go in $defs and are referenced with $ref, so recursive models work. Read-only fields are left out.
  • Hints: readOnly, destructive and idempotent, derived from the HTTP method. A method that needs confirmation is always marked destructive. openWorld is always true.

Methods are skipped when:

  • they are hidden;
  • their request body isn't JSON;
  • a success response is neither JSON nor text/*;
  • they require a header parameter;
  • two of their arguments would have the same name.

Runtime configuration

The server speaks MCP over stdio. It reads two environment variables:

  • API_BASE_URL: the base URL, defaulting to the document's first server.
  • API_TOKEN: sent as a bearer token when set.

A non-2xx response is returned as a tool result with isError: true (not a JSON-RPC error), so the model can see the error.

Dependencies

Only the generated crate gets new dependencies. Forge itself gets none: it already has serde_json under cli, which the schema builder uses.

Crate Version Why
rmcp 3.5.1 Official Rust MCP SDK, built with default-features = false and server + transport-io. Macros aren't needed because the tools come from data.
reqwest 0.13.5 HTTP client. Default features, which use rustls.
tokio 1.53.3 Only macros and rt-multi-thread.
serde_json 1.0.154 Tool arguments and schemas.

Edition 2024, rust-version = "1.88" (rmcp's MSRV).

I deliberately left these out:

  • async-trait: rmcp uses native async fns in traits.
  • schemars: the schemas come from the OpenAPI document, not from Rust types.
  • tera/minijinja: the generator writes plain Rust with writeln!, like the TS target.
  • worker/getrandom: Workers support comes later (see below).

Testing

  • Unit tests in src/bin/forge/mcp.rs and mcp/schema.rs, covering:
    • document constants;
    • parameter, flat-body and recursive-model schemas;
    • array bodies as a body argument;
    • skip rules;
    • duplicate tool names;
    • raw-string escaping;
    • JSON number and pointer helpers.
  • tests/cli.rs:
    • mcp() checks the generated layout.
    • mcp_builds() runs cargo build on the output. It's #[ignore]d because it downloads crates; run it with cargo test --test cli -- --ignored.
  • Manual end-to-end check: I generated a server from the latest openapi.forge.json release, which produced 2,486 tools and a 5.8 MB tools.rs. It builds in about 35s in debug, and every input schema parses in tools/list. Over stdio I ran initialize, tools/list and tools/call, both against a local echo server (path encoding, exploded query arrays, flat and field bodies, bearer auth, error statuses) and against the live API, where an invalid token correctly comes back as an isError result.
  • Checks: cargo fmt --check, both clippy runs with -D warnings, and cargo test --locked --all-features all pass.

Not in this PR (follow-ups)

  • Too many tools for one listing. Listing every tool for a large API is about 5 MB of tools/list, which is too much context for most clients. The next step is probably per-group selection, or a small search/execute tool surface.
  • Header parameters. Methods that require headers are skipped for now.
  • Auth. Forge doesn't parse securitySchemes yet, so auth is just a bearer token from the environment.
  • Cloudflare Workers (Streamable HTTP) entrypoint.
  • npm/npx distribution of prebuilt binaries.
  • Name and version. The package name and version are fixed (mcp-server 0.1.0) until forge has a way to configure them.

Add an `mcp` target that generates a Rust crate for an MCP server. The
server exposes every method as a tool and calls the API over HTTP when a
client calls one.

The crate is a fixed runtime (src/main.rs) plus a generated table of tools
(src/tools.rs). Each tool carries its HTTP method, path template, path and
query parameters, where its JSON body comes from, a JSON Schema of its
arguments, and read-only, destructive and idempotent hints derived from the
HTTP method and x-forge-require-confirmation.

The server speaks MCP over stdio with rmcp 3.5, sends requests with
reqwest 0.13, reads the base URL from API_BASE_URL (defaulting to the
document's first server), and sends API_TOKEN as a bearer token.
@pkg-pr-new

pkg-pr-new Bot commented Oct 18, 2026

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

commit: c9caaa3

@srikrsna

Copy link
Copy Markdown
Collaborator

I think we should start with a spec that clearly describes how OpenAPI operations are mapped to MCP protocol.

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