trackmcp
Back to directory
redpanda-data

protoc-gen-go-mcp

View on GitHub

Go protobuf compiler extension to turn any gRPC service into an MCP server

169 stars GoServers & Infrastructure Updated Oct 24, 2025

Documentation

`protoc-gen-go-mcp`

Test
Go Report Card
codecov

`protoc-gen-go-mcp` is a Protocol Buffers compiler plugin that generates Model Context Protocol (MCP) servers for your `gRPC` or `ConnectRPC` APIs.

It generates `*.pb.mcp.go` files for each protobuf service, enabling you to delegate handlers directly to gRPC servers or clients. Under the hood, MCP uses JSON Schema for tool inputs -- `protoc-gen-go-mcp` auto-generates these schemas from your method input descriptors.

Generated code is MCP-library-agnostic: pick modelcontextprotocol/go-sdk (the official SDK) or mark3labs/mcp-go at server creation time through a thin adapter -- no regeneration needed.

Features

  • Auto-generates MCP handlers from your `.proto` services
  • Outputs JSON Schema for method inputs
  • Wire up to gRPC or ConnectRPC servers/clients
  • Easy integration with `buf`
  • Runtime LLM provider selection -- standard MCP or OpenAI-compatible schemas
  • MCP library agnostic -- official go-sdk and mark3labs/mcp-go both supported

Usage

Generate code

Add entry to your `buf.gen.yaml`:

code
...
plugins:
  - local:
      - go
      - run
      - github.com/redpanda-data/protoc-gen-go-mcp/cmd/protoc-gen-go-mcp@latest
    out: ./gen/go
    opt: paths=source_relative

You need to generate the standard `*.pb.go` files as well. `protoc-gen-go-mcp` by defaults uses a separate subfolder `{$servicename}mcp`, and imports the `*pb.go` files -- similar to connectrpc-go.

After running `buf generate`, you will see a new folder for each package with protobuf Service definitions:

code
tree pkg/testdata/gen/
gen
  go
    testdata
      test_service.pb.go
      testdataconnect/
        test_service.connect.go
      testdatamcp/
        test_service.pb.mcp.go

Setting up the MCP server

Generated code programs against the `runtime.MCPServer` interface. You choose the backing MCP library by importing the corresponding adapter package.

go
import (
    "github.com/modelcontextprotocol/go-sdk/mcp"
    "github.com/redpanda-data/protoc-gen-go-mcp/pkg/runtime/gosdk"
)

// Create server -- raw is *mcp.Server for transport, s is runtime.MCPServer for tools.
raw, s := gosdk.NewServer("my-server", "1.0.0")

testdatamcp.RegisterTestServiceHandler(s, &srv)

// Serve over stdio
raw.Run(ctx, &mcp.StdioTransport{})

With mark3labs/mcp-go

go
import (
    "github.com/mark3labs/mcp-go/server"
    "github.com/redpanda-data/protoc-gen-go-mcp/pkg/runtime/mark3labs"
)

// Create server -- raw is *server.MCPServer for transport, s is runtime.MCPServer for tools.
raw, s := mark3labs.NewServer("my-server", "1.0.0")

testdatamcp.RegisterTestServiceHandler(s, &srv)

// Serve over stdio
server.ServeStdio(raw)

Wiring up handlers

Example for in-process registration:

go
srv := testServer{} // your gRPC implementation

// Register all RPC methods as tools on the MCP server
testdatamcp.RegisterTestServiceHandler(s, &srv)

Each RPC method in your protobuf service becomes an MCP tool.

Runtime LLM Provider Selection

You can choose LLM compatibility at runtime without regenerating code:

go
// Option 1: Use convenience function with runtime provider selection
testdatamcp.RegisterTestServiceHandlerWithProvider(s, &srv, runtime.LLMProviderOpenAI)

// Option 2: Register specific handlers directly
testdatamcp.RegisterTestServiceHandler(s, &srv)        // Standard MCP
testdatamcp.RegisterTestServiceHandlerOpenAI(s, &srv)  // OpenAI-compatible

Wiring up with grpc and connectrpc client

Forward MCP tool calls directly to gRPC clients:

go
testdatamcp.ForwardToTestServiceClient(s, myGrpcClient)

Same for connectrpc:

go
testdatamcp.ForwardToConnectTestServiceClient(s, myConnectClient)

This directly connects the MCP handler to the client, requiring zero boilerplate.

Extra properties

It's possible to add extra properties to MCP tools, that are not in the proto. These are written into context.

go
// Enable URL override with custom field name and description
option := runtime.WithExtraProperties(
    runtime.ExtraProperty{
        Name:        "base_url",
        Description: "Base URL for the API",
        Required:    true,
        ContextKey:  MyURLOverrideKey{},
    },
)

// Use with any generated function
testdatamcp.RegisterTestServiceHandler(s, &srv, option)
testdatamcp.ForwardToTestServiceClient(s, client, option)

