メインコンテンツまでスキップ

MCPサーバーの実装 - SDKとGo・C#

公式 SDK を使った MCP サーバーの実装方法を、Go と C# (.NET) を中心に解説します。2026-07-28 のステートレスモデルを前提にします。プロトコルの仕組みは プロトコルの基礎 と トランスポート、ツールの設計は ツール設計パターンとアンチパターン で扱い、ここでは「SDK で動くサーバーを作り、状態、長時間処理、再試行をどう設計するか」に絞ります。

1. このドキュメントで扱うこと​

  • 公式 SDK とティア制度
  • 最小構成のサーバー
  • Go SDK での実装
  • C# SDK での実装
  • TypeScript SDK での実装
  • 状態を持つ処理の設計
  • 長時間処理
  • 入力検証とエラー処理
  • 冪等性とタイムアウト
  • SDK の v2 とバージョン互換

公式 SDK とティア制度​

公式 SDK は、機能の網羅度、プロトコルへの追従、保守の約束に応じて 3 つのティアに分類されます。

ティアSDK主な要件
Tier 1TypeScript、Python、C#、Go、Rust、Ruby適合性テストの合格率 100%。新しい仕様のバージョンのリリース前に対応する。Issue は 2 営業日以内にトリアージし、重大なバグは 7 日以内に解決する。安定版のリリースと、全機能の例を含むドキュメントが必要
Tier 2Java適合性テストの合格率 80%。新機能は 6 か月以内に対応する。Issue は 1 か月以内にトリアージする
Tier 3Swift、PHP、Kotlin実験的、または部分的な実装。最低限の要件なし

Tier 1 は、サンプリングや Elicitation のようなオプションの機能を含め、実験的でない機能をすべて実装している SDK です。Tasks や MCP Apps のような実験的機能と拡張は、どのティアでも必須ではありません。したがって、Tasks を使いたい場合は、使う SDK が対応しているかを個別に確認します。ティアは適合性テストの結果で見直され、最新の安定版でテストの失敗が 4 週間続くと降格します。SDK を選ぶときは、言語の相性に加えて、このティアと、対象のプロトコルバージョン (2026-07-28) に対応した安定版があるかを確認します。

最小構成のサーバー​

どの SDK でも、サーバーの最小構成は同じ形です。サーバーのインスタンスを作り、ツールを 1 つ登録し、トランスポートに接続します。

ローカルのホストアプリケーションに組み込むなら stdio、チームや組織で共有するなら Streamable HTTP です。両方に対応するのが標準的な作りで、ツールのハンドラーは共通にし、起動時の引数でトランスポートを切り替えます。

stdio の場合は、標準出力が JSON-RPC のメッセージ専用の通路になります。ログを標準出力に書くとメッセージが壊れ、サーバーが動かなくなります。ログは標準エラー出力かファイルに書きます。Go なら fmt.Println ではなく log.Println (既定で標準エラー出力) を、C# なら Console.WriteLine ではなく標準エラー出力かファイルに書くログライブラリを、TypeScript なら console.log ではなく console.error を使います。HTTP のサーバーでは標準出力に書いてもかまいません。

Streamable HTTP のサーバーは、POST を受ける単一のエンドポイントを 1 つ公開します。各リクエストへの応答は、単一の JSON か、そのリクエスト専用の SSE ストリームのどちらかで、サーバーがリクエストごとに選べます。GET と DELETE は使われず、Mcp-Session-Id もありません。

Go SDK での実装​

Go の SDK は github.com/modelcontextprotocol/go-sdk で、サーバーの実装には mcp パッケージを使います (公式のクイックスタートは Go 1.24 以上が前提です)。

go get github.com/modelcontextprotocol/go-sdk/mcp

