protoc-gen-mcp — Proto-First MCP Server Generator
Generate type-safe MCP tool bindings from annotated protobuf services for Go, Python, Java, Kotlin, and TypeScript. JavaScript projects consume compiled TypeScript output. Protobuf is the source of truth: define your service once in .proto, generate language-specific protobuf types and MCP sidecars, implement the handler interface, and serve.
When to Use
- Building a new MCP server and want type-safe, schema-validated tools
- Already have protobuf services and want to expose them as MCP tools
- Need JSON Schema validation on MCP tool inputs derived from proto definitions
- Want ProtoJSON as the wire format for MCP tool requests and responses
- Targeting Go, Python, Java, Kotlin, TypeScript, or JavaScript runtimes
Prerequisites
Install easyp — the recommended way to lint and generate:
brew install easyp-tech/tap/easyp
Or install from source:
go install github.com/easyp-tech/easyp/cmd/easyp@latest
See https://easyp.tech/docs for full documentation.
Step-by-Step Workflow
Step 1: Define Your Proto Service
Create a .proto file with service, methods, and MCP annotations using mcp/options/v1/options.proto:
syntax = "proto3";
package myapi.v1;
option go_package = "github.com/you/myproject/myapi/v1;myapiv1";
import "mcp/options/v1/options.proto";
import "google/protobuf/empty.proto";
service MyServiceAPI {
option (mcp.options.v1.service) = {
namespace: "myapi"
description: "My tools exposed as MCP tools."
};
rpc CreateItem(CreateItemRequest) returns (CreateItemResponse) {
option (mcp.options.v1.method) = {
title: "Create item"
description: "Create a new item with validation."
annotations: { read_only_hint: false }
};
}
rpc Health(google.protobuf.Empty) returns (HealthResponse) {
option (mcp.options.v1.method) = {
title: "Health check"
description: "Verify the server is alive."
annotations: { read_only_hint: true }
};
}
}
message CreateItemRequest {
string name = 1 [(mcp.options.v1.field) = {
description: "Item name."
examples: [{ string_value: "Widget" }]
min_length: 1
max_length: 200
}];
int32 count = 2 [(mcp.options.v1.field) = {
default_value: { integer_value: 1 }
minimum: 1
maximum: 1000
}];
repeated string tags = 3 [(mcp.options.v1.field) = {
max_items: 20
unique_items: true
}];
optional string note = 4;
}
message CreateItemResponse {
string id = 1;
}
message HealthResponse {
string status = 1;
}
Step 2: Choose Your Language
Each language has its own easyp.yaml configuration and handler pattern. See the examples:
| Language | Example | Handler Style |
|---|---|---|
| Go | examples/go | <Service>ToolHandler interface |
| Python (dataclass) | examples/python/dataclass | Dataclasses from *_mcp.py |
| Python (protobuf) | examples/python/protobuf | Raw *_pb2 classes |
| Python (dataclass+protobuf) | examples/python/dataclass-protobuf | Both surfaces side by side |
| Java | examples/java | Nested <Service>ToolHandler in *Mcp.java |
| Kotlin | examples/kotlin | <Service>ToolHandler interface |
| TypeScript | examples/typescript | <Service>ToolHandler interface |
| JavaScript | examples/javascript | Compiled TS output + JSDoc types |
Step 3: Generate Code
easyp validate-config
easyp mod download
easyp lint -p proto -r .
easyp generate -p proto -r .
Step 4: Implement the Handler and Serve
Follow the language-specific example linked above.
Key Concepts
Tool Naming
Generated tool names follow the pattern {namespace}_{MethodName}. Dots in the namespace are normalized to underscores. Override the method segment with mcp.options.v1.method.name.
Requiredness Policy
Requiredness in generated MCP JSON Schema is determined by proto3 syntax:
| Proto Pattern | Required? |
|---|---|
string name = 1 (singular, no optional) |
YES |
optional string name = 1 |
NO |
repeated string names = 1 |
NO |
map<string, string> m = 1 |
NO |
oneof choice { ... } |
NO (unless mcp.options.v1.oneof.required = true) |
Fields that are not required accept explicit JSON null.
ProtoJSON Contract
MCP tool I/O uses ProtoJSON encoding. Key differences from plain JSON:
int64/uint64are JSON strings, not numbersfloat/doubleaccept"NaN","Infinity","-Infinity"as stringsbytesuse base64 encoding- Enums use string names (e.g.,
"FORECAST_MODE_DAILY") Timestamp→ RFC 3339 string,Duration→"3.5s",FieldMask→"field1,field2"
Supported Protobuf Features
- Scalars, enums, nested messages, repeated, maps,
oneof,optional - Recursive messages via
$defs/$ref - Well-known types:
Any,Empty,Timestamp,Duration,FieldMask,Struct,Value,ListValue, and all scalar wrapper types
Fail-Fast Rules
The generator rejects at generation time (not runtime):
- Proto2 syntax
- Streaming RPCs (client, server, or bidirectional)
- Unsupported
google.protobuf.*types
Quick Proto Options Reference
import "mcp/options/v1/options.proto";
// Service: namespace prefix, description, icons
option (mcp.options.v1.service) = {
namespace: "myapi"
description: "My API tools."
};
// Method: tool name, title, description, visibility, agent hints
option (mcp.options.v1.method) = {
name: "CustomName"
title: "Human Title"
description: "What this tool does."
hidden: true
annotations: {
read_only_hint: true
destructive_hint: false
idempotent_hint: true
}
};
// Field: description, examples, defaults, validation constraints
[(mcp.options.v1.field) = {
description: "Field purpose."
examples: [{ string_value: "example" }]
default_value: { integer_value: 42 }
pattern: "^[A-Z]"
min_length: 1
max_length: 255
minimum: 0
maximum: 100
min_items: 1
max_items: 50
unique_items: true
}];
// Oneof: make a oneof group required in the MCP schema
option (mcp.options.v1.oneof) = { required: true };
// Enum: title and description
option (mcp.options.v1.enum) = { title: "Status" };
// Enum value: hide sentinel zero-value
UNSPECIFIED = 0 [(mcp.options.v1.enum_value) = { hidden: true }];
For full options details, read references/options-reference.md in this skill.
Common Patterns
Hide Internal RPCs
rpc InternalDebug(DebugRequest) returns (DebugResponse) {
option (mcp.options.v1.method) = { hidden: true };
};
Read-Only vs Destructive Tools
// Read-only query
option (mcp.options.v1.method) = {
annotations: { read_only_hint: true }
};
// Destructive mutation
option (mcp.options.v1.method) = {
annotations: { destructive_hint: true }
};
Testing With MCP Inspector
# Go
npx -y @modelcontextprotocol/inspector go run ./cmd/myserver
# Python
npx -y @modelcontextprotocol/inspector python server.py
# TypeScript (build first)
npx -y @modelcontextprotocol/inspector node dist/server.js
# Java/Kotlin (after installDist)
npx -y @modelcontextprotocol/inspector ./build/install/myserver/bin/myserver
Reference Files
For detailed lookup tables, read these files from this skill directory:
references/options-reference.md— full MCP proto options with all fields and examplesreferences/schema-mapping.md— proto type → JSON Schema mapping, well-known types, nullability rules