C#でMCPサーバーを作る — .NET公式SDKクイックスタート
MCP公式C# SDKでstdioサーバーとHTTPサーバーを最短で立てる手順。Tool定義の書き方とクライアント接続、ホスト名検証の注意点まで一次ソースの実装例で確認します。
C#MCPサーバーとは何を作ることになるのか
C#でMCPサーバーを作るとは、Claude CodeやClaude Desktopのようなホストアプリケーションから呼び出せるツール・リソース・プロンプトを、.NET上に公開することです。MCP(Model Context Protocol)の公式C# SDKはTier1(最上位の機能完全性・仕様準拠・保守コミットメント)に分類されており、TypeScript・Pythonと並ぶ第一級のSDKです。
日本語での実装記事はまだ少なく、npm系のTypeScript SDKやデコレータでツールを定義するPython SDKの記事が先行しています。.NET / C#で社内ツールをMCP化したいチームにとって、公式のクイックスタートをそのまま日本語で追える記事がまだ薄いのが現状です。
C#MCPサーバーを作る前提
前提は次の3つです。動作確認まで含めると4つ目に、Node.js環境(npx)があるとMCP Inspectorや参照実装サーバーをすぐ試せて作業が速くなります。
- .NET SDKがインストール済み(
dotnetコマンドが使える) - NuGetからパッケージを取得できるネットワーク環境
- Claude CodeやMCP Inspectorなど、動作確認に使えるMCPホストが手元にある
公式SDKは3つのNuGetパッケージに分かれています。最初にどれを使うかを決めておくと、あとから依存関係を作り直さずに済みます。
| パッケージ | 向いている用途 |
|---|---|
ModelContextProtocol.Core | 向いている用途クライアントまたは低レベルサーバーAPIのみが必要で、依存を最小限にしたい場合 |
ModelContextProtocol | 向いている用途stdioベースのサーバーやクライアントを作り、ホスティング・DI・属性ベースのTool/Prompt/Resource自動検出を使いたい場合(多くのプロジェクトの起点) |
ModelContextProtocol.AspNetCore | 向いている用途ASP.NET Core上のHTTPベースサーバーを作る場合(ModelContextProtocol を内包) |
迷ったら ModelContextProtocol から始め、あとでHTTPトランスポートが必要になったときに ModelContextProtocol.AspNetCore を足す構成が公式の推奨です。
stdioサーバーを最短で立てる
コンソールアプリを新規作成し、パッケージを追加します。
dotnet new console
dotnet add package ModelContextProtocol
dotnet add package Microsoft.Extensions.HostingProgram.cs を次の内容に置き換えると、stdio経由で1つのToolを公開する最小構成のMCPサーバーが動きます。
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Server;
using System.ComponentModel;
var builder = Host.CreateApplicationBuilder(args);
builder.Logging.AddConsole(consoleLogOptions =>
{
// ログはすべて標準エラーへ流す
consoleLogOptions.LogToStandardErrorThreshold = LogLevel.Trace;
});
builder.Services
.AddMcpServer()
.WithStdioServerTransport()
.WithToolsFromAssembly();
await builder.Build().RunAsync();
[McpServerToolType]
public static class EchoTool
{
[McpServerTool, Description("Echoes the message back to the client.")]
public static string Echo(string message) => $"hello {message}";
}ここでの核心は WithToolsFromAssembly() です。アセンブリ内で [McpServerToolType] を付けたクラスをすべて走査し、[McpServerTool] を付けたメソッドを自動でToolとして登録します。同じパターンがPrompt([McpServerPromptType] / [McpServerPrompt])とResource([McpServerResourceType] / [McpServerResource])にもそのまま使えます。属性を足すだけで公開範囲が増えるので、手動でスキーマを書く必要がありません。
stdioを選ぶ理由: ローカルプロセス間通信はネットワークオーバーヘッドがなく、Claude CodeやClaude Desktopがサブプロセスとして起動するローカルMCPサーバーの標準的な形です。
HTTPサーバーへ切り替える
リモートから複数クライアントを受け付けたい場合は、ASP.NET Coreベースの構成に切り替えます。
dotnet new web
dotnet add package ModelContextProtocol.AspNetCoreusing ModelContextProtocol.Server;
using System.ComponentModel;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMcpServer()
.WithHttpTransport(options =>
{
// サーバーからクライアントへのリクエスト(sampling / elicitation)が
// 不要な場合は Stateless モードを推奨
options.Stateless = true;
})
.WithToolsFromAssembly();
var app = builder.Build();
app.MapMcp();
app.Run("http://localhost:3001");
[McpServerToolType]
public static class EchoTool
{
[McpServerTool, Description("Echoes the message back to the client.")]
public static string Echo(string message) => $"hello {message}";
}Tool側のコードはstdio版と一字一句同じです。変わるのはトランスポートの登録部分だけなので、既存のstdioサーバーをHTTP化するときもTool実装の書き直しは発生しません。
ホスト名検証を必ず設定する。ASP.NET CoreのKestrelはデフォルトで Host ヘッダーを検証しません。ローカル用途では AllowedHosts をループバック値に限定し、本番デプロイではプロキシ・ロードバランサ側でも検証してDNSリバインディング(攻撃者が制御するDNS名でローカルサーバーに到達される攻撃)を防ぎます。ブラウザからのクロスオリジンアクセスが不要ならCORSは有効化しません。CORSはホスト名検証の代わりにはなりません。
MCPクライアントを書いて動作確認する
サーバー単体では動作確認しづらいので、公式SDKでクライアントも書きます。参照実装サーバー(@modelcontextprotocol/server-everything)に接続する例です。
dotnet new console
dotnet add package ModelContextProtocolusing ModelContextProtocol.Client;
using ModelContextProtocol.Protocol;
var clientTransport = new StdioClientTransport(new StdioClientTransportOptions
{
Name = "Everything",
Command = "npx",
Arguments = ["-y", "@modelcontextprotocol/server-everything"],
});
var client = await McpClient.CreateAsync(clientTransport);
// 利用可能なToolの一覧を表示
foreach (var tool in await client.ListToolsAsync())
{
Console.WriteLine($"{tool.Name} ({tool.Description})");
}
// Toolを実行(通常はLLMのtool呼び出しから駆動される)
var result = await client.CallToolAsync(
"echo",
new Dictionary<string, object?>() { ["message"] = "Hello MCP!" },
cancellationToken: CancellationToken.None);
Console.WriteLine(result.Content.OfType<TextContentBlock>().First().Text);このクライアントはC# SDKで作ったサーバーに限らず、任意のMCPサーバーに接続できます。プロトコルはサーバー実装に依存しません。
McpClientTool は AIFunction を継承しているため、ListToolsAsync() で取得したToolをそのまま IChatClient に渡せます。
IList<McpClientTool> tools = await client.ListToolsAsync();
IChatClient chatClient = ...;
var response = await chatClient.GetResponseAsync(
"your prompt here",
new() { Tools = [.. tools] });.NETのチャットクライアント抽象化(IChatClient)にMCPのToolを直結できる設計は、C# SDKが単なるプロトコル実装ではなく Microsoft.Extensions.AI エコシステムに組み込まれていることを示しています。
よくあるつまずき
最初に起きやすいのはパッケージの取り違えです。HTTPサーバーを作るつもりで ModelContextProtocol だけを追加すると WithHttpTransport が見つかりません。HTTPには ModelContextProtocol.AspNetCore が必須です。
ログを標準出力に書いてしまうミスも起きがちです。stdioトランスポートは標準入出力をプロトコル通信に使うため、ロガーの出力先を標準エラーに向け忘れると、ログ文字列がJSON-RPCメッセージに混入してクライアント側のパースが壊れます。上のサンプルコードで LogToStandardErrorThreshold = LogLevel.Trace を明示しているのはこのためです。
DIサービスを引数に書けることに気づかない。[McpServerTool] を付けたメソッドは、クライアントが渡す通常の引数に加えて、McpServer / IProgress<ProgressNotificationValue> / ClaimsPrincipal、それにDIコンテナへ登録済みの任意のサービスを引数として受け取れます。これらは属性ベースの検出のまま自動的に解決され、クライアントへ公開される入力スキーマにはクライアントが渡す引数だけが載ります。明示的なTool登録APIへ切り替える必要はありません。
CORSを設定してホスト名検証を省略してしまうケースも見かけます。CORSはブラウザ側の制限であり、DNSリバインディング対策にはなりません。ローカルHTTPサーバーでは AllowedHosts によるホスト名の限定が別途必要です。
依存パッケージのバージョンを固定しないでいると、あとで困ることがあります。ModelContextProtocol 系パッケージはプロトコル仕様の改訂に合わせて更新が続いています。dotnet add package でバージョン指定を省くと、意図しないタイミングでメジャーアップデートが入り、Toolのシグネチャや属性の挙動が変わることがあります。プロダクション用途では明示的にバージョンを固定し、更新は変更履歴を確認してから行う運用が安全です。
Description属性を省略する。[McpServerTool] に添える [Description] はToolの説明文としてそのままLLMに渡ります。省略するとホスト側がToolの用途を推測できず、実際には使えるToolが呼び出されないままになります。パラメータにも同様に [Description] を付け、何を渡すべきかを明示しておくと、意図しないToolが選ばれる事故を減らせます。
C#MCPサーバーは他言語のSDKとどう違うか
Java向けにはSpring AIとの統合が用意されており、Rust・Go・TypeScript・PythonがTier1、Ruby・JavaがTier2、Swift・PHP・KotlinがTier3という序列です。SDKのTier区分の詳細は言語ごとに機能完全性・保守コミットメントが異なる基準で決まっています。C#はTier1に位置し、Microsoft.Extensions.Hosting によるホスティングモデルと Microsoft.Extensions.AI の IChatClient 抽象化に直結する点が、他のTier1言語には無い.NET固有の強みです。属性ベースのTool検出([McpServerTool])という書き方自体はJava SDK(Spring AI経由)とも発想が近く、リフレクションでツールスキーマを生成する言語では共通のパターンになっています。
クライアント実装の詳しい書き方はMCPクライアント自作ガイド、サーバーとクライアントそれぞれの内部構造の違いはMCPクライアントの仕組みで扱っています。
作ったMCPサーバーをClaude Codeから使う
C# SDKで作ったサーバーは、stdio版であればClaude Codeの設定にコマンドとして登録するだけで利用できます。dotnet run でビルド済みの実行ファイルを直接呼び出す形が最もシンプルで、npmパッケージのようにレジストリ経由の配布を前提にしなくても動作確認は可能です。社内ネットワーク内の自己ホスト環境でC#製MCPサーバーを配布する場合の経路は、Claude Codeセルフホスト環境へMCPサーバーを届ける3つの経路で扱っている内容がそのまま当てはまります。
一方、HTTP版のサーバーを外部に公開してMCP Registry経由で見つけてもらう場合は、server.json によるメタデータ登録が必要です。C#製サーバーはNuGetパッケージとして登録でき、npm・PyPI・Cargo・Docker/OCI・MCPBと並ぶ種別の1つとして扱われます。パッケージ種別ごとの詳細はMCP Registryのパッケージ種別ガイドで扱っています。
ResourceとPromptを属性で公開する
Toolだけでなく、URIで参照できるデータをResourceとして、定型プロンプトをPromptとして公開すると、サーバーの用途が広がります。ClaudeがファイルパスやIDから内容を取得するときはResource、決まった構成のプロンプトをテンプレートとして呼び出したいときはPromptを使います。
[McpServerResourceType]
public static class DocsResource
{
[McpServerResource(UriTemplate = "docs://{path}"), Description("ドキュメントの内容を返す")]
public static string GetDoc(string path) => File.ReadAllText($"./docs/{path}");
}
[McpServerPromptType]
public static class ReviewPrompt
{
[McpServerPrompt, Description("コードレビュー用のプロンプトを生成する")]
public static string CodeReview(string language) =>
$"次の{language}コードをレビューし、バグとスタイルの問題を指摘してください。";
}Toolとの書き方の違いは、Resourceが UriTemplate でクライアント側の参照方法(docs://xxx のようなURI)を宣言する点です。ToolがLLMの判断で呼び出される「実行可能な処理」であるのに対し、Resourceはクライアントが明示的にURIを指定して読みにいく「参照可能なデータ」、Promptは呼び出し時に引数を渡してテキストを組み立てる「テンプレート」という役割の違いがあります。
まとめ
C#でMCPサーバーを作る最短経路は、ModelContextProtocol パッケージを追加し [McpServerToolType] / [McpServerTool] を付けたstaticクラスを1つ書くことです。ローカル用途はstdio、複数クライアントからの接続が必要ならASP.NET Core経由のHTTPトランスポートに切り替えます。HTTPサーバーを公開する際は、CORSではなくホスト名検証(AllowedHosts)でDNSリバインディングを防ぐことを忘れないでください。動作確認は公式SDKのクライアントサンプルでToolの一覧取得と実行を試すだけで完結します。