サーバーは mcp.NewServer で作り、ツールはジェネリックな mcp.AddTool で登録します。ハンドラーは、リクエスト、型付きの入力、型付きの出力とエラーを扱う関数で、入力用の構造体から JSON Schema が生成されます。

  • Tool.InputSchema を指定しなければ、SDK が入力の型 (構造体または map) からスキーマを推論する。OutputSchema も、出力の型が any でなければ推論される
  • 引数は入力スキーマで検証され、出力は出力スキーマで検証される。ハンドラーに届く時点で、型と必須項目は検証済みになる
  • 構造体のフィールドは、json タグが引数名、jsonschema タグが description になる。この description はモデルが読む文字列なので、ツールの description と同じ品質で書く
  • ハンドラーが通常の error を返すと、SDK がそれを結果に入れて isError: true にする。LLM に見せたい検証エラーは、このように返す
  • ステートレスな運用で、リクエストごとに Server を作る場合は、mcp.NewSchemaCache() を起動時に 1 つ作って ServerOptions に渡し、スキーマの再生成を避ける

stdio では server.Run(ctx, &mcp.StdioTransport{}) を使います。HTTP では、mcp.NewStreamableHTTPHandler が http.Handler を返すので、Web API開発 (net/http) と同じように、http.Handle に登録し、認可のミドルウェアで包めます。このハンドラーは、リクエストごとに使う *mcp.Server を返す関数を受け取ります。

2026-07-28 のプロトコルを受けるには、StreamableHTTPOptions の Stateless を true にします。 SDK のドキュメントによれば、2026-07-28 のリクエストは Stateless でないハンドラーに対して拒否されます。Stateless: true は、セッション ID の検証をせず、リクエストごとに一時的なセッションを使う設定です。EventStore による再開機能は、2026-07-28 のセッションでは使われません。完全なコードは「4. コード例」にあります。

C# SDK での実装​

C# の SDK は NuGet で配布され、用途別に 3 つのパッケージに分かれています。

パッケージ用途
ModelContextProtocol.Core依存を最小にした、クライアントまたは低レベルのサーバー API
ModelContextProtocolクライアント、または stdio のサーバー。ホスティングと、アトリビュートによるツールの検出を含む
ModelContextProtocol.AspNetCoreASP.NET Core の HTTP サーバー。ModelContextProtocol を参照する

公式のクイックスタートは、.NET 8 以上を前提に、dotnet add package ModelContextProtocol --prerelease でパッケージを追加します。プレリリース版を使う場合があるので、バージョンの固定と更新の方針を決めておきます。

サーバーは、.NET の汎用ホストと DI に統合されています。

  • AddMcpServer() でサーバーを登録し、WithStdioServerTransport() または WithHttpTransport() でトランスポートを選ぶ
  • ツールを持つクラスに [McpServerToolType]、ツールのメソッドに [McpServerTool] を付け、WithToolsFromAssembly() でアセンブリから検出する。引数の description は、[Description] アトリビュートで付ける。プロンプトとリソースにも、[McpServerPromptType] / [McpServerPrompt]、[McpServerResourceType] / [McpServerResource] がある
  • ツールのメソッドの引数には、DI で登録したサービス (クイックスタートの HttpClient など) を受け取れる。データベースのクライアントや、ユーザーを特定するサービスも、ふつうの .NET の DI で渡す
  • ASP.NET Core では、app.MapMcp() で MCP のエンドポイントを公開する。MCPの認可 の例のように、RequireAuthorization() で認可を掛けられる

HTTP のサーバーは、ステートレスなモードにします。SDK のドキュメントは、サンプリングや Elicitation のような、サーバーからクライアントへの要求を使わないサーバーには、ステートレスなモードが既定で推奨だと説明しています。明示するなら、WithHttpTransport のオプションで SessionMode = HttpServerSessionMode.Stateless を指定します。stdio のサーバーを作る場合は、ホストのビルダーに Host.CreateEmptyApplicationBuilder(settings: null) を使います。CreateDefaultBuilder だと、ホストがコンソールへ余計なメッセージを出し、stdio のメッセージを壊します。

TypeScript SDK での実装​