Tool name prefixing

When registering the same service multiple times (e.g. separate database instances), use `WithNamePrefix` to namespace tools:

go
sqlv1mcp.RegisterSQLServiceHandler(s, postgresHandler, runtime.WithNamePrefix("postgres"))
sqlv1mcp.RegisterSQLServiceHandler(s, clickhouseHandler, runtime.WithNamePrefix("clickhouse"))
// Tools: postgres_SQLService_Query, clickhouse_SQLService_Query, ...

Tool naming

By default a tool is named after the method's full proto name with dots replaced by underscores (`redpanda_mcps_sql_v1_SQLService_Query`). Names longer than 64 characters — the strictest provider limit — are truncated: a 10-character hash of the full name plus as much of the tail (the most specific part) as fits. The hash is forced to start with a letter because Gemini rejects function names that start with a digit.

To publish a method under a stable, human-chosen name instead, set the `(mcp.v1.tool_name)` method option:

proto
import "mcp/v1/annotations.proto";

rpc GetEquityResearchReport(GetEquityResearchReportRequest) returns (GetEquityResearchReportResponse) {
  option (mcp.v1.tool_name) = "get_equity_research_report";
}

The value must match `^[a-z_][a-z0-9_-]{0,63}$` — lowercase snake/kebab case within every provider's function-name rules; lowercase guarantees configured names pass untouched through consumers' de-mangling heuristics (they key on the UpperCamel `Service_` segment of derived names). Code generation fails on an invalid value and on duplicate tool names within a service (one service is one MCP server registration — different services may legitimately reuse a name). The dynamic registration path (`gen.RegisterService`) applies the same option; an invalid or colliding configured name there falls back to the derived name so a tool is never silently dropped.

Migrating from mark3labs-only (pre-v0.2)

Generated code no longer imports `mark3labs/mcp-go` directly. It programs against the `runtime.MCPServer` interface, and you pick the MCP library via an adapter package.

1. Server creation

go
// Before
s := server.NewMCPServer("name", "1.0", server.WithToolCapabilities(true))

// After -- mark3labs
raw, s := mark3labs.NewServer("name", "1.0", server.WithToolCapabilities(true))
// raw is *server.MCPServer for transport (ServeStdio, NewStreamableHTTPServer, etc.)
// s is runtime.MCPServer for tool registration

// After -- official go-sdk
raw, s := gosdk.NewServer("name", "1.0")

2. Tool registration

Generated `Register*Handler` functions now take `runtime.MCPServer` instead of `*server.MCPServer`. Just pass `s` from step 1.

If you were calling `s.AddTool()` manually with mark3labs types to do name prefixing, delete that code and use `WithNamePrefix`:

go
// Before: 60 lines of manual register.go per service
sql.RegisterTools(s, "postgres", handler) // custom wrapper around s.AddTool(mcp.Tool, ...)

// After: one line
sqlv1mcp.RegisterSQLServiceHandler(s, handler, runtime.WithNamePrefix("postgres"))

3. HandleError return type

`runtime.HandleError()` now returns `*runtime.CallToolResult` instead of `*mcp.CallToolResult`. If you call it from custom handlers, update the return type.

4. Transport

Transport setup is unchanged -- use the raw server from step 1:

go
// mark3labs stdio
server.ServeStdio(raw)

// mark3labs streamable HTTP
mcpserver.NewStreamableHTTPServer(raw)

// go-sdk stdio
raw.Run(ctx, &mcp.StdioTransport{})

LLM Provider Compatibility

The generator creates both standard MCP and OpenAI-compatible handlers automatically. Choose which to use at runtime.

Standard MCP

  • Full JSON Schema support (additionalProperties, anyOf, oneOf)
  • Maps represented as JSON objects
  • Well-known types use native JSON representations

OpenAI Compatible

  • Restricted JSON Schema (no additionalProperties, anyOf, oneOf)
  • Maps converted to arrays of key-value pairs
  • Well-known types (Struct, Value, ListValue) encoded as JSON strings
  • All fields marked as required with nullable unions

Development & Testing

Commands

All commands use just:

bash
just build                # Build everything (Bazel)
just test-unit            # Unit + golden tests (no API keys needed)
just test                 # All tests including conformance/integration (needs API keys)
just test-cover           # Tests with coverage report
just generate             # Regenerate proto code + descriptor set
just lint                 # Run golangci-lint
just fmt                  # Format code
just install              # Install binary to GOPATH/bin
just gazelle              # Sync BUILD files from go.mod

# View all available commands
just --list

Development Workflow

bash
# 1. Edit proto or generator code
# 2. Regenerate
just generate
# 3. Run tests
just test-unit

Golden File Testing

The generator uses golden file testing to ensure output consistency. Tests re-run the generator in-process from compiled descriptors and compare against checked-in `*.pb.mcp.go` files.

