148
155

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

MCP 基礎知識 & MCP 公式の MCP サーバ自作チュートリアル (C#) やってみた

Last updated at Posted at 2025-08-13

この記事は 100% 人間(私)が書きました。

(というか もともと私の記事は全て「自分の勉強メモ」の意味が強いので、(特別な記載のない限り)全記事 人間 100 です。)

MCP 公式ドキュメントの
MCP サーバ自作チュートリアルに
C# があるのを発見しました!(Python しか無いと思ってた)

チュートリアル

コード

ちょっとやってみるか〜

image.png

作るもの

シンプルなお天気予報 MCP サーバ。

2 つの ツール を持ちます:

  • get_alerts
  • get_forecast

(これもともと Python だったので、ネーミングが Python っぽい(スネークケース)ですね)

事前知識

事前知識①: MCP の機能 3 兄弟

MCP の 3 つのプリミティブな機能:

機能名 説明 原文
1 リソース 読み取り専用のコンテンツ File-like data that can be read by clients (like API responses or file contents)
2 ツール LLM から (ユーザの承認によって) 呼ばれる (call) 機能 (functions)。この三兄弟で一番使うと思う Functions that can be called by the LLM (with user approval)
3 プロンプト プロンプトテンプレートを提供。(良質なプロンプトは長くなりがちだけど、このテンプレート機能により クソ長呪文プロンプト をユーザが使いやすくなる) Pre-written templates that help users accomplish specific tasks

このチュートリアルでは、↑の MCP 機能三兄弟の ②ツール に焦点を当てていきます。

事前知識②: MCP の通信(トランスポート)

通信規格

MCP サーバと MCP クライアントの間で情報のやりとりする仕組み(トランスポート)については、

  1. 標準入出力 (stdio)
  2. Streamable HTTP (すとりーまぶる HTTP) (旧: HTTP + SSE)

の 2 つが定義されています。

通信方法 いつ使う
1 標準入出力 (stdio) ローカル MCP サーバ(MCP ホストと同一コンピュータ上に MCP サーバが起動している)
2 Streamable HTTP
/ HTTP + SSE
リモート MCP サーバ (MCP ホストと MCP サーバの通信を HTTP を介してやりとり)

送受信されるデータの形式: JSON-RPC 2.0

送受信される全てのデータの形式は JSON-RPC 2.0 という形式にしたがっています。
呼び出したいメソッド名とその引数を JSON オブジェクトとして送ります。

JSON-RPC は、 RPC (Remote Procedure Call) を実現するプロトコルのひとつです。

Remote Procedure Call (遠隔 手続き 呼び出し) とは、
他のコンピュータのプログラムを呼び出して実行させるための技術、またはそのためのプロトコルです。

JSON-RPC 2.0 の仕様については、

皆様は 実際の中身を見た方が早いと思うので
公式ドキュメントから リクエスト/レスポンス例を引用します

リクエスト
{"jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": 1}

これは「subtract」という関数に、引数 42, 23 を渡しているということです

(subtract とは、英語で「引き算」を意味します。)

レスポンス
{"jsonrpc": "2.0", "result": 19, "id": 1}

subtract(42, 23) の結果 (result) の 19 が返ってきています。

ちなみに "jsonrpc": "2.0" ですが、
1.0 では このフィールドは存在しなかったのですが
2.0 では (互換性問題を解消するため) jsonrpc フィールドが追加されました。(「2.2 Differences between 1.0 and 2.0」より)

事前注意:標準入出力ベースのサーバを作るときは標準出力を使わない!

標準入出力ベースのサーバ (STDIO-based servers) を作るときは
標準出力 (Standard output) を使わないようにしましょう。

標準出力を書くと JSON-RPC messages に干渉し、サーバを壊してしまうおそれがあります。

(HTTP-based servers 実装時は気にしなくて良いです。HTTP レスポンスに何も干渉しないので)

チュートリアル開始

.NET 環境の確認

このチュートリアルでは .NET 8 以上が求められているので、環境を確認しましょう。

dotnet --version

image.png

空の C# プロジェクトを new する

# 作業ディレクトリを作る
mkdir weather
cd weather

# C# プロジェクトの初期化
dotnet new console

無事に C# コンソールプロジェクトの new が完了したら、
そのプロジェクトを Visual Studio や VS Code などで開くと、
こんな感じになります↓(画面は VS Code のもの)

image.png

諸々のパッケージを入れる

MCP SDK と .NET Hosting の NuGet パッケージを入れます

(もし git 管理する人は、この辺で .gitignore を追加しておきましょう)

# MCP SDK の NuGet package を入れる
dotnet add package ModelContextProtocol --prerelease

# .NET Hosting の NuGet package を入れる
dotnet add package Microsoft.Extensions.Hosting

サーバの構築

Program.cs を開き、中身をこれに書き換えます

Program.cs
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using ModelContextProtocol;
using System.Net.Http.Headers;

var builder = Host.CreateEmptyApplicationBuilder(settings: null);

builder.Services.AddMcpServer()
    .WithStdioServerTransport()
    .WithToolsFromAssembly();

builder.Services.AddSingleton(_ =>
{
    var client = new HttpClient() { BaseAddress = new Uri("https://api.weather.gov") };
    client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("weather-tool", "1.0"));
    return client;
});