TypeScript の SDK は、v2 でサーバー用の @modelcontextprotocol/server とクライアント用の @modelcontextprotocol/client に分かれています (入力スキーマには zod を使います)。公式のクイックスタートでは、McpServer に registerTool でツールを登録し、stdio には serveStdio を使います。

  • serveStdio は、サーバーを返すファクトリー関数を受け取る。クライアントごとにプロトコルのバージョンを交渉し、server/discover のプローブのためにも 1 回インスタンスを作って、initialize にフォールバックした場合は破棄する。そのため、ファクトリーは呼ばれるたびに新しい McpServer を返す
  • server.connect(new StdioServerTransport()) で手動で配線すると、2025 年版のクライアントにしか応答しないサーバーになる。2026-07-28 のクライアントも受けるには、serveStdio を使う
  • HTTP では、createMcpHandler が Web 標準の Request / Response で動くステートレスなハンドラーを提供する。Workers のような実行環境にも載せやすい。詳しい API は SDK のドキュメントを参照する

動作の確認には、Inspector の CLI を --protocol-era modern で実行します。Inspector の既定は legacy (2025 年版のハンドシェイク) なので、同じコマンドを --protocol-era legacy でもう一度実行し、両方の世代のクライアントから同じツールが見えることを確かめます。

状態を持つ処理の設計​

2026-07-28 の MCP には、プロトコル上のセッションがありません。サーバーは、接続やプロセスの同一性を、会話や利用者の同一性の代わりにできません。サーバーは、リクエストの _meta と引数だけで処理を完結できる必要があり、複数のリクエストにまたがる状態は、明示的な識別子 (ハンドル) をクライアントが毎回渡す形にします。

ハンドルはプロトコルの概念ではありません。ワイヤー上は、ツールの結果の中の普通の文字列で、次のツール呼び出しの普通の引数です。公式の仕様のガイダンス (非規範的) は、ハンドルの設計で次の点を考えるよう求めています。

  • 認可: 認証を実装するサーバーでは、ハンドルは名前であって、権限ではない。ハンドルを持っていることを認証として扱わず、毎回の呼び出しで、呼び出し元の認可をハンドルに対して検証する。認証のないサーバーでは、ハンドルがそのまま bearer トークンになるため、十分なエントロピー (UUIDv4 など) で生成し、有効期間を限る
  • 不透明性: 内部の構造を符号化したハンドルは、解析や推測を招く。意味を持たない識別子にする
  • 有効期間: ハンドルは接続より長く生きる。保持の方針 (「カゴは 24 時間操作がないと失効する」など) を、作成するツールの description に書き、状態を作る前にモデルが見られるようにする
  • 失効のエラー: 期限切れや未知のハンドルへの呼び出しは、そのことを伝えるツール実行エラーを返し、モデルが新しく作り直せるようにする

ハンドルの乗っ取りの攻撃と、保存キーを <user_id>:<handle> にして user_id を検証済みのトークンから取る対策は、MCPセキュリティベストプラクティス を参照してください。サーバーが複数のインスタンスで動く前提では、ハンドルの実体を、プロセスのメモリではなく外部のストアに置きます。ステートレスな運用では、どのリクエストがどのインスタンスに届くかが決まらないためです。

長時間処理​

数秒を超える処理には、3 つの道具があります。

進捗通知 (progress): クライアントは、進捗を受け取りたいリクエストの _meta に progressToken (文字列か整数で、有効なリクエストの間で一意) を入れます。サーバーは、任意で notifications/progress を、progress (通知のたびに増える値)、任意の total と message とともに送れます。Streamable HTTP では、この通知は、そのリクエストの SSE 応答ストリームの上で、最終的な応答の前に届きます。サーバーは、トークンの付いていないリクエストに進捗を送ってはならず、完了後は送りません。送る頻度は、レート制限を考えて決めます。

