README
ΒΆ
protoc-gen-go-mcp
protoc-gen-go-mcp is a Protocol Buffers compiler plugin that generates Model Context Protocol (MCP) servers for your gRPC 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.
β οΈ Currently supports mark3labs/mcp-go as the MCP server runtime. Future support is planned for official Go SDKs and additional runtimes.
β¨ Features
- π Auto-generates MCP handlers from your
.protoservices - π§ AI-Friendly Schemas - Clean JSON schemas using modern JSON Schema 2020-12 specification
- π JSON Schema $defs Support - Message types defined once in
$defsand referenced via$ref - π Recursive Structure Support - Handles circular and recursive message references with cycle detection
- π Advanced OneOf Support - Handles protobuf oneOf with discriminated unions and automatic transformation
- π¬ Field Comments as Descriptions - Preserves protobuf comments in tool schemas (including nested messages)
- π¦ JSON Schema Generation for method inputs with proper validation
- π Flexible Integration - Wire up to gRPC servers or clients
- π§© Easy
bufIntegration - β‘ Well-Known Types - Proper handling of Google protobuf well-known types
- π― Gemini Compliant - Tool names follow Google's restrictions
- π‘οΈ Production Ready - Robust error handling, no panics, safe type assertions
π§ Usage
Generate code
Add entry to your buf.gen.yaml:
...
plugins:
- local:
- go
- run
- github.com/shaders/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
Advanced Schema Generation
JSON Schema Structure
protoc-gen-go-mcp generates modern JSON schemas with $defs for better organization:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"user": {"$ref": "#/$defs/User"},
"settings": {"$ref": "#/$defs/Settings"}
},
"required": ["user"],
"$defs": {
"User": {
"type": "object",
"properties": {
"name": {"type": "string", "description": "User's full name"},
"email": {"type": "string"}
},
"required": ["name", "email"]
},
"Settings": {
"type": "object",
"properties": {
"theme": {"type": "string"}
},
"required": []
}
}
}
OneOf Support with Discriminated Unions
protoc-gen-go-mcp generates AI-friendly schemas for protobuf oneOf fields using discriminated unions with object_type field:
// Proto definition
message Item {
oneof item_type {
Product product = 1;
Service service = 2;
}
}
Generates schema:
{
"item_typeOneOfType": {
"oneOf": [
{
"type": "object",
"properties": {
"object_type": {"const": "product", "type": "string"},
"price": {"type": "number"}
},
"required": ["object_type"]
},
{
"type": "object",
"properties": {
"object_type": {"const": "service", "type": "string"},
"duration": {"type": "string"}
},
"required": ["object_type"]
}
]
}
}
Recursive Structure Support
Handles complex recursive structures without stack overflow:
message FilterExpression {
message Operation {
repeated FilterExpression operands = 1; // Recursive reference
}
oneof kind {
Operation operation = 1;
string value = 2;
}
}
Annotation: zero_based_pagination
If your gRPC API uses 0-based pagination (page=0 is the first page), LLM clients tend to send page=1 for the first page anyway. The (mcp.options.zero_based_pagination) = true annotation lets you keep your protobuf 0-based for production gRPC traffic while presenting an LLM-friendly 1-based view through the MCP wrapper.
import "mcp/options/options.proto";
message ListItemsRequest {
// Page number (0-based).
int32 page = 1 [(mcp.options.zero_based_pagination) = true];
}
This makes the generator:
- Set
"minimum": 1on the field in the generated JSON schema and rewrite the description to read as 1-based. - Decrement the value by 1 inside the generated MCP handler before forwarding the request to gRPC. Values <= 0 are clamped to 0 so a non-compliant client never produces a negative index.
The annotation is defined in proto/mcp/options/options.proto and the Go counterpart in github.com/shaders/protoc-gen-go-mcp/pkg/options. Other clients (REST, control panels, regular gRPC consumers) see the protobuf untouched.
Annotation: tool β first-class MCP tool metadata π·οΈ
By default the generated tool name is the mangled fully-qualified method name (my_pkg_v1_WidgetService_GetWidget) and no ToolAnnotations are emitted. That works, but it won't win a beauty contest β and MCP directories (like Anthropic's) want human-friendly names, titles and honest behavioral hints. The (mcp.options.tool) method option gives you all of that:
import "mcp/options/options.proto";
service WidgetService {
// Fetches a widget by id. This comment becomes the tool description.
rpc GetWidget(GetWidgetRequest) returns (GetWidgetResponse) {
option (mcp.options.tool) = {
name: "get_widget" // required: ^[a-z][a-z0-9_]{1,63}$, globally unique
title: "Get widget" // optional human-readable title
read_only: true // optional hints, see below
idempotent: true
open_world: false
};
}
}
namebecomes the MCP tool name. It must match^[a-z][a-z0-9_]{1,63}$and be unique across all tools generated in one plugin invocation (usebufgenerationstrategy: allor a singleprotocrun for a global guarantee).titleis emitted as themcp.ToolAnnotationtitle; at most 60 characters, enforced at generation time.read_only/destructive/idempotent/open_worldare tri-state (optional bool). A hint you don't set is omitted from the generated tool, so MCP clients keep applying the spec defaults (readOnlyHint=false,destructiveHint=true,idempotentHint=false,openWorldHint=true). A hint you set is emitted explicitly.- The tool description still comes from the method's leading comment; parameter descriptions come from field comments.
Methods without the annotation generate byte-identical output to previous releases: legacy autogenerated name, no Annotations block, no new runtime fields. Existing consumers can upgrade the plugin without any change in output.
To make the metadata mandatory, pass the require_tool_annotation=true plugin option: any exposed method with a missing, malformed or duplicate name then fails the build with the fully-qualified method name in the error β no silent fallbacks:
# buf.gen.yaml
plugins:
- local: protoc-gen-go-mcp
out: ./gen/go
strategy: all
opt:
- paths=source_relative
- require_tool_annotation=true
Wiring up with gRPC client
It is also possible to directly forward MCP tool calls to gRPC clients. Follows gRPC-Gateway pattern. Connect to gRPC server, then:
testdatamcp.ForwardToTestServiceClient(mcpServer, myGrpcClient)
This directly connects the MCP handler to the gRPC client, requiring zero boilerplate. Each RPC method in your protobuf service becomes an MCP tool.
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.ForwardToTestServiceClient(mcpServer, client, option)
π§ͺ Development & Testing
Quick Commands
# Run all tests
task test
# Build the binary
task build
# Install to GOPATH/bin
task install
# Update golden test files
task generate-golden
# View all available commands
task --list
Manual Commands
# Run tests
go test ./...
# Update golden files
./tools/update-golden.sh
# Or manually for specific packages
go test ./pkg/generator -update-golden
# Build from source
go build -o protoc-gen-go-mcp ./cmd/protoc-gen-go-mcp
# Run integration tests (requires OPENAI_API_KEY)
# Either export OPENAI_API_KEY or add to .env file
export OPENAI_API_KEY="your-api-key"
task integrationtest
Development Workflow
# Format code
task fmt
# Run linting
task lint
# Generate protobuf files for testdata
task generate
Golden File Testing
The generator uses golden file testing to ensure output consistency. The test structure in pkg/generator/testdata/ is organized as:
testdata/
βββ *.proto # Input proto files (just drop new ones here!)
βββ buf.gen.yaml # Generates into actual/
βββ buf.gen.golden.yaml # Generates into golden/
βββ actual/ # Current generated output (committed to track changes)
βββ golden/ # Expected output (committed as test baseline)
To add new tests: Simply drop a .proto file in pkg/testdata/proto/testdata/ and run the tests. The framework automatically:
- Discovers all
.protofiles - Generates code using
task generate - Compares with expected output
- Creates missing golden files on first run
To update golden files after generator changes:
# Update all golden files
task generate-golden
# Or update specific package
go test ./pkg/generator -update-golden
ποΈ Recent Improvements
v0.2.0 (Latest)
- JSON Schema 2020-12: Modern schema generation with
$defsand$ref - Cycle Detection: Prevents stack overflow with recursive message structures
- Robust Error Handling: Replaced panics with proper error propagation
- Code Quality: Eliminated code duplication, added safe type assertions
- Performance: Optimized memory allocation and string operations
- Documentation: Comprehensive documentation for all public APIs
β οΈ Limitations
- Tool name mangling for long RPC names: If the full RPC name exceeds 64 characters, the head of the tool name is mangled to fit.
- Streaming RPCs are not yet supported (unary only)