Introduction
In this blog series, we’ll show you how to build an MCP (Model Context Protocol) server packed with useful tools. Rather than starting from scratch, we’ll take advantage of our existing Protocol Buffers and Google’s gRPC transcoding. By creating a custom protoc (Protocol Buffer compiler) plugin, we can automatically generate the MCP server. This unified approach lets us produce gRPC services, OpenAPI specifications, REST APIs, and the MCP server all from the same source.
What You'll Build
By the end of this blog, you'll have:
- A single Protocol Buffer definition that generates both gRPC and REST API
- Automatic OpenAPI specification generation
- A foundation ready for MCP server generation (covered in Part 2)
Prerequisites
Before we start, make sure you have:
- Go 1.19+ installed
- Protocol Buffer compiler (protoc) installed
- Basic familiarity with gRPC and REST APIs
The Problem We're Solving
Our architecture started out simple: we used gRPC for internal services, REST APIs for external clients, and kept OpenAPI specs in a separate, manually maintained repository. As the system grew, this split became a headache—every change meant updating service code, the REST gateway, and documentation in multiple places. This led to inconsistencies, forgotten updates, and a lot of repeated work.
What is gRPC Transcoding?
gRPC Transcoding lets you expose your gRPC services as REST APIs, so clients can use familiar HTTP calls while your backend logic stays in gRPC. This is made possible by adding special HTTP annotations—known as Google API annotations—to your Protocol Buffer definitions. These annotations define how HTTP requests map to gRPC methods, including routes, path variables, and request bodies.
How Do Google API Annotations Work?
Google API annotations are a set of Protocol Buffer extensions that define how gRPC services map to REST APIs. They were originally developed by Google for their own APIs and have become the standard for gRPC-to-REST transcoding.
Key Components:
- google/api/annotations.proto: This file provides the extension for HTTP options on RPC methods.
- google.api.http: An option defined in
google/api/http.proto
that specifies the HTTP mapping for a gRPC method. - Path Variables: Placeholders in the HTTP path (like {book_id}) that are automatically mapped to fields in the request message.
- Body Mapping: The body field in the HTTP option determines which part of the request message is used as the HTTP request body.
In short, Google API annotations make it easy to maintain and extend your APIs, keeping everything in sync and reducing manual work.
Set Up Your Project
Let's start by creating a new project:
mkdir proto-to-mcp-tutorial
cd proto-to-mcp-tutorial
# Create directory structure
mkdir -p {proto,googleapis,generated/{go,openapi},cmd}
Your project structure should look like:
proto-to-mcp-tutorial/
├── proto/
├── googleapis/
├── generated/
│ ├── go/
│ └── openapi/
└── cmd/
Install Required Dependencies
First, let's install go protoc plugins:
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.34.1
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.3.0
go install github.com/google/gnostic/cmd/protoc-gen-openapi@latest
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@v2.24.0
Then we need the Google API definitions for HTTP annotations:
# Create directory for Google APIs
mkdir -p googleapis/google/api
# Download the required proto files
curl -o googleapis/google/api/annotations.proto \
https://raw.githubusercontent.com/googleapis/googleapis/master/google/api/annotations.proto
curl -o googleapis/google/api/http.proto \
https://raw.githubusercontent.com/googleapis/googleapis/master/google/api/http.proto
Create Your Protocol Buffer Definition
Create a file proto/bookstore.proto
with our bookstore service definition:
syntax = "proto3";
package bookstore.v1;
import "google/api/annotations.proto";
option go_package = "generated/go/bookstore/v1";
service BookstoreService {
// Get a book by ID
rpc GetBook(GetBookRequest) returns (Book) {
option (google.api.http) = {
get: "/v1/books/{book_id}"
};
}
// Create a new book
rpc CreateBook(CreateBookRequest) returns (Book) {
option (google.api.http) = {
post: "/v1/books"
body: "book"
};
}
}
message Book {
string book_id = 1;
string title = 2;
string author = 3;
int32 pages = 4;
}
message GetBookRequest {
string book_id = 1;
}
message CreateBookRequest {
Book book = 1;
}
The magic happens in those google.api.http
annotations. The get: "/v1/books/{book_id}"
tells the generator to create a GET endpoint where the book_id
URL parameter automatically maps to the corresponding field in our GetBookRequest
message. Similarly, body: "book"
specifies that the entire book
field becomes the HTTP request body for the CreateBook endpoint.
What's particularly elegant about this approach is that the same annotations work across multiple generators. The gRPC gateway uses them to create REST endpoints, the OpenAPI generator uses them to create proper path parameters and request body schemas, and as we'll see in Part 2, our MCP plugin can use them to understand how to structure tool inputs.
Generate Code
Once we had our proto file with annotations, the build process became surprisingly straightforward:
export GOOGLEAPIS_DIR=./googleapis
# Generate gRPC service code
protoc -I${GOOGLEAPIS_DIR} --proto_path=proto --go_out=./generated/go --go_opt paths=source_relative --go-grpc_out=./generated/go --go-grpc_opt paths=source_relative bookstore.proto
# Generate gRPC Gateway for REST endpoints
protoc -I${GOOGLEAPIS_DIR} --proto_path=proto --grpc-gateway_out=./generated/go --grpc-gateway_opt paths=source_relative bookstore.proto
# Generate OpenAPI specifications
protoc -I${GOOGLEAPIS_DIR} -I./proto --openapi_out=./generated/openapi \
--openapi_opt=fq_schema_naming=true \
--openapi_opt=version="1.0.0" \
--openapi_opt=title="Bookstore API" \
bookstore.proto
Each protoc command reads the same bookstore.proto file but generates different outputs based on the plugin used. The HTTP annotations provide the routing information that each generator needs to create proper interfaces.
Implement the gRPC Server
Create cmd/server/main.go
:
package main
import (
"context"
"fmt"
"log"
"net"
"net/http"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
pb "proto-to-mcp-tutorial/generated/go"
)
type server struct {
pb.UnimplementedBookstoreServiceServer
books map[string]*pb.Book
}
func (s *server) GetBook(ctx context.Context, req *pb.GetBookRequest) (*pb.Book, error) {
book, exists := s.books[req.BookId]
if !exists {
return nil, fmt.Errorf("book not found")
}
return book, nil
}
func (s *server) CreateBook(ctx context.Context, req *pb.CreateBookRequest) (*pb.Book, error) {
book := req.Book
if book.BookId == "" {
book.BookId = fmt.Sprintf("book-%d", len(s.books)+1)
}
s.books[book.BookId] = book
return book, nil
}
func main() {
// Initialize server with some sample data
srv := &server{
books: map[string]*pb.Book{
"book-1": {
BookId: "book-1",
Title: "The Go Programming Language",
Author: "Alan Donovan",
Pages: 380,
},
},
}
// Start gRPC server
go func() {
lis, err := net.Listen("tcp", ":9090")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterBookstoreServiceServer(s, srv)
log.Println("gRPC server starting on :9090")
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}()
// Start REST gateway
ctx := context.Background()
mux := runtime.NewServeMux()
opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
err := pb.RegisterBookstoreServiceHandlerFromEndpoint(ctx, mux, "localhost:9090", opts)
if err != nil {
log.Fatalf("failed to register gateway: %v", err)
}
log.Println("REST server starting on :8080")
if err := http.ListenAndServe(":8080", mux); err != nil {
log.Fatalf("failed to serve REST: %v", err)
}
}
Test Your Implementation
Run the server:
# Init go module
go mod init proto-to-mcp-tutorial && go mod tidy
go run cmd/server/main.go
You should see output like:
gRPC server starting on :9090
REST server starting on :8080
Open a new terminal for testing (keep the server running).
And our clients could interact with the exact same service logic through either gRPC or HTTP, with identical data models and behavior. A curl request to get a book, for example:
curl -X GET http://localhost:8080/v1/books/book-1
And returns exactly as expected:
{
"bookId": "book-1",
"title": "The Go Programming Language",
"author": "Alan Donovan",
"pages": 380
}
Verify OpenAPI Generation
Check the generated OpenAPI specification:
cat generated/openapi/openapi.yaml
You should see a complete OpenAPI spec that matches your REST endpoints! Look for:
- Path definitions for
/v1/books/{bookId}
and/v1/books
- Schema definitions for
bookstore.v1.Book
- HTTP method specifications (
get
,post
)
Conclusion
Congratulations! You've just built a unified API system where:
- Single Source of Truth: One proto file defines your entire API
- Dual Interface: Your service is accessible via both gRPC and REST
- Automatic Documentation: OpenAPI spec is generated automatically
- No Sync Issues: Everything stays in sync because it's generated from the same source
Here's what the architecture now looks like:
Next Steps
This foundation sets you up perfectly for Part 2, where we'll extend this same pattern to generate MCP servers for AI integration. The HTTP annotations you've added will help our MCP plugin understand how to structure tool inputs and outputs.