🤖

Protocol Buffers MCP Serverを実装してLLMにスキーマ情報を提供してみた

に公開

こんにちは、PortalKey の植森です。

今回は、昨日ふと思い立って MCP Server を実装してみたのでそこで得られた知見について話します。

成果物

成果物はこちらです。

https://github.com/yuemori/protobuf-mcp-server

これはなに?

protobuf-mcp-server は、Protocol Buffers の MCP サーバーです。

PortalKey では Protocol Buffers + ConnectRPC を使って API を定義しています。
この話については過去の記事でも書いているので、そちらを参照してください。

https://zenn.dev/portalkeyinc/articles/18f6f3f54da55a

この MCP Server は以下の機能を提供します。

  • list_services: Protocol Buffers をコンパイルし、定義されている Service をリストで返します
  • get_schema: Protocol Buffers をコンパイルし、指定された文字列を含む Service, Message, Enum をリストで返します
  • onboarding: プロジェクトの初期セットアップと MCP Server の使い方を案内します
  • activate_project: MCP Server の Working Directory を設定します

実装しようと思ったきっかけ

今回 MCP Server を実装しようと思ったきっかけは、最近使い始めた Serena MCP Server です。

Serena MCP Server は、 LLM が LSP Server と連携するための MCP Server です。

https://github.com/oraios/serena

Serena を利用することで、エージェントは以下のような tool を利用することができます。

  • find_symbol のようなツールを利用することで、エージェントがシンボルレベルの検索が行えるようになる
  • find_referencing_symbols のようなツールを利用することで、エージェントがシンボルの参照関係を理解できるようになる
  • insert_after_symbol のようなツールを利用することで、エージェントが文字列置換ではないコード編集を行えるようになる

これによって、エージェントが少ないトークンでコードの検索・編集を行えるようになったり、より構造的なデータを理解することができるようになります。

Protocol Buffers でも Serena のようなことができるのではないか?

エージェントと連携して実装する中で、エージェントがどういった API やメッセージの型が存在するのかを推察で行うことが度々ありました。

Serena に触れたことで、この問題はそもそも構造的なデータである Protocol Buffers のスキーマをエージェントに提供することで解決できるのではないかというのが実装の切っ掛けです。

なお、この問題に対する別のアプローチとして Protocol Buffers を OpenAPI に変換するというアプローチや、 ApiDog のようなツールを利用するというアプローチもあります。
どちらも選択肢としてはありですが、Open API を利用するのは Protocol Buffers を直接利用するのに比べて遠回りですし、 ApiDog はプロジェクトでは導入していないためこのためだけに導入するのは大ナタ過ぎると感じました。

Protocol Buffers のスキーマを返却するぐらいの MCP Server を実装するのはそれほど難しくないと思ったので今回実装してみました。

Protocol Buffers MCP Server の設計

技術選定

まず、言語は Go を選択しました。

MCP Server の実装としてメジャーなのは uvxnpx といったツールランナーが利用できる Python や Node.js です。
にもかかわらず今回 Go を選択した理由としては、今回 MCP Server が提供する機能として Protocol Buffers のコンパイルが必要になるためです。

Protocol Buffers のコンパイルには protocbuf といったツールか、専用のライブラリが必要になります。
Go では幸い Buf Technologies 社が開発している protocompile というライブラリがあり、これを使うことで Protocol Buffers を高速にコンパイルすることができます。

また、 Go でも go rungo install を利用することで uvxnpx のような体験を提供することもできるため、導入の敷居は多少上がるかもしれませんが許容範囲だろうと判断しました。
今は提供していませんが Docker Image を提供したり、直接バイナリを配布すればより導入の敷居を下げることも可能です。

利用ライブラリ

アーキテクチャ

protobuf-mcp-server は以下のフローで動作します。

  • プロジェクトルート(Working Directory)を設定する
  • プロジェクトルートに存在する .protobuf-mcp.yml 設定ファイルを読み込む
  • 設定ファイルに基づいて .proto ファイルをスキャン・コンパイルする
  • コンパイル結果を MCP Server に提供する