キャンセル: Streamable HTTP では、クライアントが応答ストリームを閉じることが、そのリクエストのキャンセルです (notifications/cancelled は使われません)。サーバーは、できるだけ早く処理を止め、そのリクエストへの応答を送ってはなりません。stdio では、クライアントが notifications/cancelled を送ります。ハンドラーでは、SDK から渡されるキャンセルを伝えるオブジェクト (Go の context.Context など) を、下流の API 呼び出しやデータベースの問い合わせに渡し、キャンセルが処理の途中まで伝わるようにします。なお、キャンセルはレースを含みます。通知が届く前に処理が終わっていることがあり、終わった処理の副作用は取り消されません。

Tasks 拡張: CI のパイプライン、バッチ処理、人間の承認のように、分や時間のかかる処理では、接続を開いたまま待たせず、永続的なハンドルを返します。Tasks はコアのプロトコルではなく拡張 (io.modelcontextprotocol/tasks) で、クライアントとサーバーの両方が対応を宣言したときだけ使えます。

  1. クライアントは、各リクエストの _meta の capabilities の extensions に io.modelcontextprotocol/tasks を入れる。サーバーは server/discover の capabilities に同じ拡張を載せる
  2. サーバーは、クライアントが対応を宣言していることを確認してから、処理が長くなりそうなリクエストに、resultType: "task" の CreateTaskResult (taskId、状態、ttlMs、pollIntervalMs) を返す。対応していないクライアントには、タスクを返してはならない。タスクは、応答を送る前に永続的に作成しておく
  3. クライアントは tasks/get でポーリングする。状態は working、input_required、completed、failed、cancelled で、最後の 3 つは終端状態
  4. 途中で入力が必要になると、状態が input_required になり、inputRequests を返す。クライアントは tasks/update で inputResponses を返す
  5. tasks/cancel による取り消しは協調的で、サーバーが必ず止めるとは限らない

タスクを使うかどうかは、サーバーがリクエストごとに決めます。クライアントは、通常の結果とタスクの結果のどちらが来ても扱えるようにします。タスクの ID は、クライアントの再起動をまたいで再開するための永続的なハンドルなので、状態ハンドルと同じく、呼び出し元の検証が必要です。

入力検証とエラー処理​

ツールのエラーには 2 種類あります。

種類例返し方
プロトコルエラー未知のツール、スキーマに合わない不正なリクエスト、サーバーの障害JSON-RPC の error (未知のツールは -32602)
ツール実行エラー外部 API の失敗、入力値の検証エラー、ビジネスルール違反成功レスポンスの中で isError: true

LLM が自己修正できるのは後者です。日付の形式の誤り、範囲外の値、存在しない ID のように、モデルが引数を直して再試行できる失敗は、ツール実行エラーとして、何が悪くどう直すかを書いて返します (設計は ツール設計パターンとアンチパターン を参照)。スキーマの検証は SDK が行いますが、スキーマで表せない検証 (存在確認、権限、業務ルール) は、ハンドラーが行います。

仕様は、サーバーに次のことを義務づけています。

  • すべてのツールの入力を検証する
  • アクセス制御を実装する
  • ツールの呼び出しにレート制限を掛ける
  • ツールの出力をサニタイズする

もう 1 つ、プロトコル層のエラーとして、リクエストの _meta に必須のフィールド (protocolVersion、clientCapabilities) がなければ -32602 で拒否し、クライアントが宣言していない機能 (Elicitation など) が必要なら MissingRequiredClientCapability (-32021) を返します。HTTP では、どちらも 400 Bad Request です。SDK は、これらの処理を肩代わりします。

冪等性とタイムアウト​

Streamable HTTP では、各リクエストが独立した POST で、応答のストリームが閉じられるとサーバーはそのリクエストをキャンセルされたものとして扱います。クライアントには、タイムアウトを設けて、応答が来なければキャンセルするよう推奨されており、ストリームが切れたときの再開 (Last-Event-ID) もありません。したがって、クライアントは、結果を受け取れなかったリクエストを、新しいリクエスト ID で、最初からもう一度送ることになります。サーバーから見ると、これは別のリクエストです (同じ ID を再利用しないことは、仕様が要求しています)。

