Protobuf를 이용한 MCP 서버 구축 (1부) – Protobuf에서 REST API로
Charlie Zhang
2025년 9월 3일
이 블로그 시리즈에서는 유용한 도구들로 가득 찬 MCP (Model Context Protocol) 서버를 구축하는 방법을 보여드립니다. 처음부터 시작하는 대신, 기존의 프로토콜 버퍼와 Google의 gRPC 트랜스코딩을 활용할 것입니다. 맞춤형 protoc (프로토콜 버퍼 컴파일러) 플러그인을 생성하여 MCP 서버를 자동으로 생성할 수 있습니다. 이 통합된 접근 방식을 통해 gRPC 서비스, OpenAPI 사양, REST API 및 MCP 서버를 모두 동일한 소스에서 생성할 수 있습니다.
이 블로그 시리즈는 4개의 기사로 구성되어 있습니다:
- 1부: (본 기사) gRPC 트랜스코딩을 사용하여 Protobuf에서 REST API 생성하기
- 2부: Protoc 플러그인으로 MCP 서버 생성 자동화하기
- 3부: Proto 주석으로 AI 상호작용 향상시키기
- 4부: 실제로 MCP 도구를 실행하면서 얻은 통찰력
구축할 내용
이 블로그를 마치면 다음을 갖게 됩니다:
- gRPC와 REST API를 모두 생성하는 단일 프로토콜 버퍼 정의
- 자동 OpenAPI 사양 생성
- MCP 서버 생성을 위한 기반 (2부에서 다룰 예정)
이 기사에 언급된 모든 코드는 GitHub 저장소 zhangcz828/proto-to-mcp-tutorial에서 찾을 수 있습니다.
전제 조건
시작하기 전에 다음을 갖추고 있는지 확인하세요:
- Go 1.19+ 설치
- Protocol Buffer 컴파일러 (protoc) 설치
- gRPC 및 REST API에 대한 기본적인 지식
우리가 해결하는 문제
우리의 아키텍처는 gRPC를 내부 서비스에 사용하고, REST API를 외부 클라이언트에 사용하며, OpenAPI 사양은 별도의 수동으로 유지 관리되는 저장소에 보관하는 방식으로 단순하게 시작했습니다. 시스템이 성장함에 따라 이러한 분리는 골칫거리가 되었습니다. 변경 사항이 생길 때마다 서비스 코드, REST 게이트웨이, 그리고 문서를 여러 곳에서 업데이트해야 했습니다. 이로 인해 불일치, 누락된 업데이트, 많은 반복 작업이 발생했습니다.
gRPC 트랜스코딩이란 무엇인가요?
gRPC 트랜스코딩을 사용하면 gRPC 서비스를 REST API로 노출할 수 있으므로, 클라이언트는 익숙한 HTTP 호출을 사용할 수 있고 백엔드 로직은 gRPC 상태로 유지됩니다. 이는 Google API 어노테이션으로 알려진 특수 HTTP 어노테이션을 프로토콜 버퍼 정의에 추가하여 가능해집니다. 이 어노테이션은 경로, 경로 변수, 요청 본문을 포함하여 HTTP 요청이 gRPC 메서드에 어떻게 매핑되는지 정의합니다.
Google API 어노테이션은 어떻게 작동하나요?
Google API 어노테이션은 gRPC 서비스가 REST API에 매핑되는 방식을 정의하는 프로토콜 버퍼 확장 집합입니다. 원래 Google이 자체 API를 위해 개발했으며, gRPC-to-REST 트랜스코딩의 표준이 되었습니다.
주요 구성 요소:
google/api/annotations.proto: RPC 메서드의 HTTP 옵션에 대한 확장을 제공하는 파일입니다.google.api.http:google/api/http.proto에 정의된 옵션으로, gRPC 메서드의 HTTP 매핑을 지정합니다.- 경로 변수: HTTP 경로의 플레이스홀더 (예:
{book_id})로, 요청 메시지의 필드에 자동으로 매핑됩니다. - 본문 매핑: HTTP 옵션의
body필드는 요청 메시지의 어느 부분이 HTTP 요청 본문으로 사용되는지 결정합니다.
요약하자면, Google API 어노테이션은 API를 쉽게 유지 관리하고 확장할 수 있도록 하여 모든 것을 동기화 상태로 유지하고 수동 작업을 줄여줍니다.
프로젝트 설정
새 프로젝트를 생성하는 것부터 시작하겠습니다:
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 protoc 플러그인을 설치합니다:
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
다음으로 HTTP 어노테이션에 필요한 Google API 정의를 다운로드해야 합니다:
# Google APIs용 디렉토리 생성
mkdir -p googleapis/google/api
# 필요한 proto 파일 다운로드
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
프로토콜 버퍼 정의 생성
서점 서비스 정의가 포함된 proto/bookstore.proto 파일을 생성합니다:
Protocol Buffers
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}"**는 book_id URL 매개변수가 GetBookRequest 메시지의 해당 필드에 자동으로 매핑되는 GET 엔드포인트를 생성하도록 제너레이터에 지시합니다. 마찬가지로, **body: "book"**은 book 필드 전체가 CreateBook 엔드포인트의 HTTP 요청 본문이 되도록 지정합니다.
이 접근 방식의 특히 우아한 점은 동일한 어노테이션이 여러 제너레이터에서 작동한다는 것입니다. gRPC 게이트웨이는 이를 사용하여 REST 엔드포인트를 생성하고, OpenAPI 제너레이터는 이를 사용하여 올바른 경로 매개변수와 요청 본문 스키마를 생성하며, 2부에서 보게 될 것처럼, 우리의 MCP 플러그인도 이를 사용하여 도구 입력을 구조화하는 방법을 이해할 수 있습니다.
코드 생성
어노테이션이 포함된 proto 파일이 준비되면 빌드 프로세스는 놀라울 정도로 간단해집니다:
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 게이트웨이 생성
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
각 protoc 명령은 동일한 bookstore.proto 파일을 읽지만, 사용된 플러그인에 따라 다른 출력을 생성합니다. HTTP 어노테이션은 각 제너레이터가 적절한 인터페이스를 생성하는 데 필요한 라우팅 정보를 제공합니다.
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) {
switch {
case req.Book == nil:
return nil, fmt.Errorf("book is required")
case req.Book.Title == "":
return nil, fmt.Errorf("book title is required")
case req.Book.Author == "":
return nil, fmt.Errorf("book author is required")
case req.Book.Pages <= 0:
return nil, fmt.Errorf("book pages must be greater than zero")
}
book := req.Book
fmt.Println("Creating book:", book.Author, book.Title, book.Pages)
if book.BookId == "" {
book.BookId = fmt.Sprintf("book-%d", len(s.books)+1)
}
s.books[book.BookId] = book
return book, nil
}
func main() {
// 일부 샘플 데이터로 서버 초기화
srv := &server{
books: map[string]*pb.Book{
"book-1": {
BookId: "book-1",
Title: "The Go Programming Language",
Author: "Alan Donovan",
Pages: 380,
},
},
}
// gRPC 서버 시작
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)
}
}()
// REST 게이트웨이 시작
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
테스트를 위해 새 터미널을 엽니다 (서버는 계속 실행 중).
이제 클라이언트는 gRPC 또는 HTTP를 통해 완벽하게 동일한 서비스 로직과 상호작용할 수 있으며, 데이터 모델과 동작은 동일합니다. 예를 들어, 책을 가져오는 curl 요청은 다음과 같습니다:
curl -X GET http://localhost:8080/v1/books/book-1
그리고 예상대로 정확하게 반환합니다:
{
"bookId": "book-1",
"title": "The Go Programming Language",
"author": "Alan Donovan",
"pages": 380
}
OpenAPI 생성 확인
생성된 OpenAPI 사양을 확인합니다:
cat generated/openapi/openapi.yaml
REST 엔드포인트와 일치하는 완벽한 OpenAPI 사양이 표시되어야 합니다! 다음을 확인하세요:
/v1/books/{bookId}및/v1/books에 대한 경로 정의bookstore.v1.Book에 대한 스키마 정의- HTTP 메서드 사양 (
get,post)
결론
축하합니다! 방금 다음과 같은 통합 API 시스템을 구축했습니다:
- 단일 정보 소스: 하나의 proto 파일이 전체 API를 정의합니다.
- 이중 인터페이스: 서비스는 gRPC와 REST를 통해 모두 액세스할 수 있습니다.
- 자동 문서화: OpenAPI 사양이 자동으로 생성됩니다.
- 동기화 문제 없음: 모든 것이 동일한 소스에서 생성되므로 동기화 상태가 유지됩니다.
현재 아키텍처는 다음과 같습니다:

다음 단계
이 기반은 2부에서 AI 통합을 위한 MCP 서버를 생성하기 위해 이 동일한 패턴을 확장하는 데 완벽하게 준비되어 있습니다. 여러분이 추가한 HTTP 어노테이션은 MCP 플러그인이 도구 입력 및 출력을 구조화하는 방법을 이해하는 데 도움이 될 것입니다.

