概要
MCPサーバーが気になったので、調べてみた。
AIに聞いた。
自作のMCPサーバーは作れますか。
自作のMCP(Model Context Protocol)サーバーは、Pythonの「FastMCP」というライブラリを使うことで最も簡単に作成できます。
MCPは、Claude DesktopやCursor、ChatGPTなどのAIホストに対し、あなたのPCローカルにあるデータや自作の関数(ツール)を安全に実行・提供するための共通規格です。
ここでは、最も手軽に作れる「Python + FastMCP」を使った自作MCPサーバーの構築手順を分かりやすく解説します。
自作MCPサーバー構築の5ステップ
Pythonのモダンなパッケージ管理ツールである「uv」を使い、最もシンプルな構成でツールを自作してClaude Desktop等に認識させる手順です。
ステップ 作業内容 具体的な実行コマンド / コード
- 開発環境の準備
uv を使って新規プロジェクトを作成し、必要なライブラリをインストールします。
uv init my-mcp-server
cd my-mcp-server
uv add fastmcp
- サーバーコードの実装
プロジェクト内の main.py(または hello.py)を書き換え、AIに提供したい関数を定義します。 下記のコードブロックを参照 - 設定ファイルの編集
AIクライアントがあなたの自作MCPを読み込めるように、指定の config.json にパスを登録します。 下記の「AIホストへの登録方法」を参照 - AIホストの再起動
設定を反映させるため、Claude Desktop や Cursor などのアプリを完全に終了して再起動します。 アプリを右クリックして終了、またはタスクマネージャーから落として再起動します。 - 動作確認
AIとのチャット上で、自作した関数(ツール)が呼び出せるかテストします。 チャット欄で「自作ツールを使って◯◯して」と指示します。
ステップ2:サーバーコード(Python)の記述例
main.py に以下のようなコードを記述します。@mcp.tool() というデコレータ(目印)を付与した関数が、そのままAIの「手足」として機能するようになります。
# main.py
from fastmcp import FastMCP
# MCPサーバーのインスタンスを作成
mcp = FastMCP("MyFirstMCP")
@mcp.tool()
def calculate_tax(price: int) -> str:
"""与えられた金額(税抜)から、10%の消費税込みの金額を計算します。"""
tax_included = int(price * 1.1)
return f"税込み金額は {tax_included} 円です。"
@mcp.tool()
def get_user_memo() -> str:
"""PCローカルに保存されている秘密のメモ帳の内容を読み出します。"""
# 実際にはここにローカルファイルを読み込む処理などを書けます
return "今日のタスク:自作MCPサーバーを完成させること!"
※関数内のトリプルクォーテーション """ で囲まれた説明文(docstring)をAIが読み取り、「いつこのツールを使うべきか」を自分で判断します。
ステップ3:AIホスト(Claude / Cursor)への登録方法
作成したサーバーをAIアプリに認識させるため、各アプリの設定ファイル(JSON)に以下のように追記します。
設定ファイルの保存場所
Claude Desktop (Windows): %APPDATA%\Claude\claude_desktop_config.json
Cursor: .cursor/mcp.json(プロジェクトルート配下) [1] (https://qiita.com/Dinn/items/032e5a7dbaa4c610e973)
記述する内容(JSON)
{
"mcpServers": {
"my-mcp-server": {
"command": "uv",
"args": [
"run",
"--with",
"fastmcp",
"python",
"C:/ユーザー名/パス/への/フォルダ/main.py"
]
}
}
}
※C:/ユーザー名/.../main.py の部分は、あなたが実際にファイルを保存した絶対パスに書き換えてください。
アプリを再起動すると、チャット画面に「コンセントのアイコン」や「Toolが利用可能になった通知」が表示され、AIが自動的にあなたの関数を呼び出すようになります。
MCP(Model Context Protocol)サーバーを叩くには
PythonからMCP(Model Context Protocol)サーバーを「叩く」(=呼び出して利用する)には、MCPの公式Python SDKが提供しているClient機能を使用します。
サーバー側を自作・起動するだけでなく、Pythonスクリプト自身を「MCPクライアント」として動作させ、サーバーが提供するツール(Tools)やリソース(Resources)を実行する流れになります。
?? 事前準備:SDKのインストール
まずはMCPの公式ライブラリをインストールします。
pip install mcp
(もしテスト用などでCLIツールも使いたい場合は pip install "mcp[cli]" を選択してください)
PythonからMCPサーバーを叩く最小コード(Stdio通信)
一般的なMCPサーバーは、標準入出力(Stdio)を介して通信します。PythonスクリプトからローカルのMCPサーバーを起動し、そのツールを呼び出す実装は以下の通りです。
import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
async def main():
# 1. 呼び出したいMCPサーバーの起動コマンドを指定する
# 例:自作の 'server.py' を叩く場合
server_params = StdioServerParameters(
command="python",
args=["server.py"], # サーバーファイルのパス
env=None
)
# 2. stdio_client を使ってサーバーに接続する
async with stdio_client(server_params) as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream) as session:
# 3. 初期化プロセスを実行
await session.initialize()
# 4. サーバーが持っているツールの一覧を取得して確認
tools_result = await session.list_tools()
print("--- 利用可能なツール一覧 ---")
for tool in tools_result.tools:
print(f"ツール名: {tool.name} - {tool.description}")
# 5. 特定のツールを名前と引数を指定して「叩く」
# 例: 'get_weather' というツールに 引数 {'city': 'Tokyo'} を渡す場合
print("\n--- ツールの実行 ---")
response = await session.call_tool(
name="get_weather",
arguments={"city": "Tokyo"}
)
# 結果の表示
print(response.content)
if __name__ == "__main__":
asyncio.run(main())
主要な通信トランスポート(接続方式)の使い分け
MCPサーバーがどこで動いているか(ローカルプロセスか、ネットワーク経由か)によって、接続用オブジェクトを使い分けます。
接続タイプ サーバーの形態 Pythonクライアント側で使う関数
Stdio (標準入出力) コマンドでその場で起動するローカルサーバー mcp.client.stdio.stdio_client
SSE (HTTP) 常時起動しているネットワーク/WEBサーバー mcp.client.sse.sse_client
「C言語のソースコードを自動生成して保存するツール」を持ったMCPサーバーをPythonで作成します。
この構成では、「サーバー側(server.py)」にC言語のファイルを作成するツールを実装し、それを先ほどの「クライアント側(ローカルLLM)」から呼び出すことで、LLMが自発的にC言語のコードをローカル環境に書き出せるようになります。
Windows環境でそのまま動作するように、デスクトップへの保存や拡張子の処理に対応したコードを作成しました。
- サーバー側の実装 (server.py)
まず、MCPサーバー本体を作成します。このサーバーは、LLMから「ファイル名」と「C言語のコード内容」を受け取り、ローカル環境に保存する generate_c_file というツールを提供します。
# server.py
import os
import mcp.server.fastmcp as fastmcp
# MCPサーバーの初期化
mcp = fastmcp.FastMCP("C-Code-Generator")
@mcp.tool()
def generate_c_file(filename: str, code_content: str) -> str:
"""
C言語のソースコードを指定されたファイル名でローカルに保存します。
Args:
filename: 保存するファイル名 (例: 'main.c', 'hello.c')
code_content: 保存するC言語のソースコード本体
"""
# 安全のため、ファイル名に拡張子 '.c' がない場合は自動付与
if not filename.endswith('.c'):
filename += '.c'
try:
# スクリプトと同じディレクトリに保存
with open(filename, "w", encoding="utf-8") as f:
f.write(code_content)
absolute_path = os.path.abspath(filename)
return f"成功: C言語のファイルを保存しました。\nパス: {absolute_path}"
except Exception as e:
return f"エラー: ファイルの保存に失敗しました。理由: {str(e)}"
if __name__ == "__main__":
# Stdioモードでサーバーを起動
mcp.run(transport="stdio")
- ? クライアント(LLM)側のプロンプト修正
前回の統合コード(クライアント側)のシステムプロンプト部分を、「C言語を生成してツールで保存させる」仕様に最適化します。
先ほど作成したクライアントコードの system_prompt と user_query の部分を、以下のように書き換えて実行してください。
# クライアント側コード内の該当箇所を以下に差し替えます
# ユーザーの質問(例: 階乗を計算するC言語プログラムを求める)
user_query = "再帰関数を使って階乗を計算するC言語のプログラムを作って、'factorial.c' という名前で保存してください。"
# システムプロンプトの差し替え
system_prompt = f"""あなたは優秀なC言語のソフトウェアエンジニアです。
ユーザーの要望に応じてC言語のソースコードを設計し、必要であれば提供されたツールを使ってファイルに保存してください。
【利用可能なツール一覧】
{json.dumps(available_tools, ensure_ascii=False, indent=2)}
【回答のルール】
- ユーザーから「C言語のコードを作って保存して」と言われた場合は、必ず以下のJSONフォーマットのみを出力してください。他の解説や文章は絶対に含めないでください。
{{
"thought": "どのようなC言語コードを作成し、なぜそのファイル名にするのかの理由",
"call_tool": {{
"name": "generate_c_file",
"arguments": {{
"filename": "保存するファイル名.c",
"code_content": "ここに具体的なC言語のソースコード全体(#includeからmain関数まで)を記述"
}}
}}
}}
- ツールを実行した後の最終回答では、保存が完了した旨と、生成したコードの簡単な解説をユーザーに伝えてください。
実行手順と動作の流れ
- 同じフォルダ内に server.py と、修正した クライアント側のスクリプト を配置します。
- クライアント側のスクリプトを実行します。
- 動作フロー:
LLM(llm-jp-3)がユーザーの指示を解釈し、generate_c_file ツールを使うためのJSON(C言語コード入り)を出力。
クライアントプログラムがJSONをパースし、裏で動いている server.py のツールを呼び出す。
server.py がローカルフォルダに factorial.c を自動生成する。
LLMが「保存が完了しました!」と最終回答を出力する。
これでLLMが自分でC言語のコードをファイルとして書き出せるようになりました。
c#で
C#でも、MCP(Model Context Protocol)サーバーを自作して、stdio(標準入出力)方式でClaude Desktopなどと連携させることが可能です。
C#で開発する場合、コミュニティによって開発されている非常に完成度の高い非公式SDKである「ModelContextProtocol.NET(McpDotNet)」を利用するのが最もおすすめです。
内部で Console.SetOut() や Console.SetError() などのハンドリングがしっかり行われているため、注意して実装すればC#特有の「stdio汚染」も防ぐことができます。
C#での最小限のMCPサーバーの実装手順と、stdio汚染を徹底的に防ぐためのポイントをまとめました。
準備・プロジェクトの作成
- コンソールアプリの作成
dotnet new console -n MyMcpServer
cd MyMcpServer
- NuGetパッケージの追加
MCPサーバーの機能を提供するライブラリ(McpDotNet)を追加します。
dotnet add package McpDotNet
最小限の自作MCPサーバー実装例 (Program.cs)
C#のトップレベルステートメントを使用した、stdio通信を行うMCPサーバーの最小コードです。
using McpDotNet.Server;
using System.Text.Json.Serialization;
// 1. サーバーの基本情報を設定して初期化
var options = new McpServerOptions
{
Name = "MyCSharpServer",
Version = "1.0.0"
};
var server = new McpServer(options);
// 2. AIが呼び出せる「ツール」を登録する
server.RegisterTool(
name: "calculate_special_tax",
description: "独自の税率(15%)計算を行うツールです。",
handler: async (TaxRequest args) =>
{
// ? 内部のロジック処理
int result = (int)(args.Price * 1.15);
// ?? デバッグログは必ず「Console.Error」に出す(stdio汚染対策)
await Console.Error.WriteLineAsync($"[Log] Tax calculated for price: {args.Price}");
return new TaxResponse { Message = $"独自の計算結果は {result} 円です。" };
}
);
// 3. stdio(標準入出力)のリスナーを開始して、Claudeからの要求を待機
// (このメソッドが完了するまでプログラムは終了せずに動き続けます)
await server.StartStdioAsync();
// --- ツールでやり取りするためのデータ構造(DTO)を定義 ---
public class TaxRequest
{
[JsonPropertyName("price")]
[JsonDescription("計算対象の金額(円)")] // AIが引数を理解するための説明
public int Price { get; set; }
}
public class TaxResponse
{
[JsonPropertyName("message")]
public string Message { get; set; } = string.Empty;
}
C#における「stdio汚染」の超重要チェックポイント
C#のコンソールアプリケーションをMCPサーバーとして動かす際、最も注意すべきなのは Console.Write や Console.WriteLine を絶対にそのまま使わないこと です。
- Console.Out(標準出力)はライブラリに乗っ取られる
server.StartStdioAsync() を呼び出すと、SDKが onsole.OpenStandardOutput() や Console.OpenStandardInput() を使って、Claudeとのメッセージ送受信スレッドを確立します。
この状態で、開発者がうっかり Console.WriteLine("Debug") を実行してしまうと、Claude宛のJSONデータストリームに「Debug」というただの文字列が混入し、一発でパースエラー(stdio汚染)になります。
- デバッグ出力は Console.Error.WriteLine に統一
上記のコード例にあるように、何かログを出したい場合は必ず Console.Error.WriteLine(標準エラー出力) を使用してください。Claude Desktopは、MCPサーバーの標準エラー出力(stderr)をエラーとはみなさず、自身のログファイルに逃がしてくれる仕様になっているため、通信を阻害しません。
- サードパーティ製ライブラリの「勝手なコンソール出力」に注意
例えば、データベース(EF Coreなど)やHTTP通信のライブラリ(HttpClientなど)を組み込んだ際、ライブラリのログ出力先がデフォルトで Console(標準出力)になっていると、裏で自動的にSQLクエリやリクエストURLがコンソールに出力され、stdio汚染を引き起こします。
対策: 外部ライブラリを導入する場合は、ロギング(Microsoft.Extensions.Loggingなど)の設定で、出力先をコンソール(Console)ではなく、デバッグ(Debug)やトレース、あるいはファイル(Serilogなど)に向けるよう必ず確認してください。
Claude Desktopへの登録方法
C#(.NET)で作ったサーバーは、ビルドして生成された .exe ファイルを直接指定して動かすことができます。
- プロジェクトをリリースビルドします。
dotnet publish -c Release -r win-x64 --self-contained
- %APPDATA%\Claude\claude_desktop_config.json を開き、以下のように追記します。
{
"mcpServers": {
"my-csharp-server": {
"command": "C:\\path\\to\\your\\MyMcpServer\\bin\\Release\\net8.0\\win-x64\\publish\\MyMcpServer.exe",
"args": []
}
}
}
※exeを直接叩くため、args(引数)は空で構いません。
C#(.NET 8+)と McpDotNet ライブラリを使用した、実用的なMCP Tools(ツール)のサンプルコードを作成しました。
AIが実務でよく使う「ファイル操作」「Web API連携(お天気情報)」「システムのステータス取得」の3つの実用的なツールを組み込んだ、そのままコピペして動かせる Program.cs の完全なサンプルです。
実用的なMCPツールの実装サンプル (Program.cs)
using McpDotNet.Server;
using System.Text.Json.Serialization;
using System.ComponentModel;
// 1. サーバーの初期化
var options = new McpServerOptions
{
Name = "UsefulCSharpToolsServer",
Version = "1.0.0"
};
var server = new McpServer(options);
// ==========================================
// ツール1: 指定ディレクトリのファイル一覧を取得する
// ==========================================
server.RegisterTool(
name: "list_directory_files",
description: "指定されたローカルフォルダ内のファイルとフォルダの一覧を取得します。",
handler: async (DirectoryRequest args) =>
{
try
{
if (!Directory.Exists(args.FolderPath))
{
return new ToolResponse { IsError = true, Message = $"エラー: フォルダ '{args.FolderPath}' が見つかりません。" };
}
var entries = Directory.GetFileSystemEntries(args.FolderPath)
.Select(p => Path.GetFileName(p));
string resultText = $"【{args.FolderPath} の中身】\n" + string.Join("\n", entries);
return new ToolResponse { IsError = false, Message = resultText };
}
catch (Exception ex)
{
await Console.Error.WriteLineAsync($"[Error] {ex.Message}"); // ? stdio汚染対策
return new ToolResponse { IsError = true, Message = $"エラーが発生しました: {ex.Message}" };
}
}
);
// ==========================================
// ツール2: 外部のWeb APIから天気予報を取得する
// ==========================================
server.RegisterTool(
name: "get_weather_forecast",
description: "日本の指定された地域(都道府県など)の簡易的な天気予報を取得します。",
handler: async (WeatherRequest args) =>
{
try
{
// 本来は気象庁APIやOpenWeatherMap等にアクセスしますが、ここではサンプルとして擬似返却
// 外部通信(HttpClient)を使う際、ライブラリのログが標準出力(Console)に漏れないよう注意してください。
await Console.Error.WriteLineAsync($"[Log] 天気検索リクエスト: {args.AreaName}");
string weather = args.AreaName.Contains("東京") ? "晴れ ??" : "くもり ??";
string temp = args.AreaName.Contains("東京") ? "22℃" : "18℃";
return new ToolResponse {
IsError = false,
Message = $"【{args.AreaName}の天気予報】\n天気: {weather}\n気温: {temp}\n※これはC# MCPサーバーから取得したデータです。"
};
}
catch (Exception ex)
{
return new ToolResponse { IsError = true, Message = $"天気情報の取得に失敗: {ex.Message}" };
}
}
);
// ==========================================
// ツール3: PCのシステム状態(メモリ等)を取得する
// ==========================================
server.RegisterTool(
name: "get_system_status",
description: "ホストPCのシステム情報(OSや現在時刻など)を取得します。",
handler: async (SystemStatusRequest args) =>
{
var os = Environment.OSVersion.ToString();
var dotNetVersion = Environment.Version.ToString();
var now = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
string status = $"【システムステータス】\nOS: {os}\n.NET Version: {dotNetVersion}\nサーバー現在時刻: {now}";
return new ToolResponse { IsError = false, Message = status };
}
);
// 3. stdioリスナーの開始(AIからの命令を待ち受ける状態にする)
await server.StartStdioAsync();
// ==========================================
// ツールで使用する引数(Request)と戻り値(Response)の定義
// ==========================================
// ツール1用
public class DirectoryRequest
{
[JsonPropertyName("folderPath")]
[JsonDescription("一覧を取得したいローカルフォルダのフルパス(例: C:\\Users\\Public)")]
public string FolderPath { get; set; } = string.Empty;
}
// ツール2用
public class WeatherRequest
{
[JsonPropertyName("areaName")]
[JsonDescription("天気を調べたい地域名(例: 東京都、大阪府など)")]
public string AreaName { get; set; } = string.Empty;
}
// ツール3用 (引数がない場合でも空のクラスが必要です)
public class SystemStatusRequest { }
// 共通の戻り値構造
public class ToolResponse
{
[JsonPropertyName("isError")]
public bool IsError { get; set; }
[JsonPropertyName("message")]
public string Message { get; set; } = string.Empty;
}
C#でツールを実装するときのポイント
- [JsonDescription] 属性が命:
引数のプロパティに付与している [JsonDescription("...")] は非常に重要です。AIはこの説明文を読んで「この引数には何を渡せばいいのか」を理解します。 できるだけ具体的で分かりやすい説明(例などを交える)を書いておくと、AIの誤動作が激減します。 - 引数クラスは必須:
たとえ「システム状態の取得」のように、引数を必要としないツールであっても、McpDotNet のジェネリクス制約上、空のクラス(上記の SystemStatusRequest)を定義してハンドラーに渡す必要があります。 - 例外処理(try-catch)の徹底:
ツール内部でエラー(ファイルのアクセス拒否やネットワーク切断など)が起きた場合、プログラム自体がクラッシュして終了してしまうと、Claudeとの接続が切れてしまいます。必ず try-catch で囲み、エラーが起きても「エラーメッセージ」をAIにテキストとして返却するように設計するのがスマートです。
これをビルドし、前述の claude_desktop_config.json に設定すれば、Claude Desktopから「ファイルを調べて」「天気を教えて」「PCの状態は?」と指示するだけで、これらのC#コードが自動実行されるようになります。
テストベンチ
MCPサーバーの開発における「テストベンチ」ですね。
MCPにおけるテストベンチとは、自作したMCPサーバー(今回であればC#で作ったもの)が、AIアプリ(Claude Desktopなど)と正しくやり取りできるかを、AIアプリを介さずに、開発者が手動または自動スクリプトでシミュレート・検証するための環境のことです。
C#での開発において、最も手軽で強力なテストベンチの構築方法(MCP Inspectorの活用)と、C#コード側でできるユニットテスト的な検証方法をまとめました。
- 公式テストベンチ「MCP Inspector」をC#で使う
前述の通り、公式の「MCP Inspector」が最強のビジュアルテストベンチになります。C#のビルド成果物(.exe)を直接指定して、ブラウザ上でツールを叩くことができます。
? テストベンチの起動手順(Windows)
- コマンドプロンプトやPowerShellを開きます。
- 以下のコマンドを実行して、C#でビルドした .exe をインスペクターに喰わせます。
npx -y @modelcontextprotocol/inspector C:\path\to\your\MyMcpServer.exe
-
ブラウザで http://localhost:3000 を開くと、C#コード内に登録した list_directory_files や get_system_status などのツールが画面に並びます。引数を入れてボタンを押すだけで、C#のロジックが正しく動くか、C#側でstdio汚染(余計なコンソール出力)が発生していないかを完璧にデバッグできます。
-
C#コード内だけで完結する「セルフ・テストベンチ」
「外部ツール(npx)を毎回立ち上げるのが面倒」「Visual Studioのデバッグ実行(F5)だけでツールのロジックテストを完結させたい」という場合は、C#のコード内にテストモード(デバッグ用の模擬実行ロジック)を自作してしまうのが一番簡単です。
以下のように、起動時の引数(args)に ---test などの文字が入っていたら、stdio通信を開始せずに、直接ツールの関数を呼び出してコンソール(またはデバッグウィンドウ)に結果を出す「即席テストベンチ」を組み込む手法です。
変更を加えた Program.cs の例
using McpDotNet.Server;
using System.Text.Json.Serialization;
// --- 1. ツールのロジック(ハンドラー)を変数として分離しておく ---
var listDirectoryHandler = async (DirectoryRequest args) =>
{
try
{
if (!Directory.Exists(args.FolderPath))
return new ToolResponse { IsError = true, Message = "見つかりません" };
var entries = Directory.GetFileSystemEntries(args.FolderPath).Select(Path.GetFileName);
return new ToolResponse { IsError = false, Message = string.Join("\n", entries) };
}
catch (Exception ex)
{
return new ToolResponse { IsError = true, Message = ex.Message };
}
};
// --- 2. 起動引数に [--test] があれば、セルフテストベンチとして動作 ---
if (args.Length > 0 && args[0] == "--test")
{
// F5デバッグ時はここを通るようにする
System.Diagnostics.Debug.WriteLine("=== [TestBench] 模擬テストを開始 ===");
// ツールを直接呼び出してテスト
var testInput = new DirectoryRequest { FolderPath = @"C:\Users\Public" };
var result = await listDirectoryHandler(testInput);
System.Diagnostics.Debug.WriteLine($"[結果] IsError: {result.IsError}");
System.Diagnostics.Debug.WriteLine($"[出力]\n{result.Message}");
System.Diagnostics.Debug.WriteLine("=== [TestBench] テスト完了 ===");
return; // プログラムをここで終了させる(stdio通信に行かない)
}
// --- 3. 通常起動時はMCPサーバーとして動作 ---
var options = new McpServerOptions { Name = "MyCSharpServer", Version = "1.0.0" };
var server = new McpServer(options);
// 分離しておいたハンドラーを登録
server.RegisterTool("list_directory_files", "フォルダ内のファイル一覧を取得", listDirectoryHandler);
// 通常のstdio通信を開始
await server.StartStdioAsync();
Visual Studioでのデバッグ方法
-
プロジェクトのプロパティを開き、「デバッグ起動プロファイル」の「アプリケーション引数」に --test を設定します。
-
これで F5キー を押して実行すれば、Claudeと通信することなく、Visual Studioの「出力ウィンドウ」やブレークポイントを使ってツールのロジックを1行ずつステップ実行でテストできます。
-
Claude Desktopに登録する際は引数(args)を空にしておけば、通常通りMCPサーバーとして動作します。
-
MCP公式が定義する「テスト自動化」
もしxUnitやNUnitなどのテスティングフレームワークを使って、CI/CDラインなどで厳密な統合テストを行いたい場合は、McpDotNetのクライアント機能(McpClient)を使って、テストコード側から自作サーバーを叩くという本物のテストベンチ環境を作ることも可能です。
- テストプロジェクト側で McpClient を作成する。
- 自作サーバーの .exe を子プロセスとして起動し、stdioで接続する。
- client.CallToolAsync("list_directory_files", ...) を実行し、期待通りのJSONが返ってくるかをアサート(検証)する。
まずは「1. MCP Inspector」で視覚的に動くか試し、開発中の細かいロジック修正は「2. セルフ・テストベンチ(F5デバッグ)」で行うのが、C#における最も手軽で効率的な開発サイクルです。
FizzBuzzで
C#(McpDotNet)を使った、FizzBuzzを実行するMCPツールと、それを検証するためのテストベンチ(自動テスト・デバッグ環境)の完全な実装サンプルを作成しました。
このコードには、AIが呼び出す「FizzBuzzツール」と、C#単体で動作確認ができる「セルフ・テストベンチ機能」の両方が1つのファイルに含まれています。
FizzBuzzツール & テストベンチ内蔵コード (Program.cs)
using McpDotNet.Server;
using System.Text.Json.Serialization;
using System.ComponentModel;
using System.Text;
// ==========================================
// 1. FizzBuzzのコアロジック(ハンドラー)の定義
// ==========================================
var fizzBuzzHandler = async (FizzBuzzRequest args) =>
{
// バリデーション(例外処理)
if (args.Count < 1 || args.Count > 100)
{
return new FizzBuzzResponse
{
IsError = true,
Message = "エラー: countは 1 から 100 の間で指定してください。"
};
}
var sb = new StringBuilder();
for (int i = 1; i <= args.Count; i++)
{
if (i % 15 == 0) sb.Append("FizzBuzz");
else if (i % 3 == 0) sb.Append("Fizz");
else if (i % 5 == 0) sb.Append("Buzz");
else sb.Append(i);
if (i < args.Count) sb.Append(", ");
}
// ? stdio汚染対策: ログは必ず Console.Error(標準エラー出力)に吐く
await Console.Error.WriteLineAsync($"[Log] FizzBuzz processed successfully up to: {args.Count}");
return new FizzBuzzResponse
{
IsError = false,
Message = sb.ToString()
};
};
// ==========================================
// 2. セルフ・テストベンチ モード (F5デバッグ用)
// ==========================================
// 起動引数に "--test" が指定されていたら、MCPサーバーを起動せずにロジックをテストする
if (args.Length > 0 && args[0] == "--test")
{
Console.Error.WriteLine("=== ? [FizzBuzz TestBench] 模擬テストを開始 ===");
// テストケース1: 正常系 (15まで)
Console.Error.WriteLine("\n[Test 1] 正常系テスト (count = 15):");
var req1 = new FizzBuzzRequest { Count = 15 };
var res1 = await fizzBuzzHandler(req1);
Console.Error.WriteLine($" -> IsError: {res1.IsError}");
Console.Error.WriteLine($" -> Result: {res1.Message}");
// テストケース2: 異常系 (境界値エラー)
Console.Error.WriteLine("\n[Test 2] 異常系テスト (count = 101):");
var req2 = new FizzBuzzRequest { Count = 101 };
var res2 = await fizzBuzzHandler(req2);
Console.Error.WriteLine($" -> IsError: {res2.IsError}");
Console.Error.WriteLine($" -> Result: {res2.Message}");
Console.Error.WriteLine("\n=== ? [FizzBuzz TestBench] テスト完了 ===");
return; // プログラムをここで安全に終了(stdio通信には進まない)
}
// ==========================================
// 3. 通常起動モード (MCPサーバーとして動作)
// ==========================================
var options = new McpServerOptions
{
Name = "FizzBuzzMcpServer",
Version = "1.0.0"
};
var server = new McpServer(options);
// ツールを登録(AI向けの説明を添えて)
server.RegisterTool(
name: "execute_fizzbuzz",
description: "1から指定された数(最大100)までのFizzBuzzの計算結果をカンマ区切りの文字列で返します。",
handler: fizzBuzzHandler
);
// stdio(標準入出力)での通信を開始(Claude Desktop等の接続を待機)
await server.StartStdioAsync();
// ==========================================
// データの構造(DTOクラス)の定義
// ==========================================
public class FizzBuzzRequest
{
[JsonPropertyName("count")]
[JsonDescription("FizzBuzzをいくつまで実行するか(1?100の整数)")]
public int Count { get; set; }
}
public class FizzBuzzResponse
{
[JsonPropertyName("isError")]
public bool IsError { get; set; }
[JsonPropertyName("message")]
public string Message { get; set; } = string.Empty;
}
このコードを使った2通りのテスト方法
テスト方法A:Visual Studio / VS Code での「セルフ・テスト」
- プロジェクトのデバッグ引数(起動オプション)に --test を設定して実行(F5)します。
- すると、stdio通信を汚染することなく、コンソール上でダイレクトにFizzBuzzの実行結果やエラーハンドリングが正しいかを確認できます。
- fizzBuzzHandler の中にブレークポイントを貼れば、値がどう処理されているか1行ずつステップ実行でデバッグ可能です。
テスト方法B:公式「MCP Inspector」による「AI視点でのテスト」
- --test 引数を外してビルド(または直接実行)し、MCP Inspectorから呼び出します。
npx -y @modelcontextprotocol/inspector dotnet run
- ブラウザで http://localhost:3000 を開きます。
- Tools タブに execute_fizzbuzz が表示されるので、count に 20 などを入力して 「Execute」 をクリックします。
- 下記のような綺麗なJSONレスポンスが、stdio汚染によるパースエラーを起こさずに返ってくれば大成功です!
{
"isError": false,
"message": "1, 2, Fizz, 4, Buzz, Fizz, 7, 8, Fizz, Buzz, 11, Fizz, 13, 14, FizzBuzz"
}
サーバーがソースコード(テストベンチ)を生成して書き出す
失礼いたしました!「AI(Claudeなど)から要求された数まで、C#のサーバー自身がテストベンチのC#ソースコードを動的に自動生成してファイルとして書き出すツール」という意味ですね。
AIから count を指定されると、「その数までのFizzBuzzを検証するための本格的なxUnit(またはNUnit)テストコード(.cs)」をサーバーが自動生成してローカルPCに保存する、という超実践的なMCPツールのサンプルを作成しました。
サーバーがソースコード(テストベンチ)を生成して書き出す実装例
using McpDotNet.Server;
using System.Text.Json.Serialization;
using System.ComponentModel;
using System.Text;
using System.IO;
var options = new McpServerOptions
{
Name = "CodeGeneratorMcpServer",
Version = "1.0.0"
};
var server = new McpServer(options);
// ==========================================
// ツール: FizzBuzzを検証するC#のテストベンチコードを生成・保存する
// ==========================================
server.RegisterTool(
name: "generate_fizzbuzz_testbench",
description: "指定された数までのFizzBuzzロジックを検証するための、C#のxUnitテストベンチのソースコード(.cs)を生成して指定フォルダに保存します。",
handler: async (GenerateRequest args) =>
{
try
{
// 1. 出力先フォルダのチェック
if (!Directory.Exists(args.OutputDirectory))
{
Directory.CreateDirectory(args.OutputDirectory);
}
// 2. 指定された数に基づいて、xUnit形式のテストケース([InlineData])を動的に生成
var testCasesCode = new StringBuilder();
for (int i = 1; i <= args.Count; i++)
{
string expected = i % 15 == 0 ? "FizzBuzz" :
i % 3 == 0 ? "Fizz" :
i % 5 == 0 ? "Buzz" : i.ToString();
// xUnitのインラインデータ形式でソースに埋め込む
testCasesCode.AppendLine($" [InlineData({i}, \"{expected}\")]");
}
// 3. 完全なC#のソースコード(テストベンチ)を組み立てる
var sourceBuilder = new StringBuilder();
sourceBuilder.AppendLine("using Xunit;");
sourceBuilder.AppendLine();
sourceBuilder.AppendLine("namespace FizzBuzz.Tests");
sourceBuilder.AppendLine("{");
sourceBuilder.AppendLine(" public class FizzBuzzTestBench");
sourceBuilder.AppendLine(" {");
sourceBuilder.AppendLine(" // サーバーが自動生成したテストケース一覧");
sourceBuilder.Append(testCasesCode.ToString());
sourceBuilder.AppendLine(" [Theory]");
sourceBuilder.AppendLine(" public void Test_FizzBuzz_Logic(int input, string expected)");
sourceBuilder.AppendLine(" {");
sourceBuilder.AppendLine(" // Act");
sourceBuilder.AppendLine(" string result = GetFizzBuzzValue(input);");
sourceBuilder.AppendLine();
sourceBuilder.AppendLine(" // Assert");
sourceBuilder.AppendLine(" Assert.Equal(expected, result);");
sourceBuilder.AppendLine(" }");
sourceBuilder.AppendLine();
sourceBuilder.AppendLine(" // 検証対象のロジック");
sourceBuilder.AppendLine(" private string GetFizzBuzzValue(int i)");
sourceBuilder.AppendLine(" {");
sourceBuilder.AppendLine(" if (i % 15 == 0) return \"FizzBuzz\";");
sourceBuilder.AppendLine(" if (i % 3 == 0) return \"Fizz\";");
sourceBuilder.AppendLine(" if (i % 5 == 0) return \"Buzz\";");
sourceBuilder.AppendLine(" return i.ToString();");
sourceBuilder.AppendLine(" }");
sourceBuilder.AppendLine(" }");
sourceBuilder.AppendLine("}");
// 4. ファイルへの書き出し
string fileName = $"FizzBuzzTestBench_UpTo{args.Count}.cs";
string fullPath = Path.Combine(args.OutputDirectory, fileName);
await File.WriteAllTextAsync(fullPath, sourceBuilder.ToString(), Encoding.UTF8);
// stdio汚染対策のログ
await Console.Error.WriteLineAsync($"[Log] ソースコードを生成しました: {fullPath}");
return new GenerateResponse
{
IsError = false,
Message = $"成功: 指定された{args.Count}件のテストケースを内蔵したC#ソースコード(xUnitテストベンチ)を生成し、保存しました。\n保存先: {fullPath}"
};
}
catch (Exception ex)
{
await Console.Error.WriteLineAsync($"[Error] {ex.Message}");
return new GenerateResponse { IsError = true, Message = $"ソースコードの生成に失敗しました: {ex.Message}" };
}
}
);
// サーバー開始
await server.StartStdioAsync();
// ==========================================
// 引数と戻り値の定義
// ==========================================
public class GenerateRequest
{
[JsonPropertyName("count")]
[JsonDescription("テストベンチに含めるFizzBuzzの最大件数(例: 30 と指定すると 1?30 までのテストコードが生成されます)")]
public int Count { get; set; }
[JsonPropertyName("outputDirectory")]
[JsonDescription("生成したC#ソースファイル(.cs)を保存するローカルPCのフォルダパス(例: C:\\Temp)")]
public string OutputDirectory { get; set; } = string.Empty;
}
public class GenerateResponse
{
[JsonPropertyName("isError")]
public bool IsError { get; set; }
[JsonPropertyName("message")]
public string Message { get; set; } = string.Empty;
}
この「ソース生成ツール」の動き
- Claude DesktopなどのAIに対して、「C:\McpTest フォルダに、30までのFizzBuzzを検証するC#のテストベンチコードを生成して」と指示します。
- AIが自動で generate_fizzbuzz_testbench ツールを選び、count: 30, outputDirectory: "C:\McpTest" を指定してこのC#サーバーを呼び出します。
- サーバー内のロジックが走り、指定フォルダに FizzBuzzTestBench_UpTo30.cs という本物のC#ファイルが書き出されます。
実際に生成されるソースコード(中身)
サーバーからは以下のような、そのままコンパイルして dotnet test が通る綺麗なxUnitのコードが出力されます。
using Xunit;
namespace FizzBuzz.Tests
{
public class FizzBuzzTestBench
{
// サーバーが自動生成したテストケース一覧
[InlineData(1, "1")]
[InlineData(2, "2")]
[InlineData(3, "Fizz")]
// ...中略...
[InlineData(15, "FizzBuzz")]
[Theory]
public void Test_FizzBuzz_Logic(int input, string expected)
{
// Act
string result = GetFizzBuzzValue(input);
// Assert
Assert.Equal(expected, result);
}
// 検証対象のロジック
private string GetFizzBuzzValue(int i)
{
if (i % 15 == 0) return "FizzBuzz";
if (i % 3 == 0) return "Fizz";
if (i % 5 == 0) return "Buzz";
return i.ToString();
}
}
}
このように、MCPサーバー側にテンプレートやコード組み立てのロジックを持たせておけば、AIからの簡単なパラメータ指示だけで、仕様を満たした正確なプログラムソースや自動テストコードをローカルディスクに量産させることができます。
以上。