これらのフローを実現するために、protobuf-mcp-server は以下のような tool を提供します。

1. activate_project

MCP Server の Working Directory を設定するツールです。
MCP Server はプロセスとして起動し、 Protocol Buffers のコンパイルはパスの解決が重要なため処理する対象のディレクトリを設定する必要があります。

また、 .protobuf-mcp.yml が存在しない場合、 onboarding ツールを実行することを促すプロンプトを返却することで LLM に対して使い方をガイドします。

2. onboarding

プロジェクトのセットアップを行うツールです。
実行すると、 .protobuf-mcp.yml 設定ファイルを作成し、 設定を行うプロンプトを返却します。

# .protobuf-mcp.yml の設定例
proto_files:
  - "proto/**/*.proto"
# protoc の --proto_path オプションに指定するパス
import_paths:
  - "."
  - "proto"
  - "third_party"

プロンプトはプロジェクトの .proto ファイルを検索し、 .protobuf-mcp.yml 設定ファイルを編集するような内容になっています。
ただ、自分で指示を出したときは上手く設定が出来ず、結局手動で編集したのでここは改良の余地があるでしょう。

3. list_services

proto ファイル内のすべてのサービスを一覧表示するツールです。
各サービスの概要情報(パッケージ名、メソッド数など)を提供し、 API 全体像の把握を支援します。

protocompile によるコンパイルがかなり高速だったため、現状はコンパイル結果をキャッシュせずに毎回コンパイルを行っています。

以下は返却されるレスポンスの例です。

{
  "success": true,
  "message": "Found 24 services",
  "services": [
    {
      "name": "ChannelService",
      "full_name": ".portalkey.v1.ChannelService",
      "methods": [
        {
          "name": "Create",
          "input_type": "ChannelServiceCreateRequest",
          "output_type": "ChannelServiceCreateResponse",
          "client_streaming": false,
          "server_streaming": false,
          "description": "ChannelServiceCreate creates a new channel."
        },
        {
          "name": "Update",
          "input_type": "ChannelServiceUpdateRequest",
          "output_type": "ChannelServiceUpdateResponse",
          "client_streaming": false,
          "server_streaming": false,
          "description": "GetChannel gets a channel by ID.\n rpc GetChannel(GetChannelRequest) returns (GetChannelResponse);\n ListChannels lists all channels.\n rpc ListChannels(ListChannelsRequest) returns (ListChannelsResponse);\n ChannelServiceUpdate updates a channel."
        },
        {
          "name": "Delete",
          "input_type": "ChannelServiceDeleteRequest",
          "output_type": "ChannelServiceDeleteResponse",
          "client_streaming": false,
          "server_streaming": false,
          "description": "ChannelServiceDelete deletes a channel."
        }
      ],
      "file": "portalkey/v1/channel_service.proto",
      "package": "portalkey.v1",
      "description": "ChannelService is the service for managing channels."
    },
    ...
  ],
  "count": 24
}

4. get_schema

詳細なスキーマ情報を取得するメインツールです。
名前で部分一致検索を行い、 Service, Message, Enum を返却します。

ここもツールの呼び出しごとにコンパイルを行っています。

以下は返却されるレスポンスの例です。