結果として、次の事態が起こります。

  • 1 回目の処理は、ストリームが切れた後も、キャンセルが届く前にすでに副作用を起こしているかもしれない
  • クライアントの再試行は、1 回目の再送とは区別できない

そのため、副作用のある操作 (注文、送金、チケットの作成、メールの送信) は、冪等に設計します。

  • 作成系のツールに、クライアントが生成する冪等性キー (idempotency_key) を引数として持たせ、同じキーでの 2 回目の呼び出しには、1 回目の結果を返す
  • 「同じ内容を 2 回受けたら、1 回分として扱う」ことを、一意制約や条件付きの書き込みで、ストア側で保証する
  • 長く掛かる処理は、Tasks (または自前のジョブのハンドル) にして、結果の取得と作成を分ける
  • 外部の API を呼ぶときは、その API の冪等性キーに、自分の冪等性キーを引き継ぐ

冪等性キーは、モデルが生成する引数でもよいですが、モデルが再試行のたびに別のキーを作ると意味がなくなります。ツールの description に、「同じ操作の再試行には同じキーを使う」と書くか、ホストアプリケーションが決定的に付与できる設計にします。

SDK の v2 とバージョン互換​

SDK には、2026-07-28 に対応するメジャーバージョンがあります。Python のクイックスタートは「Python MCP SDK 2.0.0 以上」、TypeScript は @modelcontextprotocol/server / @modelcontextprotocol/client を使い、C# はクイックスタートが --prerelease を付けています。2026-07-28 のサーバーを作るなら、これらの版を使います。

旧リビジョン (2025-11-25 以前、initialize のハンドシェイクを使う) のクライアントを受けるかどうかは、サーバーの方針です。仕様は、両方に対応する実装を「dual-era」と呼び、_meta を持つリクエストは新しい仕様でステートレスに処理し、initialize で始まるリクエストは旧仕様のセマンティクスで処理することを認めています。

項目2026-07-28 のサーバーが旧クライアントに対して行うこと
バージョンの不一致UnsupportedProtocolVersion (-32022) を返し、data.supported に対応バージョンを入れる。クライアントは、共通のバージョンを選んで再試行する
initialize への応答 (2026-07-28 以降だけに対応するサーバー)エラーに、対応するバージョンを書く。旧クライアントには、前方互換の仕組みがなく、このメッセージが唯一の診断になり得る
GET または DELETE405 Method Not Allowed を返す
Mcp-Session-Id、Last-Event-ID無視する。セッション ID を発行したり、返したりしない

SDK の設定で、旧クライアントを受ける範囲が決まります。たとえば、TypeScript の serveStdio は、両方の世代に応答します。Go の Stateless や C# の SessionMode のような設定が、旧クライアントとの関係にどう影響するかは、SDK のドキュメントで確認します。旧クライアントを受けるときは、旧仕様のセッションに依存した実装 (サーバー側のセッション状態) を、2026-07-28 のリクエストに持ち込まないよう、世代ごとに処理を分けます。

2. 言語ごとの SDK​

言語リポジトリ備考
Gomodelcontextprotocol/go-sdkTier 1。net/http と組み合わせる。HTTP は Stateless: true
C# (.NET)modelcontextprotocol/csharp-sdkTier 1。パッケージは ModelContextProtocol (stdio、ホスティング)、ModelContextProtocol.AspNetCore (HTTP)、ModelContextProtocol.Core (低レベル)
TypeScriptmodelcontextprotocol/typescript-sdkTier 1。@modelcontextprotocol/server。Web 標準ベースで Workers などでも動作
Pythonmodelcontextprotocol/python-sdkTier 1。2026-07-28 には SDK 2.0.0 以上

3. 開発者がつまずきやすいポイント​