var app = builder.Build();

await app.RunAsync();

このコードは、Model Context Protocol SDK を使用して
標準入出力トランスポートを備えた MCP サーバを作成しています。

標準入トランスポートのサーバ (servers using STDIO transport) を作る際は、
ApplicationHostBuilder を作るとき CreateDefaultBuilder ではなく CreateEmptyApplicationBuilder を使うようにしましょう。
これにより、サーバがコンソールに余計なメッセージを書き込まなくなります。

天気 API ヘルパー関数

JSON リクエストの処理を簡素化する、
HttpClient の拡張クラスを作ります。

Tools というフォルダを作って、その下に HttpClientExt.cs を作ります。

Tools/HttpClientExt.cs
using System.Text.Json;

internal static class HttpClientExt
{
    public static async Task<JsonDocument> ReadJsonDocumentAsync(this HttpClient client, string requestUri)
    {
        using var response = await client.GetAsync(requestUri);
        response.EnsureSuccessStatusCode();
        return await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
    }
}

次に、
National Weather Service API からの応答をクエリして変換するハンドラーを持つクラスを定義します。

Tools/WeatherTools.cs を作成します

Tools/WeatherTools.cs
using ModelContextProtocol.Server;
using System.ComponentModel;
using System.Globalization;
using System.Text.Json;

namespace QuickstartWeatherServer.Tools;

[McpServerToolType]
public static class WeatherTools
{
    [McpServerTool, Description("Get weather alerts for a US state.")]
    public static async Task<string> GetAlerts(
        HttpClient client,
        [Description("The US state to get alerts for.")] string state)
    {
        using var jsonDocument = await client.ReadJsonDocumentAsync($"/alerts/active/area/{state}");
        var jsonElement = jsonDocument.RootElement;
        var alerts = jsonElement.GetProperty("features").EnumerateArray();

        if (!alerts.Any())
        {
            return "No active alerts for this state.";
        }

        return string.Join("\n--\n", alerts.Select(alert =>
        {
            JsonElement properties = alert.GetProperty("properties");
            return $"""
                    Event: {properties.GetProperty("event").GetString()}
                    Area: {properties.GetProperty("areaDesc").GetString()}
                    Severity: {properties.GetProperty("severity").GetString()}
                    Description: {properties.GetProperty("description").GetString()}
                    Instruction: {properties.GetProperty("instruction").GetString()}
                    """;
        }));
    }

    [McpServerTool, Description("Get weather forecast for a location.")]
    public static async Task<string> GetForecast(
        HttpClient client,
        [Description("Latitude of the location.")] double latitude,
        [Description("Longitude of the location.")] double longitude)
    {
        var pointUrl = string.Create(CultureInfo.InvariantCulture, $"/points/{latitude},{longitude}");
        using var jsonDocument = await client.ReadJsonDocumentAsync(pointUrl);
        var forecastUrl = jsonDocument.RootElement.GetProperty("properties").GetProperty("forecast").GetString()
            ?? throw new Exception($"No forecast URL provided by {client.BaseAddress}points/{latitude},{longitude}");

        using var forecastDocument = await client.ReadJsonDocumentAsync(forecastUrl);
        var periods = forecastDocument.RootElement.GetProperty("properties").GetProperty("periods").EnumerateArray();

        return string.Join("\n---\n", periods.Select(period => $"""
                {period.GetProperty("name").GetString()}
                Temperature: {period.GetProperty("temperature").GetInt32()}°F
                Wind: {period.GetProperty("windSpeed").GetString()} {period.GetProperty("windDirection").GetString()}
                Forecast: {period.GetProperty("detailedForecast").GetString()}
                """));
    }
}