{
  "success": true,
  "message": "Retrieved schema information: 15 messages, 2 services, 0 enums",
  "schema": {
    "messages": [
      {
        "name": "UserProfileUpdatedPayload",
        "full_name": "portalkey.v1.UserProfileUpdatedPayload",
        "fields": [
          {
            "name": "user_profile",
            "number": 1,
            "type": "message",
            "optional": true,
            "repeated": false,
            "description": "ユーザープロフィール"
          }
        ],
        "file": "portalkey/v1/message_payload.proto",
        "package": "portalkey.v1",
        "description": "Gateway がクライアントに対し、ユーザプロフィール更新を通知するメッセージ"
      },
      ...
    ],
    "services": [
      {
        "name": "UserService",
        "full_name": "portalkey.v1.UserService",
        "methods": [
          {
            "name": "Get",
            "input_type": "portalkey.v1.UserServiceGetRequest",
            "output_type": "portalkey.v1.UserServiceGetResponse",
            "client_streaming": false,
            "server_streaming": false,
            "description": "Get は自身のユーザ情報を取得します"
          },
          {
            "name": "GetWorkspaces",
            "input_type": "portalkey.v1.UserServiceGetWorkspacesRequest",
            "output_type": "portalkey.v1.UserServiceGetWorkspacesResponse",
            "client_streaming": false,
            "server_streaming": false,
            "description": "GetWorkspaces はユーザーの所属しているワークスペース一覧を取得します"
          },
          {
            "name": "GetWorkspaceMember",
            "input_type": "portalkey.v1.UserServiceGetWorkspaceMemberRequest",
            "output_type": "portalkey.v1.UserServiceGetWorkspaceMemberResponse",
            "client_streaming": false,
            "server_streaming": false,
            "description": "GetWorkspaceMember はワークスペースメンバー情報を取得します"
          }
        ],
        "file": "portalkey/v1/user_service.proto",
        "package": "portalkey.v1",
        "description": "The UserService service provides methods for users."
      },
      ...
    ],
    "enums": []
  },
  "count": 17
}

実装のポイント

1. protocompile ライブラリの活用

Protocol Buffers のコンパイルには、Buf Technologies 社が開発している protocompile ライブラリを採用しています。

compiler := protocompile.Compiler{
    Resolver: &protocompile.SourceResolver{
        ImportPaths: []string{"."},
    },
}

linkedFiles, err := compiler.Compile(context.Background(), "example.proto", "api.proto")

Compiler の interface は protoc に近く、 protoc を利用したことがあれば分かりやすいと思います。
ImportPaths には protoc の --proto_path オプションに指定するパスを指定することで、外部ファイルを読み込むことができます。

2. mark3labs/mcp-go ライブラリの活用

MCP Server の実装には、mark3labs 社が開発している mcp-go ライブラリを採用しています。

MCP Server の機能としてツール一覧を返す必要があります。
そのため、サーバの初期化後に Add メソッドでツールを追加します。

mcpServer := mcp.NewServer()

mcpServer.Add(mcp.Tool{
    Name: "list_services",
    Description: "List all services",
    Handler: listServices,
})

mcpServer.Add(mcp.Tool{
    Name: "get_schema",
    Description: "Get schema information",
    Handler: getSchema,
})

mcpServer.Add(mcp.Tool{
    Name: "onboarding",
    Description: "Onboarding",
    Handler: onboarding,
})

ここで Add しておくことで、 LLM が MCP Server から利用可能なツール一覧を取得できるようになります。

3. MCP Server のテスト

MCP Server 自体は JSON-RPC を利用するため通信が必要ですが、テスト実装の場合には mark3labs/mcp-go ライブラリが提供する NewInProcessClient を利用することでテストが簡単にかけます。

// MCP Server の初期化
s := server.NewMCPServer(
    "protobuf-mcp-server",
    "1.0.0",
    server.WithToolCapabilities(true),
)

// MCP Server のツールを追加
s.Add(mcp.Tool{
    Name: "list_services",
    Description: "List all services",
    Handler: listServices,
})

// MCP Server の起動
mcpClient, err := client.NewInProcessClient(server.server)
if err != nil {
    t.Fatalf("Failed to create client: %v", err)
}

ctx := context.Background()

// MCP Client の Initialize
_, err = mcpClient.Initialize(ctx, mcp.InitializeRequest{
    Params: mcp.InitializeParams{
        ProtocolVersion: mcp.LATEST_PROTOCOL_VERSION,
        ClientInfo: mcp.Implementation{
            Name:    "test-client",
            Version: "1.0.0",
        },
        Capabilities: mcp.ClientCapabilities{},
    },
})
if err != nil {
    t.Fatalf("Failed to initialize: %v", err)
}