stdio のサーバーが標準出力にログを書く​

fmt.Println、Console.WriteLine、console.log は、標準出力に書き、JSON-RPC のメッセージを壊します。症状は、ホストが「サーバーが起動しない」「JSON のパースエラー」と報告するだけで、原因が分かりにくい点です。ログは標準エラー出力かファイルに書き、C# では CreateEmptyApplicationBuilder も使います。

Go の HTTP ハンドラーを Stateless にし忘れる​

2026-07-28 のクライアントのリクエストが、Stateless でないハンドラーに拒否されます。mcp.StreamableHTTPOptions{Stateless: true} を指定します。

TypeScript で serveStdio を使わずに手動で接続する​

server.connect(new StdioServerTransport()) は、コンパイルも動作もしますが、2025 年版のクライアントにしか応答しません。2026-07-28 のクライアントも受けるなら、serveStdio にファクトリーを渡します。ファクトリーが同じインスタンスを返し続けるのも誤りで、呼ばれるたびに新しいインスタンスを返します。

サーバーのメモリにセッション状態を持つ​

プロセスのメモリにカゴや認証済みの状態を持つと、ロードバランサーの背後で、リクエストが別のインスタンスに届いた時点で壊れます。ステートは外部のストアに置き、ハンドルを引数で受け取ります。

ハンドルを持っていることを認可として扱う​

ハンドルは名前であって、権限ではありません。毎回、呼び出し元をハンドルの所有者と照合します。

副作用のあるツールを冪等にしない​

ストリームの切断とクライアントの再試行で、同じ注文が 2 回作られます。冪等性キーか、ストア側の一意制約で防ぎます。

検証エラーを例外やプロトコルエラーで返す​

モデルが直せない形のエラーを返すと、再試行が無駄になります。入力値の誤りは、isError: true のツール実行エラーとして、直し方を含めて返します。

キャンセルをハンドラーの下流に伝えない​

クライアントが切断しても、データベースの問い合わせや外部 API の呼び出しが走り続け、リソースを使い続けます。SDK が渡すキャンセルの仕組みを、下流の呼び出しに引き継ぎます。

長い処理を接続を開いたまま待つ​

プロキシやクライアントのタイムアウトで切られます。数秒を超える処理は、進捗通知で状況を返すか、Tasks (または自前のジョブのハンドル) にします。ただし、Tasks は拡張で、クライアントが対応を宣言した場合にしか使えません。

実験的な機能を前提にする​

Tasks のような拡張は、どのティアの SDK にも必須ではなく、ホストの対応状況もさまざまです。対応していないクライアントのために、通常の結果を返す経路も用意します。

4. コード例​

Go: stdio と Streamable HTTP の両方で公開する​

入力に構造体を使い、絞り込みの引数、検証エラーの返し方、冪等性キー、ハンドルの所有者の検証を含めた例です。ユーザーの特定 (userFromContext) は、認可のミドルウェアが、検証済みのトークンから context に設定する前提です (MCPの認可 を参照)。

package main

import (
"context"
"errors"
"fmt"
"log"
"net/http"
"os"
"sync"

"github.com/modelcontextprotocol/go-sdk/mcp"
)

type ctxKey struct{}

// 認可のミドルウェアが、検証済みのトークンから設定する。
func userFromContext(ctx context.Context) (string, bool) {
u, ok := ctx.Value(ctxKey{}).(string)
return u, ok && u != ""
}

type SearchOrdersInput struct {
Status string `json:"status" jsonschema:"注文の状態。pending、shipped、cancelled のいずれか"`
Limit int `json:"limit,omitempty" jsonschema:"返す件数の上限。既定は 20、最大は 100"`
}

type OrderSummary struct {
ID string `json:"id"`
Status string `json:"status"`
}

type SearchOrdersOutput struct {
Orders []OrderSummary `json:"orders"`
}

