GoでMCPサーバーを書く公式SDKの使い方
公式Go SDK「go-sdk」でツールを定義し、stdio経由でClaude Codeに接続するまでの手順と、実装でつまずきやすい落とし穴をコード付きで解説します。
GoでMCPサーバーを書くと何が手に入るか
MCP(Model Context Protocol)のGo実装はgithub.com/modelcontextprotocol/go-sdkというモジュール名で公式配布されており、TypeScript・Python・C#・Rust・Rubyと並ぶTier1(最上位の完成度・保守コミットメント)に分類されています。GoのgoroutineとcontextはMCPの非同期なリクエスト処理・キャンセル伝播とそもそも相性がよく、既にGoでバックエンドを書いているチームなら新しい非同期モデルを学び直さずにサーバーを組み込めます。
SDKのバージョンとサポートするMCP仕様の対応は次のとおりです。
| Go SDKバージョン | 最新対応MCP仕様 | 対応する全MCP仕様 |
|---|---|---|
| v1.7.0以降 | 最新対応MCP仕様2026-07-28 | 対応する全MCP仕様2026-07-28、2025-11-25(クライアント側OAuthのみ実験的サポート)、2025-06-18、2025-03-26、2024-11-05 |
| v1.4.0〜v1.6.1 | 最新対応MCP仕様2025-11-25(クライアント側OAuthのみ実験的サポート) | 対応する全MCP仕様2025-11-25、2025-06-18、2025-03-26、2024-11-05 |
| v1.2.0〜v1.3.1 | 最新対応MCP仕様2025-11-25(部分対応:クライアント側OAuthとツール付きsamplingは未提供) | 対応する全MCP仕様2025-11-25、2025-06-18、2025-03-26、2024-11-05 |
| v1.0.0〜v1.1.0 | 最新対応MCP仕様2025-06-18 | 対応する全MCP仕様2025-06-18、2025-03-26、2024-11-05 |
2026-07-28仕様を使うにはv1.7.0以降が必須です。この仕様改訂でroots・sampling・logging機能がSEP-2577により非推奨化されましたが、SDKは移行期間として最低12か月は引き続きサポートします。既存コードをすぐに書き換える必要はありません。
MCPの仕組み自体や他言語SDKとの比較はMCP実用ガイド、TypeScript・PythonでのMCPサーバー自作はMCPサーバー自作ガイドが扱っています。本記事はGo SDK(go-sdk)固有の実装手順に絞ります。GoアプリそのものをClaude Codeで開発する流れはClaude CodeでGoアプリを開発する手順にあります。
導入からツール定義まで — 3ステップ
ステップ1: モジュールを追加する
go getでSDKをモジュールに追加します。SDKの新しいリリースはGoのサポート対象バージョン(Goのリリースポリシー上メンテナンスされている最新2バージョン)のみを対象とするため、古いGoでは新しいリリースが対象外になります。事前にGoのバージョンを最新化しておくと確実です。パッケージは4つに分かれており、通常のサーバー・クライアント実装にはmcpパッケージだけで足ります。
go get github.com/modelcontextprotocol/go-sdk/mcp独自トランスポートを実装する場合はjsonrpcパッケージ、OAuthを扱う場合はauthパッケージ(認可の基本プリミティブ)とoauthexパッケージ(ProtectedResourceMetadataなどOAuthプロトコルの拡張)も必要になります。用途に応じて追加してください。
ステップ2: サーバーとツールを定義する
mcp.NewServerでサーバーを作り、mcp.AddToolでツールを登録します。入力と出力はGoの構造体で定義し、jsonschemaタグに書いた説明がそのままツールのJSON Schemaに反映されます。
package main
import (
"context"
"log"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
type Input struct {
Name string `json:"name" jsonschema:"the name of the person to greet"`
}
type Output struct {
Greeting string `json:"greeting" jsonschema:"the greeting to tell to the user"`
}
func SayHi(ctx context.Context, req *mcp.CallToolRequest, input Input) (
*mcp.CallToolResult,
Output,
error,
) {
return nil, Output{Greeting: "Hi " + input.Name}, nil
}
func main() {
server := mcp.NewServer(&mcp.Implementation{Name: "greeter", Version: "v1.0.0"}, nil)
mcp.AddTool(server, &mcp.Tool{Name: "greet", Description: "say hi"}, SayHi)
if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
log.Fatal(err)
}
}ツール関数のシグネチャはfunc(ctx, *mcp.CallToolRequest, Input) (*mcp.CallToolResult, Output, error)という固定形です。戻り値の*mcp.CallToolResultはnilのままで構いません。手動でContentを組み立てたいときだけ自分で埋めます。server.Runはmcp.StdioTransportを渡すとstdin/stdoutで待ち受け、クライアントが切断するまでブロックします。
このバイナリをgo buildしてから動かせば、それだけで1つのMCPサーバーです。
go build -o mytool .mcp.AddToolとは別に、Server.AddToolというメソッドもあります。両者の違いは、仕様どおりのツールを作るための下ごしらえをどちらが引き受けるかです。
Server.AddToolとmcp.AddToolの分担
Server.AddTool
ツールとハンドラーを登録するだけです。入出力スキーマの用意、引数の検証、Goの値への展開、構造化出力のJSON化、出力の検証、エラーをIsError付きの結果に詰める処理は、すべてハンドラー側で実装します。
mcp.AddTool
In型とOut型からスキーマを推論し、引数の検証、Inへの展開、OutのStructuredContentとContentへの書き込み、出力の検証までを自動で行います。普通のerrorを返すとIsError: trueの結果になります。
推論されたスキーマに制約を足したいときは、Tool.InputSchemaを明示します。jsonschema.Forで型からスキーマを作り、フィールドごとに上下限などを書き換えてから渡します。
import "github.com/google/jsonschema-go/jsonschema"
type ForecastInput struct {
Days int `json:"days" jsonschema:"number of days to forecast"`
}
in, err := jsonschema.For[ForecastInput](nil)
if err != nil {
log.Fatal(err)
}
days := in.Properties["days"]
days.Minimum = jsonschema.Ptr(0.0)
days.Maximum = jsonschema.Ptr(10.0)
mcp.AddTool(server, &mcp.Tool{Name: "forecast", InputSchema: in}, Forecast)範囲外のdaysは、ハンドラーに届く前にスキーマ検証ではじかれます。型ごとにスキーマを差し替えるForOptions.TypeSchemasの例は、SDKリポジトリのexamples/server/toolschemasにあります。
ステップ3: Claude Codeに接続する
ビルドしたバイナリをclaude mcp addで登録します。ローカルバイナリなので、サーバー名のあとに--を挟んで実行ファイルをそのまま渡します。
claude mcp add --transport stdio mytool -- ./mytoolClaude Code v2.1.289のclaude mcp add --helpでは、--transport(-t)の選択肢がstdio・sse・httpで、省略するとstdioになります。--scope(-s)の既定はlocalです。次の出力は抜粋です。
claude mcp add --help # Add stdio server with subprocess flags:
claude mcp add my-server -- my-command --some-flag arg1
-s, --scope <scope> Configuration scope (local, user, or project)
(default: "local")
-t, --transport <transport> Transport type (stdio, sse, http). Defaults to
stdio if not specified.Goのバイナリがフラグを受け取るなら--の右側にそのまま並べます。環境変数は-e KEY=valueで渡せます。登録後はclaude mcp listで認識されているか確認し、Claude Codeのセッション内でgreetツールを呼び出せます。設定の保存スコープ(local / project / user)やOAuth連携の詳細はClaude Code MCPサーバー完全ガイドにまとめています。
クライアントも公式SDKで自作できる
サーバー単体だけでなく、mcp.NewClientで任意のMCPサーバーに接続するクライアントも同じSDKで書けます。他言語製のサーバーを含め、標準準拠のMCPサーバーであれば言語を問わず呼び出せます。
package main
import (
"context"
"log"
"os/exec"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
func main() {
ctx := context.Background()
client := mcp.NewClient(&mcp.Implementation{Name: "mcp-client", Version: "v1.0.0"}, nil)
transport := &mcp.CommandTransport{Command: exec.Command("myserver")}
session, err := client.Connect(ctx, transport, nil)
if err != nil {
log.Fatal(err)
}
defer session.Close()
params := &mcp.CallToolParams{
Name: "greet",
Arguments: map[string]any{"name": "you"},
}
res, err := session.CallTool(ctx, params)
if err != nil {
log.Fatalf("CallTool failed: %v", err)
}
if res.IsError {
log.Fatal("tool failed")
}
for _, c := range res.Content {
log.Print(c.(*mcp.TextContent).Text)
}
}mcp.CommandTransportはコマンドを起動してそのstdin/stdout経由で接続します。テストやCIでサーバー単体の動作確認をしたいときは、この形でクライアント側からツールを叩くのが手軽です。
stdioの外へ — HTTPでサーバーを公開する
ローカル起動のstdioサーバーだけでなく、リモートに公開したいときはStreamableHTTPHandlerを使います。mcp.Serverを包んだhttp.Handlerを作るだけで、あとは通常のGo HTTPサーバーと同じ扱いです。
server := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.1.0"}, nil)
handler := mcp.NewStreamableHTTPHandler(func(r *http.Request) *mcp.Server {
return server
}, &mcp.StreamableHTTPOptions{Stateless: true})
http.ListenAndServe(":8080", handler)クライアント側はStreamableClientTransportにエンドポイントURLを渡すだけで、stdio版のコードとほぼ同じ形でセッションを張れます。
transport := &mcp.StreamableClientTransport{Endpoint: "http://localhost:8080/mcp"}
client := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v1.0.0"}, nil)
session, err := client.Connect(ctx, transport, nil)MCP仕様2026-07-28では必ずStateless: trueにする必要があります。このバージョンのstreamable HTTPトランスポートは、StreamableHTTPOptions.Statelessをtrueにしたハンドラーでしか2026-07-28のリクエストを受け付けません。非ステートレスのハンドラーに投げると拒否されます。ステートレスモードでは、サーバー側からクライアントへリクエストを送ることはできません(クライアントの応答をセッションに戻す経路が無いため)。分散環境で複数プロセスにサーバーを分散させたい場合の実装例は、SDKリポジトリのexamples/server/distributedにあります。
既定ではメッセージの再送・再開(resumability)に対応していません。永続ストレージか無制限のメモリ使用のどちらかが必要になるためです。テストや簡易用途であればStreamableHTTPOptions.EventStoreにMemoryEventStoreを設定すると再開に対応できますが、本番用途にはより堅牢な実装を自前で用意するのが安全です。なお2026-07-28仕様はSSEストリームの再開機構(Last-Event-ID)自体を廃止したため、この仕組みが効くのは2025-11-25以前のセッションに限られます。
2026-07-28仕様でセッションの始まり方が変わる
HTTPをStateless: trueで動かす理由は、2026-07-28仕様がセッションの始まり方そのものを変えたことにあります。同じClient.ConnectとServer.ConnectのAPIで、SDKがネゴシエートしたプロトコルバージョンに応じて2つの流れを使い分けます。
プロトコルバージョンによる接続の違い
2025-11-25まで
initializeで握手し、クライアントがnotifications/initializedを送るまでサーバーのセッションは初期化済みになりません。それまでのリクエストは拒否されます。ServerOptions.InitializedHandlerで完了を待てます。
2026-07-28
initializeの握手はなく、最初のリクエストが届いた時点でサーバーが処理します。各リクエストがプロトコルバージョンとクライアントのcapabilityを_metaに載せるため、サーバーはそれを毎回検証します。
握手が無くなった代わりに、クライアントはserver/discoverで対応バージョン・capability・サーバー識別を先に問い合わせられます。2026-07-28を実装するサーバーにはこの実装が必須です。
配信するバージョンを絞りたいときは、ServerOptions.SupportedProtocolVersionsで対応集合を狭められます。運用側で退役させたリビジョンの提供を止める用途です。
2026-07-28ではレスポンスのストリームが途中で壊れると、処理中だったリクエストは失われます。クライアントは新しいIDを付けた新規リクエストとして再送する必要があります。
実装でつまずきやすい落とし穴
SDKのメンテナーが「v2で直す予定のAPI設計ミス」として公開しているドキュメントには、v1.0.0リリース後に見つかった既知の癖がまとめられています。破壊的変更なしには直せないため、v1系を使う限りは次の回避策を覚えておく必要があります。
- 不正なツール名がエラーログで済んでしまう: SEP-986がSDKのv1確定後に決まった経緯があり、本来はpanicすべきところがログ出力止まりです。ツール名のバリデーションを自前で入れておくと事故を防げます
ClientCapabilities.Rootsが構造体ポインタでない: issue #607として報告されている設計ミスで、他のcapabilityフィールドと挙動を揃えたい場合はClientCapabilities.RootsV2を使いますEventStore.Openが実質不要: イベントの永続化と配信が分離される前の設計の名残です。実装するなら空実装(no-op)で構いませんCreateMessageResult.Contentが単数形のまま: 2025-11-25仕様は並列tool callのためcontentに配列を許容しますが、単数フィールドのままなので複数ブロックを返したい場合はCreateMessageResultWithTools(Content []Content)を使う回避策がありますToolAnnotationsのフィールドが*boolでない: MCPクライアントによっては全フィールドを明示的にtrue/falseで送ることを要求しますが、現状の型ではその制御ができません
これらはすべて「動かない」バグではなく「意図と違う挙動をする」設計の癖です。samplingでツール呼び出し結果を複数返したい、厳密なcapability制御をしたい、といった発展的な実装をするタイミングで初めて表面化します。
デバッグに使えるLoggingTransportとMCP Inspector
MCPは仕様の解釈に幅がある部分があり、クライアント・サーバーSDKの実装差やバグで想定と違う挙動になることがあります。Go SDKには、stdio接続などのTransportを包んで生のJSON-RPCトラフィックをそのまま覗けるLoggingTransportが用意されています。
t1, t2 := mcp.NewInMemoryTransports()
server := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil)
serverSession, _ := server.Connect(ctx, t1, nil)
defer serverSession.Close()
client := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, nil)
var b bytes.Buffer
logTransport := &mcp.LoggingTransport{Transport: t2, Writer: &b}
clientSession, _ := client.Connect(ctx, logTransport, nil)TransportをLoggingTransportでラップし、送受信のJSON-RPCメッセージをWriterにそのまま書き出すだけです。ファイルやos.Stderrに向けても構いません。HTTPトランスポート(streamable / legacy SSE)の場合は、http.HandlerFuncでリクエストボディを読み取ってから元のハンドラーに渡すミドルウェアを挟むか、WiresharkやtcpdumpでHTTPトラフィックを直接観察します。
言語をまたいだ相互接続を確認したいときは、TypeScript SDK製の公式デバッグツール「MCP Inspector」も有効です。任意の言語で書いたサーバーに接続してツール一覧・呼び出し結果を確認できるため、Go実装がプロトコルに準拠しているかの検証に使えます。バグを見つけたらgo-sdkリポジトリのissueページにあるバグ報告用テンプレート(bug_report)から、収集したログを添えて報告するとメンテナーの対応が早まります。
まとめ
2026-07-28仕様で動かすなら、Go SDKはv1.7.0以降が前提で、HTTP公開はStateless: trueのハンドラーに限られます。stdioでClaude Codeにつなぐだけなら、ここは気にしなくて済みます。
v1系に残るデフォルトcapabilityや不正なツール名の扱いといった癖は、samplingやcapabilityを厳密に制御する段階で効いてきます。挙動が想定とずれたときはLoggingTransportで生のJSON-RPCを見て、それでも切り分けられなければMCP Inspectorで他言語実装と突き合わせます。Rust・Ruby版の公式SDKによる実装はRustでMCPサーバーを書く・RubyでMCPサーバーを書くにあります。