// Tool の呼び出し
result, err := mcpClient.CallTool(ctx, mcp.CallToolRequest{
    Params: mcp.CallToolParams{
        Name: "activate_project",
        Arguments: map[string]interface{}{
            "project_path": tempDir,
        },
    },
})
if err != nil {
    t.Fatalf("Failed to call activate_project: %v", err)
}

if result.IsError {
    if textContent, ok := mcp.AsTextContent(result.Content[0]); ok {
        t.Fatalf("activate_project returned error: %s", textContent.Text)
    } else {
        t.Fatalf("activate_project returned error")
    }
}

4. MCP Server のデバッグ

MCP Server のデバッグには2種類の方法があります。

1つはビルドしたサーバを起動し、デバッグする方法です。
MCP Server は起動すると標準入力で JSON-RPC のリクエストを受け付けるので、これを利用することで簡易的なデバッグを行えます。

$ echo '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"activate_project","arguments":{"project_path":"."}}}' | protobuf-mcp-server server
2025/09/24 07:29:30 Starting Protobuf MCP Server (using mcp-go)...
{"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"{\"success\":true,\"message\":\"Project activated successfully\",\"project_root\":\"/home/xxx/ghq/src/github.com/portalkey/portalkey-server\",\"proto_files\":28,\"services\":24,\"messages\":288,\"enums\":16}"}]}}

ただ、この方法は複雑なデバッグをするには向いてないのと、正しいレスポンスを返せているかなどのデバッグにはなりません。
そこで、 @modelcontextprotocol/inspector を利用することで MCP Server のデバッグを UI から行うことが出来ます。

起動すると下記のような画面になり、コマンドの指定をして Connect ボタンを押すことで MCP Server と接続することができます。

起動に成功すると以下のようにデバッグ用の UI が表示され、 Swagger-UI のようにデバッグを行うことが出来ます。

コーディングエージェントを利用した実装の振り返り

今回の実装時間は大体10時間程度で、基礎設計は Claude Desktop を利用し、コーディングのほとんどは Claude Code および Cursor を利用しました。
※途中で Claude Code の利用可能範囲を超えてしまったのでそこから Cursor を利用しました。

途中まではいい感じで書けていたと思いましたが、以下のような問題にあたってそこからは自分で実装したり、修正させたりしていました。

  • mark3labs/mcp-go ライブラリを使わずに実装していたためレスポンス形式が間違っていた。途中で気づいて明示的に指示を出して修正させた
  • 当初はテストで NewInProcessClient を利用せずにビルドしたバイナリを起動するテストが作成されていたため複雑になっていた。途中で別の方法を検討するように指示を出したら NewInProcessClient を利用するコードに自律的に修正した
  • protocompile ライブラリの利用が正しくなく、ImportPaths 周りの実装が誤っていた。これは proto ファイルのインポートの仕組みが分かりにくく解決できなかったので自分でデバッグして実装し直した

おそらく Protocol Buffers のコンパイル周りの実装例や protocompile の利用事例が少なかったこと、 また MCP Server の実装も新しい技術であるため思ったよりも時間がかかりましたが、指示の出し方次第ではより早く実装を終えることが出来ると思います。
自分は以前 protobuf plugin や protoreflect 、 protodescriptor 周りを扱ったことがあるので知識があったというのも大きいですが。

余談ですが今回の実装で $60 ぐらい Cursor のクレジットを消費しました。つらい。
手で実装した時のことを考えると自分で開発するよりも効率的ですが、この調子で使っているとすぐにプラン上限まで使い切ってしまうのでご利用は計画的に。

まとめ

以下は実際に Cursor に protobuf-mcp-server 経由でプロジェクトの Protocol Buffers の情報を取得した様子です。

ちゃんと取得できているのがわかりますね。

MCP Server の実装をすることで Project Specific な情報をエージェントに提供することができるようになり、日々の開発が便利になる部分もありそうなので今後も活かせる場面を検討していきたいと思います。

実装したコードは GitHub で公開していますので、参考にしていただければと思います。
https://github.com/yuemori/protobuf-mcp-server

それではまた。

PortalKey Tech Blog

Discussion