func searchOrders(ctx context.Context, req *mcp.CallToolRequest, in SearchOrdersInput) (
*mcp.CallToolResult, SearchOrdersOutput, error,
) {
switch in.Status {
case "pending", "shipped", "cancelled":
default:
// 通常の error は、SDK が isError: true の結果にする。LLM が直せるように書く。
return nil, SearchOrdersOutput{}, fmt.Errorf(
"status %q は使えません。pending、shipped、cancelled のいずれかを指定してください", in.Status)
}
limit := in.Limit
if limit <= 0 {
limit = 20
}
if limit > 100 {
limit = 100
}
// ... ストアを検索する (ctx を渡し、キャンセルを伝える) ...
return nil, SearchOrdersOutput{Orders: []OrderSummary{}}, nil
}

type PlaceOrderInput struct {
BasketID string `json:"basket_id" jsonschema:"create_basket が返したカゴの ID。24 時間操作がないと失効する"`
IdempotencyKey string `json:"idempotency_key" jsonschema:"同じ操作を再試行するときは同じキーを使う"`
}

type PlaceOrderOutput struct {
OrderID string `json:"order_id"`
}

var (
mu sync.Mutex
results = map[string]PlaceOrderOutput{} // 本番では外部のストアと一意制約を使う
)

func placeOrder(ctx context.Context, req *mcp.CallToolRequest, in PlaceOrderInput) (
*mcp.CallToolResult, PlaceOrderOutput, error,
) {
user, ok := userFromContext(ctx)
if !ok {
return nil, PlaceOrderOutput{}, errors.New("認証が必要です")
}

// 所有者を含めたキーで冪等性を保証する。他の利用者のキーとは衝突しない。
key := user + ":" + in.IdempotencyKey
mu.Lock()
defer mu.Unlock()
if r, done := results[key]; done {
return nil, r, nil // 再試行には 1 回目の結果を返す
}

// ハンドルの所有者を検証する (例: ストアのキーを user + ":" + basket_id にする)。
// 見つからない、または期限切れなら、モデルが回復できるエラーを返す。
// ... ストアから <user>:<basket_id> を引く ...

out := PlaceOrderOutput{OrderID: "ord_123"}
results[key] = out
return nil, out, nil
}

func main() {
server := mcp.NewServer(&mcp.Implementation{Name: "orders", Version: "1.0.0"}, nil)

mcp.AddTool(server, &mcp.Tool{
Name: "search_orders",
Description: "注文を状態で検索する。注文の一覧や、状況の確認に使う。" +
"1 件の注文の詳細は get_order を使う。",
}, searchOrders)
mcp.AddTool(server, &mcp.Tool{
Name: "place_order",
Description: "カゴの内容で注文を確定する。課金が発生する。" +
"再試行では同じ idempotency_key を使えば、二重に注文されない。",
}, placeOrder)

// 引数 stdio なら、ローカルのホストが起動する子プロセスとして動く。
// 標準出力は JSON-RPC 専用なので、ログは標準エラー出力 (log の既定) に書く。
if len(os.Args) > 1 && os.Args[1] == "stdio" {
if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
log.Fatal(err)
}
return
}

// Streamable HTTP。2026-07-28 のクライアントを受けるには Stateless: true にする。
handler := mcp.NewStreamableHTTPHandler(
func(r *http.Request) *mcp.Server { return server },
&mcp.StreamableHTTPOptions{Stateless: true},
)
http.Handle("/mcp", handler) // 本番では、認可のミドルウェアと Origin の検証で包む
log.Fatal(http.ListenAndServe("127.0.0.1:8080", nil))
}

C#: ASP.NET Core のステートレスな HTTP サーバー​

// Program.cs
using System.ComponentModel;
using ModelContextProtocol.Server;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddMcpServer()
.WithHttpTransport(options =>
{
// サーバーからクライアントへの要求を使わないので、ステートレスにする。
options.SessionMode = HttpServerSessionMode.Stateless;
})
.WithToolsFromAssembly();

