StdioTransport を使ったクロスプラットフォームな MCP Server を Go で実装して、Cline から呼び出してみる

記事タイトルとURLをコピーする

こんにちは。自称ソフトウェアエンジニアの橋本 (@hassaku_63)です。

最近のメインの仕事ではログデータ基盤としての OpenSearch (Service) の検証に沼っています。めちゃくちゃ便利だし非常に応用も広い一方で、一般ユーザー/管理者として押さえるべき概念モデルの獲得や、プロダクションユースに耐えうるサイジングやセキュリティ構成の問題、そしてインフラの稼働コスト、その後の運用など、相応に「高価」なサービスだなと実感します。約半年(?)前に知識まっさらの状態から始まり、今はやっと少しは要領を飲みこめてきたかな...?というところでしょうか。まあ、一度構築してしまえばその基盤の上で色々な自由が待っていますので、そこの試行錯誤のスピード感を出すことで価値を提示していければいいかなと思っています。

さて、今回は今話題の MCP Server の構築をやってみようと思います。ぶっちゃけ本業で割と手一杯の感はあるんですけど、さすがに今これをキャッチアップしないという選択は(SWE としての軸を自称したい私としては)ナシなので、日曜プログラミングの一環として触ってみています。周辺の実務に投入するイメージもいくつかあるので、それでそのうち面白いことができれば・示せたらいいかなぁと思います。

想定

公式から SDK が出ていますが、今回はそれらを使わず Go で実装を行いました。これはメインのユースケースを鑑みて TypeScript, Python よりも Go の方がマッチしていると判断したからです。

メインは社内のエンジニア職に配布するツールとしての想定ですが、私の職務上エンジニア以外の職種の方も含めて配布対象になる可能性は大いにありそうでした。開発者がやるような言語のランタイムや依存関係の導入(あるいは Docker コンテナの準備)などの導入作業を、MCP Server の利用者側に課すのはナンセンスです*1。その点シングルバイナリで成果物がすぐ使える Go は、配布相手に対して負担させる初期導入タスクのかなりの部分をカットしてくれるため、今回の私の想定には適していると考えました。

また、不特定多数に対する配布は今回想定していません。

実装

こちら。

github.com

とりあえず初期バージョンである v0.1.0 を GitHub Release に配置したので、すぐに試していただくことが可能です...が、1点非常に重要なお断りがあります。

MCP Server は、その仕組み上やろうと思えばそれこそなんでもできてしまう仕組みです。この実装例ではバイナリの作成と配布を完全に自動化しており、基本的に故意に悪意あるコードを作者(私)がコミット外から混入させる余地はありません。が、しかし、第三者が作ったバイナリ形式の MCP Server を無遠慮に入れてしまうのは、現時点では非常に危ないことだと考えます。

基本的に Tool を用いたアクションはユーザー承認が必要とされる仕組みになっていますが、それもアプリケーションの実装次第ですし、また不用意に Auto-Approve してしまい歯止めが効かなかった、ということもありえます。

特に怖いのは、MCP Client 側に公開される ListTools などの結果で嘘をつき、裏側の Tool 実装ではホストマシンの情報を収集して攻撃者のサーバーに情報を送信していた...といったケースでしょうか。MCP のプロトコル上で Client 側に公開される MCP Server の Tool 情報は、その Tool の名前・説明・スキーマ程度です。よって、Server の実装者は Client 側に意図的に真実を申告せず、無害を装うことがいくらでもできてしまいます。

読者の方には、基本的にはバイナリをダウンロードして試す行為はおすすめしません(特に当社の所属ではない方。自分で公開しておいてアレですが)。もし試してみる場合は自己責任でお願いします

使い方

すぐ上でも述べましたが非常に重要なことなので再掲します。基本的にここで書く内容を実際にお試しすることは推奨しません。やる場合は完全に自己責任でお願いします。当方ではこのソースコードやその配布物の利用に関して発生するいかなる不都合についても責任を負いかねます

環境によってはダウンロードしたバイナリの実行が OS によって保護されている場合があります。その場合は当該バイナリの実行を明示的に許可する必要があります(例えば、MacOS なら Gatekeeper)

作り手と使い手の間に十分に信頼がある、主に社内のメンバーに向けて、導入の利便性のためバイナリ形式での配布をする・・・という使い勝手を実現するサンプルコードとして今回の実装を提示しています。実際に使っていただくためのものではありません。

Releases からお使いのプラットフォームに応じた tar をダウンロードして、解凍してバイナリを入手します。これをいずれかのパスに配置し、Cline の MCP Server の設定ファイル cline_mcp_settings.json を開きます。以下の要領で設定します。

{
  "mcpServers": {
    "mcp-server-start": {
      "name": "mcp-server-start",
      "description": "this is a test server",
      "command": "${ABSOLUTE_PATH_TO_YOUR_DOWNLOAD}",
      "args": []
    }
  }
}

設定が通れば以下のような画面が出るはずです。name というパラメータを受け取るだけの、 greeting というツールが登録されているはずです。

MCP Server 設定後の画面

試しに、次のようなプロンプトを与えてみます。

