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・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 -- ./mytool

Claude 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つの流れを使い分けます。

くらべる

プロトコルバージョンによる接続の違い

initializeあり

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サーバーを書くにあります。

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