var app = builder.Build();
app.MapMcp("/mcp"); // 本番では RequireAuthorization() で認可を掛ける
app.Run();

[McpServerToolType]
public static class OrderTools
{
[McpServerTool, Description(
"注文を状態で検索する。注文の一覧や、状況の確認に使う。1 件の詳細は get_order を使う。")]
public static string SearchOrders(
// DI で登録したサービスは、ツールのメソッドの引数に受け取れる。
// OrderStore store,
[Description("注文の状態。pending、shipped、cancelled のいずれか")] string status,
[Description("返す件数の上限。既定は 20、最大は 100")] int limit = 20)
{
var allowed = new[] { "pending", "shipped", "cancelled" };
if (!allowed.Contains(status))
{
return $"status '{status}' は使えません。pending、shipped、cancelled のいずれかを指定してください。";
}
limit = Math.Clamp(limit, 1, 100);
// ... ストアを検索して、結果を返す ...
return "[]";
}
}

stdio のサーバーにする場合は、Host.CreateEmptyApplicationBuilder(settings: null) で、AddMcpServer().WithStdioServerTransport().WithToolsFromAssembly() を使い、ログは標準エラー出力に書きます。

TypeScript: stdio のサーバー​

import { McpServer } from "@modelcontextprotocol/server";
import { serveStdio } from "@modelcontextprotocol/server/stdio";
import { z } from "zod";

// serveStdio は、必要なたびに呼ぶ。毎回、新しいインスタンスを返す。
function createServer() {
const server = new McpServer({ name: "orders", version: "1.0.0" });

server.registerTool(
"search_orders",
{
description:
"注文を状態で検索する。注文の一覧や、状況の確認に使う。1 件の詳細は get_order を使う。",
inputSchema: z.object({
status: z
.enum(["pending", "shipped", "cancelled"])
.describe("注文の状態"),
limit: z
.number()
.int()
.min(1)
.max(100)
.default(20)
.describe("返す件数の上限"),
}),
},
async ({ status, limit }) => {
// ... ストアを検索する ...
return { content: [{ type: "text", text: `${status}: 0 件 (limit ${limit})` }] };
},
);

return server;
}

serveStdio(createServer, {
onerror: (error) => console.error("orders server error:", error),
});
console.error("orders server running on stdio"); // console.log は使わない

動作の確認 (Inspector)​

npx @modelcontextprotocol/inspector --cli node build/index.js --protocol-era modern --method tools/list
npx @modelcontextprotocol/inspector --cli node build/index.js --protocol-era legacy --method tools/list

5. まとめ​

  • 公式 SDK は 3 つのティアに分かれ、Tier 1 は TypeScript、Python、C#、Go、Rust、Ruby。Tasks のような拡張は、どのティアでも必須ではない
  • サーバーの最小構成は、インスタンスを作り、ツールを登録し、stdio と Streamable HTTP に接続する形。stdio では標準出力に書かない
  • Go は mcp.AddTool と構造体で入力スキーマを生成する。HTTP は NewStreamableHTTPHandler を Stateless: true で使う
  • C# は ModelContextProtocol と ModelContextProtocol.AspNetCore、アトリビュートと DI で実装し、HTTP はステートレスにする
  • TypeScript は serveStdio にファクトリーを渡し、2026-07-28 と旧世代の両方のクライアントを受ける
  • プロトコル上のセッションがないので、状態はハンドルを引数で受け渡し、毎回、呼び出し元の認可を検証する。実体は外部のストアに置く
  • 長時間処理は、進捗通知、キャンセル (HTTP ではストリームを閉じる)、Tasks 拡張を使い分ける
  • 入力値の誤りは isError のツール実行エラーとして返し、副作用のある操作は、再試行を前提に冪等にする
  • 旧世代のクライアントを受けるかどうかは、サーバーの方針で、UnsupportedProtocolVersion と対応バージョンの一覧で互換を取る

関連ドキュメント​

参考資料​