ASP.NETとC#でOpenAI向けのMCPサーバーを作る際のはまりどころ

Higtyのシステムの作り方

概要

HigLaboでChatGPTからASP.NETのMCPサーバーのエンドポイントを呼び出す機能を作成しました。

この記事では最低限必要なコードとちょっとしたはまりどころについて解説します。


初期化処理

まずはNugetパッケージでModelContextProtocol.AspNetCoreをインストールします。

まだプレビュー版です。


インストール後、Program.csに以下の初期化処理を書きます。

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMcpServer()
    .WithHttpTransport(options =>
    {
        options.Stateless = true;
    })
    .WithToolsFromAssembly()
    .WithPromptsFromAssembly()
    .WithResourcesFromAssembly();

var app = builder.Build();
app.Use(async (context, next) =>
{
    var accept = context.Request.Headers.Accept.ToString();
    if (!accept.Contains("text/event-stream"))
    {
        context.Request.Headers.Accept = $"{accept}, text/event-stream";
    }
    await next();
});
app.MapMcp("/mcp");

普通に作っていくとOpenAIからの呼び出しがエラーになります。

はまりどころが3つもあります。


ステートレスの設定

まずは

options.Stateless = true;

という形でステートレスに設定をしないとうまくいきません。

これがないと「mcp_protocol_error: Session terminated」というエラーが発生します。これはツールを呼び出した直後にサーバー側のセッションが破棄されてしまうことが原因です。Streamable HTTP におけるセッション管理の既知の問題で、OpenAI 側の実装がツール呼び出し後に DELETE /mcp を送ってしまうために起こります。同様の症状は他の開発者からも報告されており、Streamable HTTP ではツール呼び出しが常に「Session terminated」で失敗するという報告が複数あります

https://community.openai.com/t/openai-mcp-client-starts-to-fail-when-moving-from-sse-to-streamable-http/1275728


ステートレスモードではクライアント情報を Mcp-Session-Id ヘッダーにエンコードし、サーバー側でセッションオブジェクトを保持しないため、OpenAI クライアントがセッションを削除しても問題になりません。


ステートレスモードにすると以下の点が変わります。

サーバーは /sse や /message エンドポイントを公開しません。代わりに、クライアントは各リクエストごとに独立した POST を行い、その応答でツールの実行結果を受け取ります。


  1. セッション管理が不要になるので、OpenAI クライアントがセッションを削除しても「Session terminated」が発生しません。
  2. 進捗通知や長時間のストリーミング応答など、サーバー側からの非同期メッセージ送信は利用できません。


ステートレスモードへ切り替えても GetTodaysWeather のように即時に結果を返すツールであれば特に問題はありませんので、まずは上記の変更を試してみてください。


ヘッダーの調整

AddMcpServer().WithHttpTransport() で生成される Streamable HTTP エンドポイントは、MCP 仕様に沿ってリクエスト/レスポンスの形式やヘッダーをかなり厳密に検査します。Streamable HTTP の POST は Accept ヘッダーに application/json と text/event-stream の両方が含まれていることを要求します。実装では Accept ヘッダーの配列を走査し、双方が含まれていなければ 406 エラーを返し、その JSON‐RPC エラーコードとして –32600(Invalid Request)を返します。OpenAI のツール呼び出しでは通常ヘッダーにapplication/jsonのみセットされているためこのチェックに引っ掛かります。


以下のようにヘッダーを修正します。

app.Use(async (context, next) =>
{
    // リクエストに text/event-stream を強制追加。本来であればUser-AgentがOpenAIからかどうかもチェックしたほうが良いかも?
    var accept = context.Request.Headers.Accept.ToString();
    if (!accept.Contains("text/event-stream"))
    {
        context.Request.Headers.Accept = $"{accept}, text/event-stream";
    }
    await next();
});

Accept ヘッダーを書き換えるミドルウェアは app.MapMcp() より 前 に登録する必要があります。


ルートページの上書きの防止

最後にMCPのマップするパスをルート以外に設定します。

app.MapMcp("/mcp");

通常ドメインのルートはトップページとしてサービスの概要などのページになっているはずです。引数無しの

app.MapMcp();

だとルートのページがMCPサーバー用に使われて正しく表示されなくなります。



ツールの作成

ツールの作成はクラスを作成してその中にメソッドを定義し、クラストメソッドのそれぞれに属性をセットするだけです。

using ModelContextProtocol.Server;
using System.ComponentModel;

namespace HigLaboApp.Core;

[McpServerToolType]
public class WeatherTools
{
    [McpServerTool(ReadOnly = true), Description("本日の天気を取得します。")]
    public string GetTodaysWeather()
    {
        var today = DateTime.Now.ToDateOnly();
        var temperatureC = Random.Shared.Next(-20, 55);
        return $"本日の天気({today}):{temperatureC}°C、暑いです";
    }
}



動作確認

OpenAIのテスト用ページから動作確認ができます。

https://platform.openai.com/chat/edit?models=gpt-4.1


+AddからMCPサーバーを選択し https://your-service.com/mcp という感じでサーバーURLを入力してあげるとテストできるようになります。