Claude Media
GoでMCPサーバーを書く公式SDKの使い方

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と並ぶ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)固有の実装手順に絞ります。

導入からツール定義まで — 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のままにすると、SDKがOutputをJSONへシリアライズしてレスポンスを自動生成します。手動でContentを組み立てたいときだけCallToolResultを自分で埋めます。server.Runmcp.StdioTransportを渡すとstdin/stdoutで待ち受け、クライアントが切断するまでブロックします。

このバイナリをgo buildしてから動かせば、それだけで1つのMCPサーバーです。

go build -o mytool .

ステップ3: Claude Codeに接続する

ビルドしたバイナリをclaude mcp addでstdioトランスポート指定して登録します。ローカルバイナリなので--transport stdioとコマンドをそのまま渡すだけです。

claude mcp add --transport stdio mytool -- ./mytool

登録後は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.EventStoreMemoryEventStoreを設定すると再開に対応できますが、本番用途にはより堅牢な実装を自前で用意するのが安全です。なお2026-07-28仕様はSSEストリームの再開機構(Last-Event-ID)自体を廃止したため、この仕組みが効くのは2025-11-25以前のセッションに限られます。

実装でつまずきやすい落とし穴

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接続の生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)

TransportLoggingTransportでラップし、送受信のJSON-RPCメッセージをWriterにそのまま書き出すだけです。ファイルやos.Stderrに向けても構いません。HTTPトランスポート(streamable / legacy SSE)の場合は、http.HandlerFuncでリクエストボディを読み取ってから元のハンドラーに渡すミドルウェアを挟むか、WiresharkやtcpdumpでHTTPトラフィックを直接観察します。

言語をまたいだ相互接続を確認したいときは、TypeScript SDK製の公式デバッグツール「MCP Inspector」も有効です。任意の言語で書いたサーバーに接続してツール一覧・呼び出し結果を確認できるため、Go実装がプロトコルに準拠しているかの検証に使えます。バグを見つけたらgo-sdkリポジトリのissueページにあるバグ報告用テンプレート(bug_report)から、収集したログを添えて報告するとメンテナーの対応が早まります。

まとめ

Go公式SDKはgo get一発で導入でき、構造体とタグだけでツールのスキーマを定義できる薄いAPIです。stdioで動かしてclaude mcp add --transport stdioで繋ぐところまでは数分で完了します。実運用で注意すべきはSDKのバージョンとMCP仕様の対応表、そしてv1系に残るデフォルトcapability・不正名バリデーションなどの設計上の癖です。詰まったときはLoggingTransportで生トラフィックを確認し、それでも解決しなければMCP Inspectorで他言語実装との相互接続性を切り分けてください。Rust・Ruby版の公式SDKによる実装はRustでMCPサーバーを書くRubyでMCPサーバーを書くにまとめています。

この記事を共有:XはてブLinkedIn