Protoc Gen Go Mcp
About
Go protobuf compiler extension to turn any gRPC service into an MCP server
Details
- Author
- redpanda-data
- GitHub stars
- 213
- Downloads
- 553
- Categories
- Developer Tools
Jump to
- Auto-generates MCP handlers from .proto services
- Outputs JSON Schema for method inputs
- Wire up to gRPC or ConnectRPC servers/clients
- Easy integration with buf build tool
- Runtime LLM provider selection (Standard/OpenAI)
- MCP library agnostic (official SDK and mark3labs)
Add the plugin to your buf.gen.yaml, then run buf generate to produce .pb.mcp.go files. In your Go server code, import the adapter for your chosen MCP library (gosdk or mark3labs), create a server, and register handlers using generated RegisterHandler functions. Forward tool calls to gRPC or ConnectRPC clients with generated ForwardTo*Client functions.
protoc-gen-go-mcp
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:
...
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:
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.
With the official go-sdk (recommended)
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
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:
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:
// 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:
testdatamcp.ForwardToTestServiceClient(s, myGrpcClient)
Same for connectrpc:
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.
// 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:
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:
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
// 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 RegisterHandler 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:
// 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:
// 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 representationsOpenAI 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 unionsDevelopment & 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
Sign in to leave a review
Use Google, GitHub, or an email account so ratings stay tied to real people.
No reviews posted yet.