To add new tests: Drop a `.proto` file in `pkg/testdata/proto/testdata/` and run `just generate`. The golden test automatically discovers all generated files and compares them.

Proto-to-JSON Schema mapping

The plugin maps protobuf types to JSON Schema. Understanding these mappings matters because LLMs see the schema, not your proto definitions.

Scalar types

Proto typeJSON Schema typeNotes
`double`, `float``number`
`int32`, `uint32`, `sint32`, `fixed32`, `sfixed32``integer`
`int64`, `uint64`, `sint64`, `fixed64`, `sfixed64``string`JSON cannot represent 64-bit integers precisely
`bool``boolean`
`string``string`
`bytes``string``contentEncoding: base64`
`enum``string``enum` array with all value names

Messages and maps

Nested messages become nested `object` schemas. Maps depend on the mode:

Standard mode: `map` becomes a JSON object with `propertyNames` constraints (e.g., `pattern` for int keys, `enum` for bool keys).

OpenAI mode: `map` becomes an array of `{key, value}` objects. Key constraints (patterns, enums) are preserved on the `key` field. At runtime, `FixOpenAI` converts these arrays back to objects before proto unmarshaling.

Well-known types

Proto typeStandard schemaOpenAI schema
`google.protobuf.Timestamp``string` with `format: date-time`Same
`google.protobuf.Duration``string` with duration patternSame
`google.protobuf.Struct``object` with `additionalProperties: true``string` (JSON-encoded)
`google.protobuf.Value`any (no type constraint)`string` (JSON-encoded)
`google.protobuf.ListValue``array``string` (JSON-encoded)
`google.protobuf.FieldMask``string`Same
`google.protobuf.Any``object` with `@type` + `value`Same, with `additionalProperties: false`
Wrapper types (`StringValue`, etc.)nullable scalar (e.g., `["string", "null"]`)Same

In OpenAI mode, `FixOpenAI` handles the reverse transformation at runtime: JSON-encoded strings are parsed back, wrapper objects `{"value": X}` are unwrapped to `X`, and map arrays are converted to objects.

Oneof fields

Standard mode: Uses JSON Schema `anyOf` with one entry per oneof group. Each entry specifies one allowed alternative.

OpenAI mode: Oneof fields are flattened into nullable fields (e.g., `type: ["string", "null"]`) with a description noting the oneof constraint. At runtime, `FixOpenAI` strips null oneof alternatives and keeps only the first non-null field per group.

Validation constraints

buf.validate annotations are mapped to JSON Schema keywords:

ValidationJSON Schema
`string.uuid``format: uuid`
`string.email``format: email`
`string.pattern``pattern`
`string.min_len/max_len``minLength/maxLength`
`int32.gte/lte``minimum/maximum`
`int32.gt/lt``minimum+1 / maximum-1`
`float.gt/lt``exclusiveMinimum/exclusiveMaximum`
`double.gte/lte``minimum/maximum`

Recursive messages

Self-referencing and mutually recursive messages (e.g., tree nodes, linked lists) are supported. The schema expands up to 3 levels deep with full field detail. Beyond that, recursive fields become `{"type": "string", "description": "JSON-encoded TreeNode. Provide a JSON object as a string."}` -- the same pattern used for `Struct`/`Value`/`ListValue` in OpenAI mode.

At runtime, `FixOpenAI` parses these string-encoded fields back into JSON objects before proto unmarshaling. The actual data can nest arbitrarily deep -- the depth limit only applies to the schema the LLM sees, not to what it can send.

Tool name mangling

If the fully qualified RPC name (dots replaced with underscores) exceeds 64 characters, the name is truncated: the head is replaced with a 10-character SHA-256 hash prefix, preserving the tail (the most specific part, typically `ServiceName_MethodName`). The 64-char limit exists because Claude desktop enforces it.

Limitations

  • No interceptor support (yet). Registering with a gRPC server bypasses interceptors.
  • Recursive message schemas lose field-level detail beyond 3 levels of nesting (see above). The LLM must encode deeper levels as JSON strings.
  • Extra properties added via `WithExtraProperties` must not collide with proto field names. If they do, the extra property value will be extracted into context but will also leak into the proto message.

Feedback

We'd love feedback, bug reports, or PRs! Join the discussion and help shape the future of Go and Protobuf MCP tooling.

Frequently asked questions

What is protoc-gen-go-mcp?

protoc-gen-go-mcp is Go protobuf compiler extension to turn any gRPC service into an MCP server

How do I install protoc-gen-go-mcp?

Open the GitHub repository and follow its README. Most MCP servers are added to your client's MCP config, then called by your agent.

Is protoc-gen-go-mcp open source?

Yes — it is hosted on GitHub at https://github.com/redpanda-data/protoc-gen-go-mcp and has 169 stars.

Related MCP tools

Run your own MCP server? See who uses it and what to fix.

Measure it with TrackMCP