Earlier this year I wired an LLM agent into five internal microservices. Each one had an OpenAPI spec. Each one needed an MCP server so the agent could call it as a tool. By the third service I was convinced someone had already written the generator I needed. No-one had, so I built it: openapi-go-mcp is the OpenAPI counterpart to the gRPC-to-MCP generator Redpanda shipped at the end of 2025. It takes any OpenAPI 3.x or Swagger 2.0 spec and emits a Go-based MCP server where every operation becomes an MCP tool.
This post is the guide I wish I had: what the generator gives you, how it works under the hood, and two ways to run it — one for a standalone proxy and one for embedding MCP into a service you already own.
The boilerplate nobody wants to write
MCP solves the N×M integration problem by collapsing N models and M tools into N+M standard connections. But that only works if the M tools actually exist. In a typical enterprise, the M tools are decade-old REST APIs with hundreds of operations and schemas already specified in OpenAPI. Turning each of those into an MCP server by hand means writing Go structs for every request and response, hand-crafting JSON Schema for every tool definition, and maintaining a translation layer that forwards tool calls to HTTP — all for code the spec already describes.
The uncomfortable truth is that an OpenAPI specification contains almost exactly what an MCP tool needs:
operationId→ the tool namesummary/description→ the tool description the LLM readsparameters+requestBody→ the tool’s JSON Schema inputresponses→ the structured output the LLM receives back
The gap is mechanical translation, not domain logic. openapi-go-mcp bridges that gap.
How the generator works
The CLI reads your OpenAPI spec, walks every operation that is not excluded, and emits a single *.mcp.go file. In companion mode, that file imports your existing oapi-codegen client and registers one MCP tool per operation. In proxy mode, the generator produces a complete runnable module with the HTTP client included. When the MCP host (Claude Desktop, an IDE, or your own agent) calls a tool, the generated code maps the tool arguments back to the right HTTP path, method, query string, headers, and body, then forwards the request through the typed oapi-codegen client.
flowchart LR
S[OpenAPI spec] --> G[openapi-go-mcp]
C[oapi-codegen client] --> G
G --> M[*.mcp.go]
M --> R[MCP runtime adapter]
Host[MCP Host / LLM] -->|"tools/list + tools/call"| R
Key design decisions keep it composable rather than magic:
- Companion to
oapi-codegen— the generated MCP layer imports the user’s own HTTP client package. Zero hand-written glue between the MCP tool and the actual REST call. - MCP-library-agnostic — the generated code targets a thin
MCPServerinterface. Runtime adapters exist for both the officialmodelcontextprotocol/go-sdkandmark3labs/mcp-go. Switch backends by changing one import and constructor call. - Schema fidelity — the tool’s JSON Schema is built from the spec’s components with
$defsfor shared types. A-openai-compatflag flattens it into a$ref-free schema for OpenAI’s strict validator. - Batch generation — point
-specat a directory, a glob, or a comma-separated list. Every match becomes its own<slug>mcp/package under-out, which is how you surface a fleet of microservices to an agent in one invocation. - Curated exposure with
x-mcp— mark operations, path items, or the entire document withx-mcp: falseto keep sensitive endpoints out of the tool list, or flip the default with-exclude-by-defaultfor an opt-in-only surface.
Mode one: proxy (a runnable server in four commands)
If you just want an MCP server now, proxy mode is a zero-boilerplate buildable module. One command produces main.go, go.mod, the generated *.mcp.go, and a README.md that lists the required environment variables for auth.
openapi-go-mcp \
-mode=proxy \
-spec petstore.yaml \
-out gen/petstore-mcp \
-module github.com/me/petstore-mcp
cd gen/petstore-mcp
go mod tidy
go build
# The exact env var name is derived from your spec's securitySchemes and listed in the generated README.md.
BEARER_TOKEN_BEARERAUTH=xxx ./petstore-mcp
The generated server reads API_BASE_URL to know where to proxy requests, uses the spec’s securitySchemes to load credentials from derived env vars, validates incoming tool arguments against the JSON Schema built from the spec, and surfaces HTTP responses — status codes, headers, and body — as MCP tool results. It speaks MCP over stdio by default, which is what Claude Desktop and most IDE integrations expect. No other wiring is required.
This is the fastest way to give an existing public or internal REST API an MCP face without touching the service itself.
Proxy mode data flow
sequenceDiagram
participant Host as MCP Host (Claude Desktop / IDE)
participant PS as Proxy Server (generated *.mcp.go + main.go)
participant Up as Upstream REST API
Host->>PS: tools/list (stdio)
PS->>Up: GET /pets (HTTP)
Up-->>PS: [{"id":1,"name":"Fluffy"}]
PS-->>Host: tool schemas + descriptions
Host->>PS: tools/call: listPets (stdio)
PS->>PS: validate args against JSON Schema
PS->>Up: GET /pets?status=available (HTTP)
Up-->>PS: 200 OK + [{"id":1,"name":"Fluffy"}]
PS-->>Host: structured MCP result (text + metadata)
The proxy is a thin translation layer. Every tool call becomes one HTTP request. Auth credentials and base URL come from env vars, so the proxy needs no code changes when the upstream changes.
Mode two: companion (embed MCP into your service)
Most real services already have a Go binary: an API gateway, a BFF, a sidecar, or the service itself. Companion mode generates only the MCP layer and expects you to write main.go, own the transport, and inject the oapi-codegen client yourself. That means custom retries, distributed tracing, mTLS, and auth middleware ride on the same client your service already uses.
# 1. Generate the typed HTTP client with oapi-codegen.
oapi-codegen -generate types,client -package pet -o gen/pet/pet.gen.go petstore.yaml
# 2. Generate the MCP companion.
openapi-go-mcp \
-spec petstore.yaml \
-out gen/petmcp \
-package petmcp \
-client-import github.com/me/myrepo/gen/pet
The generated companion exposes a single registration function. You instantiate your client, create an MCP server with whichever adapter you prefer, and call Register:
package main
import (
"context"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/me/myrepo/gen/pet"
"github.com/me/myrepo/gen/petmcp"
"github.com/dipjyotimetia/openapi-go-mcp/pkg/runtime/gosdk"
)
func main() {
client, _ := pet.NewClientWithResponses("https://api.example.com")
raw, s := gosdk.NewServer("petstore-mcp", "1.0.0")
petmcp.RegisterSwaggerPetstoreClient(s, client)
_ = raw.Run(context.Background(), &mcp.StdioTransport{})
}
Here the HTTP client belongs to you. The MCP layer is a thin side-effect of a service you already ship. Companion mode is what I reach for when the MCP server is one feature inside a larger binary rather than a standalone process.
Companion mode data flow
sequenceDiagram
participant Host as MCP Host (Claude Desktop / IDE)
participant MB as MyService binary (main.go I wrote)
participant MCP as MCP layer (*.mcp.go generated)
participant OC as oapi-codegen client (my code)
participant Up as Upstream REST API
Host->>MB: tools/list (stdio)
MB->>MCP: register tools from spec
MCP-->>MB: tool schemas
MB-->>Host: available tools + descriptions
Host->>MB: tools/call: createPet (stdio)
MB->>MCP: forward tool call with args
MCP->>OC: typed HTTP call (CreatePetWithResponse)
OC->>Up: POST /pets (HTTP with my retry/mTLS/tracing)
Up-->>OC: 201 Created + {"id":2}
OC-->>MCP: typed response struct
MCP-->>MB: structured MCP result
MB-->>Host: tool result + any custom enrichment
Because you own main.go and the oapi-codegen client, you can inject your existing middleware — distributed tracing spans, circuit breakers, custom auth refresh logic — into the same path the MCP tools use. The generated layer only handles schema translation; the transport is yours.
Filtering what the agent can see
Not every operation should be an agent tool. Admin endpoints, destructive batch jobs, and internal health checks have no place in an LLM’s tool list. The generator respects x-mcp at three levels, with the most specific winning:
paths:
/admin:
x-mcp: false # exclude every operation under /admin …
delete:
operationId: purgeAll
get:
operationId: listAdmins
x-mcp: true # … except this one
Add -exclude-by-default to the CLI and nothing becomes an MCP tool unless it carries x-mcp: true explicitly. That is the right posture for a large third-party spec where you want to whitelist a handful of safe operations rather than blacklist the risky ones.
Batch generation: one tool for a fleet
Most organisations do not have one REST API. They have dozens, and each one ships its own spec in a repository or a directory. The -spec flag accepts a directory, a glob, or a comma-separated list. Every match is rendered into its own subdirectory.
# Recursive directory: every spec under apis/ becomes its own tool set
openapi-go-mcp \
-spec apis/ \
-out gen \
-client-import github.com/acme/apis/gen \
-force
The filename stem becomes the package slug and the package name. You can register them all against a single MCP server with WithNamePrefix to namespace the tools (petstore_listPets, billing_getInvoice) so an agent that loads your whole fleet never sees colliding tool names.
Comparing the two bridges: REST vs. gRPC
Redpanda’s protoc-gen-go-mcp works from Protobuf, which is structurally similar: a service definition already carries method names, request types, and response types. The difference is where the types live.
| Concern | gRPC / Protobuf | REST / OpenAPI |
|---|---|---|
| Schema source | .proto file with service and message definitions | OpenAPI 3.x / Swagger 2.0 with operations and component schemas |
| Codegen step | protoc-gen-go-mcp + protoc | oapi-codegen for types/client, then openapi-go-mcp for the MCP layer |
| Transport | gRPC / HTTP/2 | HTTP/1.1 or HTTP/2 via the oapi-codegen client |
| Auth | Usually mTLS or per-RPC metadata | Spec-driven: securitySchemes mapped to env vars (proxy) or your middleware (companion) |
| Filtering | Protobuf options / custom annotations | x-mcp in the spec or -exclude-by-default |
| Ecosystem fit | gRPC-first microservices, internal RPC | Public REST APIs, legacy services, anything already documented in Swagger |
Both generators solve the same problem: the spec already knows what the MCP server should look like, so stop writing it by hand. If your organisation is gRPC-native, use Redpanda’s toolchain. If your APIs are REST — which is most of them — openapi-go-mcp fills the same gap.
What I would do differently next time
Building this taught me two things about spec-driven generators that I did not fully appreciate going in.
Recursion is harder than it looks. OpenAPI specs reuse components via $ref, and those refs can be mutually recursive. JSON Schema for MCP tools does not allow infinite recursion, so the generator has to track visited refs and break cycles at a safe depth. The first naive implementation panicked on Kubernetes’ own OpenAPI spec. Now it handles arbitrarily deep shared schemas and emits diagnostics for cycles instead of failing.
Proxy auth is a UX problem, not a technical one. Deriving an env var name from a securitySchemes key sounds trivial, but OAuth2 implicit with spaces and BearerAuth_V2 produce different conventions. The fix was strict sanitisation and a clear diagnostic: the generated README.md lists every required env var, and missing credentials produce an explicit error before the server ever starts.
Where this fits in the agent stack
The agentic ecosystem is sorting itself into predictable layers. At the bottom is JSON-RPC, the wire format. On top sits MCP, the semantic protocol for tool discovery and invocation. Above that are the agents themselves. The missing layer — and the one openapi-go-mcp targets — is the bridge between the world of existing REST APIs and the world of MCP-native tools. You should not have to rewrite a working service to make it agent-accessible. The OpenAPI spec is the contract; the generator is the adapter.
Getting started
Install the binary from Homebrew, a release archive, a container image, or go install:
brew install dipjyotimetia/tap/openapi-go-mcp
# or
go install github.com/dipjyotimetia/openapi-go-mcp/cmd/openapi-go-mcp@latest
Point it at a spec, choose proxy or companion mode, and you have an MCP server. The repository has a working Petstore example that runs through both modes end to end.
If you have a REST API that an LLM should be able to call, and that API already has an OpenAPI spec, the right question is not “how do I write an MCP server?” It is “why haven’t I generated one yet?”