mcp-server-start という mcp server の、 greeting というツールを実行してみてほしい

greeting は name というパラメータを受け付ける。ここでは "hassaku" という値を使って。

すると Cline が次のように greeting ツールの利用を尋ねてきます。

greeting ツールの使用をリクエストした後の承認画面

タスクが実行され、実装側で意図した文字列が返ってくることがわかります。

ツールの使用結果の表示と、タスクの完了

少しだけ実装の解説

Go における MCP の実装は、すでに MIT ライセンスで配布されている方がおられました。mark3labs/mcp-go というモジュールです。執筆時点の実装を軽く眺めてみましたが、特にあやしい挙動はなさそうに見えましたので、今回これを使用しています。

internal/tool/greeting 以下は MCP Server の実装の詳細によらないロジック部分の実装になります。今回は MCP Server としてのガワを被せていますが、別に単なる CLI ツールとして実行してもいいし、ECS Service/Task に乗せて業務フローの自動化に役立ててもよいわけです。ロジック本体は MCP とは無関係ですので、その実装は MCP の文脈を含まないように切り出しています。

github.com

cmd/cli/main.go は Transport に Stdio を決め打ちして MCP Server を起動するものです*2

github.com

だいたい読んで字の如くなので解説することもないのですが、internal で切り出したロジックと、MCP Server のハンドラとして求められるインタフェースをアダプトしつつ Server 構築する感じの役割を持っています。このへんは CLI 引数としてトランスポートを選べるように切り出してみるのも、より汎用的な使い方ができ面白いと思います。 --transport stdio とすれば stdio で起動する、というわけです。このような仕様にした場合は Cline の設定ファイルも args に然るべきパラメータを指定するように変更が必要です。

また、実行環境ごとのビルドには GoReleaser を使っています。当初は matrix を使ってビルドを構成していたんですが、自分の意図しない動きをしたりして沼りそうだったので大人しく優れた既製品に頼ることにしました。GitHub Actions にも対応していますので、非常に便利です。

まとめ

今回は Tool の実装だけですが、MCP には他にも Resources, Prompts, Sampling など異なるモチベーションを持った機能があります。この記事では触れていませんが、いずれ触れたいと思います。

興味があれば Concepts を一読してみるとよいと思います。

modelcontextprotocol.io

これらの概念は、いきなり公式ドキュメントを読んでもピンとこない可能性があります。少なくとも私はそうでした。

その場合は「ユーザー」「アプリケーション」「MCP Client」「MCP Server」「LLM」これらの登場人物の関係性を先に押さえていくとよいと思います。ポイントは次の3つです。

  • 通信の手段である Transport の存在は "プロトロル上で意味を持つやり取り" の理解とは分離できること*3
  • LLM と MCP Server はアプリケーション(とそのアプリケーションが腹持ちする MCP Client)を介して間接的にやりとりするのが基本ということ*4
  • 最初は最も典型的と思われる "Tool" の場合に絞ってやり取りをイメージすること

このへんはいずれ解説記事出せればと思います。

また、他社さんの記事にはなりますが、プロトロルの理解に関しては非常に良くまとめていただいてる記事がありましたので、この場を借りてご紹介させていただきます*5

dev.classmethod.jp

スライド p.10 以降のプロトロル上の概念の整理が非常にわかりやすいです。大変勉強になりました。ありがとうございます。

さいごに

途中でも触れたように、MCP Server はマシンが環境を「好き勝手」できてしまうがゆえに、本質的に危うい仕組みであると考えています*6。個人的には MCP Server (特にローカル)向けの Sandbox 環境のような概念が提唱され、仕組み化されることを期待したいところです。

MCP 本体の方でも Go SDK を公式にサポートする、という動きがあるようです。駆け出し Gopher としてはこちらの動きも引き続きウォッチしていきたいところです。

github.com

*1:エンジニア職であっても、誰もが皆特定の言語ランタイムや Docker に用があるわけではありませんしね

*2:ディレクトリ階層は cli よりは server のような命名の方が良かったかもしれません

*3:TCP/IP のスタックに例えるなら、HTTP というプロトロルで会話するための語彙を理解するだけなら、通信方法に関する詳細である L4/L3 などの下位層を意識する必要はない、というのと同じです

*4:AWS の API とやり取りするアプリケーションを想像してください。アプリケーション側では AWS API の Client である SDK を腹持ちするような実装を通常しますよね。喋っているプロトロルが違うだけで、これと要領は同じです

*5:本来同業の他社様の記事を借用するのは気が引けるところですが、本記事の主題ではない参考文献への参照ということで容赦いただけると

*6:とはいえ、MCP に限らずあらゆるサードパーティー製のツールは同じことが言えるわけで、これは「これまでだって、ずっとそうだったでしょ?」な話です。ただ、ローカルマシンでどのようなプロセスをいつどのように実行するのか、そのコントロールを部分的にリモート = LLM の非決定的な制御に委ねることになるがゆえ、緩みやすい部分も出てくるよねというのが私なりの懸念です

橋本 拓弥(記事一覧)

マネージドサービス部

内製開発中心にやってます。普段はサーバーレス関連や CDK を触ることが多いです