Protobuf로 MCP 서버 구축하기 (Part 1) – Protobuf에서 REST API까지
Charlie Zhang
2025년 9월 3일
들어가며
이번 블로그 시리즈에서는 유용한 툴들을 담은 MCP(Model Context Protocol) 서버를 어떻게 구축할 수 있는지 보여드리겠습니다. 처음부터 새로 만드는 대신, 이미 존재하는 Protocol Buffers와 Google의 gRPC Transcoding을 활용합니다.
우리는 커스텀 protoc(Protocol Buffer 컴파일러) 플러그인을 만들어 MCP 서버를 자동으로 생성할 수 있습니다. 이렇게 하면 gRPC 서비스, OpenAPI 스펙, REST API, MCP 서버까지 모두 하나의 소스에서 통합적으로 생성할 수 있습니다.
이 시리즈는 총 4편으로 구성됩니다:
- Part 1 (이번 글): Protobuf에서 gRPC Transcoding을 활용한 REST API 생성
- Part 2: Protoc 플러그인으로 MCP 서버 자동 생성
- Part 3: Proto 주석에서 툴 설명 자동 생성 (예정)
- Part 4: 실제 MCP 툴 운영 경험에서 얻은 인사이트 (예정)
이번 글에서 만들 것
이 글을 끝까지 따라가면 다음을 얻게 됩니다:
- 하나의 Protocol Buffer 정의로 gRPC와 REST API를 동시에 생성
- 자동 OpenAPI 스펙 생성
- MCP 서버 생성을 위한 기초 (Part 2에서 다룸)
👉 모든 코드는 GitHub 저장소 zhangcz828/proto-to-mcp-tutorial 에서 확인할 수 있습니다.
사전 준비
시작하기 전 준비물:
- Go 1.19+
- Protocol Buffer 컴파일러(protoc)
- gRPC와 REST API에 대한 기본 이해
우리가 해결하려는 문제
초기에는 단순했습니다. 내부 서비스는 gRPC, 외부 클라이언트는 REST API, 그리고 OpenAPI 스펙은 별도 저장소에서 수동 관리했습니다.
하지만 시스템이 커지면서 문제들이 생겼습니다:
- 코드, 게이트웨이, 문서를 각각 업데이트해야 했고
- 일관성이 깨지고, 누락이 생기며, 중복 작업이 늘어났습니다.
gRPC Transcoding이란?
gRPC Transcoding은 gRPC 서비스를 REST API로 노출하는 기능입니다. 즉, 클라이언트는 익숙한 HTTP 호출을 사용할 수 있고, 백엔드는 gRPC 로직을 그대로 유지합니다.
이는 Google API annotations라는 특별한 HTTP 어노테이션을 Protobuf 정의에 추가함으로써 가능합니다.
- 경로, Path 변수, Request Body 등을 지정
- 동일한 주석을 gRPC Gateway, OpenAPI 생성기, MCP 플러그인이 모두 이해할 수 있음
프로젝트 세팅
mkdir proto-to-mcp-tutorial
cd proto-to-mcp-tutorial
mkdir -p {proto,googleapis,generated/{go,openapi},cmd}
구조는 다음과 같습니다:
proto-to-mcp-tutorial/
├── proto/
├── googleapis/
├── generated/
│ ├── go/
│ └── openapi/
└── cmd/
의존성 설치
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
그리고 Google API 어노테이션 파일 다운로드:
mkdir -p googleapis/google/api
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
Protobuf 정의 작성
proto/bookstore.proto
파일:
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;
}
여기서 google.api.http 어노테이션이 핵심입니다:
get: "/v1/books/{book_id}"
→ GET 요청에서 book_id 경로 변수를 자동 매핑body: "book"
→ CreateBook 엔드포인트의 HTTP body에 Book 메시지 전체 매핑
코드 생성
export GOOGLEAPIS_DIR=./googleapis
# gRPC 코드
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
# REST 엔드포인트 (gRPC Gateway)
protoc -I${GOOGLEAPIS_DIR} --proto_path=proto \
--grpc-gateway_out=./generated/go --grpc-gateway_opt paths=source_relative \
bookstore.proto
# OpenAPI 스펙
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
👉 하나의 .proto
파일에서 gRPC, REST, OpenAPI를 모두 생성
gRPC 서버 구현
이제 실제 gRPC 서버를 구현해봅시다. 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"
"google.golang.org/grpc/reflection"
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)
reflection.Register(s)
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)
}
}
서버 실행 및 테스트
먼저 Go 모듈을 초기화하고 의존성을 정리합니다:
go mod init proto-to-mcp-tutorial && go mod tidy
go run cmd/server/main.go
정상적으로 실행되면 다음과 같은 출력이 보입니다:
gRPC server starting on :9090
REST server starting on :8080
이제 새로운 터미널을 열고(서버는 계속 실행 중이어야 합니다) REST API를 호출해봅니다.
curl -X GET http://localhost:8080/v1/books/book-1
결과는 예상대로 다음과 같습니다:
{
"bookId": "book-1",
"title": "The Go Programming Language",
"author": "Alan Donovan",
"pages": 380
}
👉 동일한 서비스 로직을 gRPC 또는 HTTP REST API 중 원하는 방식으로 호출할 수 있고, 데이터 모델과 동작은 완전히 동일합니다.
OpenAPI 스펙 확인
생성된 OpenAPI 명세도 확인해봅시다:
cat generated/openapi/openapi.yaml
여기에는 다음이 포함되어 있어야 합니다:
/v1/books/{bookId}
및/v1/books
경로 정의bookstore.v1.Book
스키마 정의- GET, POST 메서드 명세
결론
축하합니다! 🎉 이제 통합된 API 시스템을 완성했습니다.
- 단일 소스 오브 트루스(Single Source of Truth): 하나의 proto 파일로 전체 API 정의
- 이중 인터페이스(Dual Interface): 동일한 서비스를 gRPC와 REST 양쪽에서 접근 가능
- 자동 문서화(Automatic Documentation): OpenAPI 스펙이 자동으로 생성
- 싱크 문제 없음(No Sync Issues): 모든 결과물이 같은 소스에서 생성되므로 항상 일관성 유지
최종 아키텍처는 다음과 같은 형태를 띱니다:
Protobuf (.proto)
├── gRPC 서비스
├── REST API (gRPC Gateway)
└── OpenAPI 스펙

다음 단계
이제 Part 2로 넘어가면, 같은 패턴을 확장하여 AI 통합을 위한 MCP 서버 자동 생성을 다룰 예정입니다.
이번에 추가한 HTTP 어노테이션은 MCP 플러그인이 툴의 입력과 출력을 어떻게 구조화해야 하는지 이해하는 데 중요한 역할을 하게 됩니다.
참고 자료
Google API HTTP Annotations Documentation