サーバの実行

最後に、次のコマンドを使用してサーバを実行します。

dotnet run

これにより、サーバーが起動し、標準入出力でのリクエストをリッスンします。

自作サーバのテスト:MCP インスペクターを使う

原文では初手 Claude Desktop でテストしていますが、
まずは MCP インスペクターで疎通確認をしたほうが良いと思いましたので、追記します。

MCP インスペクターとは、MCP サーバのテスト・デバッグするための開発者ツールです。

実行するプロジェクトと同じディレクトリでこれを叩きます

npx @modelcontextprotocol/inspector

image.png

するとローカルで web サーバが起動してブラウザが立ち上がります。

image.png

接続設定を以下のように入れます:

項目
Transport Type STDIO
Command dotnet
Arguments run

そして(MCP サーバが dot net run された状態で)「Connect」を押すと
接続されます。

右の「List Tools」を押すとエンドポイントの一覧が表示されます。(ここでは「get_forecast」と「get_alearts」の2つ)

image.png

ちゃんと動いているようですね!

Claude Desktop で、自作サーバのテストをする

今回は、MCP クライアントとして Claude Desktop を使います。

Claude Desktop の設定ファイル claude_desktop_config.json を編集します。

VS Code で編集する場合のコマンドはこれです:

mac の人
code ~/Library/Application\ Support/Claude/claude_desktop_config.json
Windows の人
code $env:AppData\Claude\claude_desktop_config.json

image.png

{
  "mcpServers": {
    "weather": {
      "command": "dotnet",
      "args": ["run", "--project", "/プロジェクトへの絶対PATH", "--no-build"]
    }
  }
}

トラブルシューティング

Claude Desktop から MCP サーバに繋がらない

image.png

原文ではコマンド記載について dotnet だけですが、
私の環境だとこれでは動かず、
dotnet をフルパス指定 (/usr/local/share/dotnet/dotnet) (mac) したら動きました。

詳細は別記事に書きました

Claude Desktop との接続を確認

テキストボックスの左下の "Search and tools" アイコンを押します。

image.png

そこで「weather」と自作 MCP サーバが出てきたら OK です。

自作 MCP サーバを叩いてみる

ツール設定アイコンが表示された場合は、Claude Desktop で次のコマンドを実行してサーバーをテストできます。

  • サクラメントの天気はどうですか?
  • テキサス州で発令中の気象警報は何ですか?

するとこんなダイアログが表示されます

image.png

「"weather" という MCP サーバの "get_forecast" というツールを使用してもいいですか?」

「許可する」を押すと、MCP サーバが叩かれて、
こんな感じになります

image.png
 

やったぜ!
(ちなみに、この MCP サーバが叩いているお天気 API は、米国だけしか対応していないので、テストできるのは米国のエリアのみです)

何が起きているのか

  1. 人間 (あなた) が Claude Desktop で質問を入力する
  2. クライアント (Claude Desktop) があなたの質問を Claude (LLM) に送る
  3. Claude (LLM) は利用可能なツールを分析し、どのツールを使用するかを決定
  4. クライアント (Claude Desktop) は MCP サーバーを通じて選択されたツールを実行する
  5. 結果は Claude (LLM) に送り返される
  6. Claude (LLM) は自然言語で応答する
  7. クライアント (Claude Desktop) に応答が表示される

まとめ

楽しい

追記

有識者(mattn 先生)から MCP のツールの命名についてのアドバイスをいただきました!

要約:MCP のツールの名前はグローバルに相当するから、他のツールとの名前のコンフリクトを避けるために、長くなってもいいからもっと詳細な名前をつけたほうがいい

以下引用

おそらく今後、他の MCP を自作された際に気付くかもですが、MCP のツールの名前はグローバルに相当するので、複数の MCP が混在するとバッティングする可能性や、コンテキストが異なる事で get_alerts は選ばれなくなる可能性が高いです。
若干ウザい名前になってもいいので get_weather_forecast や get_weather_alerts という名前にした方が良いかも。
こうすることで get_earthquake_alerts 等と区別できる様になって、地震や天気の会話でそれぞれが選ばれやすくなります。
https://x.com/mattn_jp/status/1955872341273272753

なるほどオブザイヤー!!!!!
ありがとうございます!!!

追記2:トレンド2位ありがとうございます!

この記事がトレンド入りしていました!!
たくさんの方に読んでいただけて大変嬉しいです!ありがとうございます!

image.png

148
155
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
148
155

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?