MCP Server の試作してみる(C#版)

目的

MCP Server の試作を行い、MCP の機能を確認する。

  • 既存のデータベースを AI エージェントから使えるようにする.
  • たとえば「2025年08月02日の春日部の予約状況は?」とチャットで質問したときに、

  日付「2025年08月02日」、店舗「春日部」を見つけ出して、既存のデータベースから予約状況を取得する。

ちょっと前は RAG(Retrieval-Augmented Generation)と呼ばれていたが、今は MCP(Model Context Protocol)と呼ばれている。vscode の Chat 機能から特定の MCP Server にアクセスして、既存のシステムを参照できるようになる。

構成

  • – kkc-webapi: 既存システムを Laravel で実装した Web API
      OpenAPI 仕様が定義されていて、これを使う
  • – kkc-mcp-server: MCP Server の実装(標準入出力形式)
      Chat からの入力を受け取り、必要な情報を kkc-webapi から取得して、Chat に返す
  • – kkc-mcp-server-api: MCP Server の実装(HTTP API 形式)
      Chat からの入力を受け取り、必要な情報を kkc-webapi から取得して、Chat に返す
  • – kkc-mcp-client: MCP Server にアクセスするクライアント
      vscode の Chat 機能ではなく、独自の GUI アプリを提供する

kkc-webapi の作成

既存システムが独自 MVC と ChakPHP のハイブリッドになっているので、これを Laravel の Web API 形式に書き直します。手作業で書き直すのは大変なので、

  • ddl.md の作成
  • openapi.yaml の作成

ということで、既存データベースを CRUD できる Web API を自動生成してしまいます。このあたりはまめに php artisan make コマンドを使うことになるのですが、Claude Sonnet を使って作成してもらいます。

プロンプトでは、以下のように指示するだけで十分です。

ddl.md に従って CRUD できる openapi 仕様を作成して。
openapi.yaml に従って Laravel の Controller を作成して。

実際のところは、途中でトークンを忘れてしまうらしく、なんどか修正を指示しないとけないのですが、それでも手作業でつくるよりはるかに早いです。openapi.yaml についても、すべてを一遍に作成するのではなく、一定のカテゴリごとに分けてプロンプトで指示をするとうまくいく率が高いです。

このあたり、うまくやらば Claude Code を使って夜間バッチ的に作ることもできるのでしょうが、まあ、そこまでしなくても Claude Sonnet で十分でしょう。今回の場合はテーブル数が20弱ぐらいなのでプロンプトで指示をしていますが、100を超えるような場合は、ddl.md から openapi.yaml を作成して Laravel の Controller を作成する、ツールを作ったほうがよいでしょう。これを「治具」と呼ぶのですが、この手の使い捨てのツールは良く作ります。環境変数などはコードに埋め込みにしてしまうので他のプロジェクトで使うことはできないのでが、あえてそのような汎用性を捨ててしまって、スピードと精度を優先させます。

まあ、そんなこんなで kkc-webapi を作成しておきます。

ModelContextProtocol の活用

MCP Server の実装にあたっては、ModelContextProtocol を活用します。これは、Chat からの入力を受け取り、必要な情報を kkc-webapi から取得して、Chat に返すためのプロトコルです。以前は、JSON 形式のリクエストを作ってちまちまやっていたのですが、ModelContextProtocol を使うことで、より効率的にデータを取得できるようになります。現在はプレビュー版 0.3.0-preview.3 なのが難点ではあるのですが。

現状で MCP Server は標準入出力型と HTTP API 型の2つの実装があります。

  • 標準入出力型: コマンドラインから実行し、標準入力と標準出力を使ってやり取りする。
  • HTTP API 型: HTTP リクエストを受け取り、レスポンスを返す形式。

ということになっていて、ModelContextProtocol を使った標準入出力型の型の例があります。

Build a Model Context Protocol (MCP) server in C# – .NET Blog
https://devblogs.microsoft.com/dotnet/build-a-model-context-protocol-mcp-server-in-csharp/

ただし、この標準入出力型には難点があって、ストリーム形式なのでローカルな MCP Server を建てる必要が(たぶん)あります。レスポンス&リクエストが一連のストリームになっているので、エラーを返すことができません。適当にログを出力する必要がでてきます。お手軽ではあるのですが、あまり実用的ではないかもしれません。

最初に標準入出力型を実装してみたのですが、あまりうまく拡張できないので HTTP API 型の kkc-mcp-server-api で実装しなおしています。

実は実装自体はあまり手間ではなくて、

dotnet new console

でプロジェクトを作成した後に、以下のプロンプトで作成して貰っています。

内部で kkc-webapi を呼び出す MCP Server を作成して。

kkc-mcp-server は、vscode のチャットと OpenAPI の kkc-webapi を繋ぐ役目になっています。

動作状況

作成したコードはさておき、vscode の Chat からどういう風に呼び出されるのかを見ておきましょう。

なぜか、powershell のコマンドを実行しようとするのですが、これが HTTP API 型のためなのかよくわかりません。

ひとまず、自前の MCP Server を呼び出すことはできているようです。

実際に利用するときは、vscode の Chat 機能ではなくて、適当なブラウザアプリかデスクトップアプリを作ることになるので、このあたりはもうちょっと作り込みが必要そうです。

コマンドパレットから、MCP Server を指定

コマンドパレットで「MCP サーバーの追加」を見つけ出して、「コマンド(stdio)」か「HTTP」を選択します。

標準入出力型の stdio のほうは、デバッグがしづらいので、ある程度 HTTP API 型のようで調節をしていから、stdio のほうに戻したほうがよいかもしれません。

ちなみに、私の現状の環境では、まだ HTTP API 型の MCP Server から kkc-webapi のほうにパラメーターが正確にわたっていなくて、完成していません。これは近いうちに。

MCP Server の呼び出し

$request = @{
    jsonrpc = "2.0"
    id = "1"
    method = "tools/call"
    params = @{
        name = "search_reservations_by_name"
        arguments = @{
            Store = "春日部"
            ReservationDate = "2025-08-02"
        }
    }
} | ConvertTo-Json -Depth 10

$response = Invoke-RestMethod -Uri "http://localhost:5000" -Method Post -Body $request -ContentType "application/json"
Write-Host "=== 2025年08月02日 春日部店の予約状況 ===" -ForegroundColor Green
$response | ConvertTo-Json -Depth 10

Chat が MCP Server を呼び出すときのコマンドを見ると解るのですが、jsonrpc 形式でリクエストを送信しています。vscode の Chat 部分で「2025年08月02日の「春日部」の予約状況を示して。」と入力すると、うまく JSON RPC 形式のリクエストに変換してくれることがわかります。このために、ユーザーは search_reservations_by_name などの関数名を意識することなく、自然言語を使って指示を与えるあるいは質問をすることができるのです。

このあたりで Chat 形式のユーザ―インターフェースが変わってくるという話です。

まあ、もっとも、予約のチェックのように画面で何かを選択するほうが早い場合もあるので、このチャット形式も善しあしでああるのですが。JR の忘れ物の問い合わせのような検索システムの場合は、入力する項目をうまく誘導するためにこの手のチャット形式が有効でしょう。

kkc-mcp-server-api

Program.cs

using System.Text.Json;
using KkcMcpServerApi.Models;
using KkcMcpServerApi.Services;
using Microsoft.OpenApi.Models;

var builder = WebApplication.CreateBuilder(args);

// ロギング設定
builder.Logging.ClearProviders();
builder.Logging.AddConsole();
builder.Logging.SetMinimumLevel(LogLevel.Information);

// サービス登録
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new OpenApiInfo 
    { 
        Title = "KKC MCP Server API", 
        Version = "v1",
        Description = "Model Context Protocol (MCP) サーバーAPI - kkc-webapi のプロキシとして動作します"
    });
});

// HttpClient設定
builder.Services.AddHttpClient<IWebApiClient, WebApiClient>(client =>
{
    client.BaseAddress = new Uri("http://localhost:8000");
    client.Timeout = TimeSpan.FromSeconds(30);
});

// MCPツールプロバイダー登録
builder.Services.AddScoped<McpToolProvider>();

// CORS設定
builder.Services.AddCors(options =>
{
    options.AddDefaultPolicy(policy =>
    {
        policy.AllowAnyOrigin()
              .AllowAnyMethod()
              .AllowAnyHeader();
    });
});

var app = builder.Build();

app.UseCors();

// MCP over HTTP のルートエンドポイント
app.MapPost("/", async (HttpContext context, McpToolProvider toolProvider) =>
{
    try
    {
        using var reader = new StreamReader(context.Request.Body);
        var jsonRequest = await reader.ReadToEndAsync();
        
        var requestDoc = JsonDocument.Parse(jsonRequest);
        var root = requestDoc.RootElement;
        
        if (!root.TryGetProperty("method", out var methodElement))
        {
            return Results.BadRequest(new { error = "Missing method property" });
        }
        
        var method = methodElement.GetString();
        var id = root.TryGetProperty("id", out var idElement) ? idElement.GetString() : null;
        
        object? response = method switch
        {
            "initialize" => new
            {
                jsonrpc = "2.0",
                id = id,
                result = new
                {
                    protocolVersion = "2024-11-05",
                    capabilities = new
                    {
                        tools = new { }
                    },
                    serverInfo = new
                    {
                        name = "kkc-mcp-server-api",
                        version = "1.0.0"
                    }
                }
            },
            "tools/list" => new
            {
                jsonrpc = "2.0",
                id = id,
                result = new
                {
                    tools = new object[]
                    {
                        new
                        {
                            name = "search_reservations",
                            description = "予約を検索します(ID指定)",
                            inputSchema = new
                            {
                                type = "object",
                                properties = new
                                {
                                    storeId = new { type = "integer", description = "店舗ID" },
                                    reservationDate = new { type = "string", description = "予約日 (YYYY-MM-DD)" },
                                    customerName = new { type = "string", description = "顧客名" },
                                    limit = new { type = "integer", description = "取得件数", @default = 10 }
                                }
                            }
                        },
                        new
                        {
                            name = "search_reservations_by_name",
                            description = "予約を検索します(名前指定)",
                            inputSchema = new
                            {
                                type = "object",
                                properties = new
                                {
                                    store = new { type = "string", description = "店舗名" },
                                    areaGroup = new { type = "string", description = "エリアグループ名" },
                                    customerName = new { type = "string", description = "顧客名" },
                                    reservationDate = new { type = "string", description = "予約日 (YYYY-MM-DD)" },
                                    limit = new { type = "integer", description = "取得件数", @default = 10 }
                                }
                            }
                        },
                        new
                        {
                            name = "get_calendar",
                            description = "カレンダー情報を取得します",
                            inputSchema = new
                            {
                                type = "object",
                                properties = new
                                {
                                    date = new { type = "string", description = "日付 (YYYY-MM-DD)" },
                                    areaGroupId = new { type = "integer", description = "エリアグループID" }
                                },
                                required = new[] { "date", "areaGroupId" }
                            }
                        }
                    }
                }
            },
            "tools/call" => await HandleToolCall(root, toolProvider, id),
            _ => new
            {
                jsonrpc = "2.0",
                id = id,
                error = new
                {
                    code = -32601,
                    message = $"Method not found: {method}"
                }
            }
        };
        
        return Results.Json(response);
    }
    catch (Exception ex)
    {
        return Results.Json(new
        {
            jsonrpc = "2.0",
            error = new
            {
                code = -32603,
                message = "Internal error",
                data = ex.Message
            }
        });
    }
});

async Task<object> HandleToolCall(JsonElement root, McpToolProvider toolProvider, string? id)
{
    try
    {
        if (!root.TryGetProperty("params", out var paramsElement))
        {
            return new
            {
                jsonrpc = "2.0",
                id = id,
                error = new { code = -32602, message = "Missing params" }
            };
        }
        
        if (!paramsElement.TryGetProperty("name", out var nameElement))
        {
            return new
            {
                jsonrpc = "2.0",
                id = id,
                error = new { code = -32602, message = "Missing tool name" }
            };
        }
        
        var toolName = nameElement.GetString();
        var arguments = paramsElement.TryGetProperty("arguments", out var argsElement) 
            ? argsElement.GetRawText() 
            : "{}";
        
        var result = toolName switch
        {
            "search_reservations" => await toolProvider.CallToolAsync(toolName, JsonSerializer.Deserialize<Dictionary<string, object>>(arguments)),
            "search_reservations_by_name" => await toolProvider.CallToolAsync(toolName, JsonSerializer.Deserialize<Dictionary<string, object>>(arguments)),
            "get_calendar" => await toolProvider.CallToolAsync(toolName, JsonSerializer.Deserialize<Dictionary<string, object>>(arguments)),
            _ => throw new ArgumentException($"Unknown tool: {toolName}")
        };
        
        return new
        {
            jsonrpc = "2.0",
            id = id,
            result = new
            {
                content = result.Content.Select(c => new
                {
                    type = c.Type,
                    text = c.Text
                }).ToArray()
            }
        };
    }
    catch (Exception ex)
    {
        return new
        {
            jsonrpc = "2.0",
            id = id,
            error = new
            {
                code = -32603,
                message = "Tool execution failed",
                data = ex.Message
            }
        };
    }
}

// 既存のエンドポイントも保持(開発・テスト用)
app.MapGet("/health", () => Results.Ok(new { status = "healthy", timestamp = DateTime.UtcNow }));

app.MapGet("/info", () => Results.Ok(new { 
    name = "KKC MCP Server API",
    version = "1.0.0",
    description = "ASP.NET Core Minimal API - MCP Server",
    endpoints = new[] { "/", "/health", "/info" }
}));

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.Run();

McpToolProvider.cs

using System.Text.Json;
using KkcMcpServerApi.Models;

namespace KkcMcpServerApi.Services;

/// <summary>
/// MCPツールプロバイダー
/// </summary>
public class McpToolProvider
{
    private readonly IWebApiClient _webApiClient;
    private readonly ILogger<McpToolProvider> _logger;

    public McpToolProvider(IWebApiClient webApiClient, ILogger<McpToolProvider> logger)
    {
        _webApiClient = webApiClient;
        _logger = logger;
    }

    /// <summary>
    /// 利用可能なツールの一覧を取得
    /// </summary>
    public ToolsListResult GetToolsList()
    {
        var tools = new List<ToolInfo>
        {
            new ToolInfo
            {
                Name = "search_reservations",
                Description = "予約を検索します。複数の条件で絞り込み検索が可能です。",
                InputSchema = new
                {
                    type = "object",
                    properties = new
                    {
                        customerName = new { type = "string", description = "顧客名での検索" },
                        customerNameKana = new { type = "string", description = "顧客名カナでの検索" },
                        phoneNumber = new { type = "string", description = "電話番号での検索" },
                        email = new { type = "string", description = "メールアドレスでの検索" },
                        receptionDate = new { type = "string", description = "受付日での検索(YYYY-MM-DD形式)" },
                        areaGroupId = new { type = "integer", description = "エリアグループIDでの検索" },
                        storeId = new { type = "integer", description = "店舗IDでの検索" },
                        reservationDate = new { type = "string", description = "予約日での検索(YYYY-MM-DD形式)" },
                        timeZone = new { type = "string", description = "時間帯での検索" },
                        status = new { type = "string", description = "予約ステータスでの検索" },
                        page = new { type = "integer", description = "ページ番号(デフォルト: 1)" },
                        perPage = new { type = "integer", description = "1ページあたりの件数(デフォルト: 10)" }
                    }
                }
            },
            new ToolInfo
            {
                Name = "search_reservations_by_name",
                Description = "販売会社名、店舗名、エリアグループ名などの名前を使用して予約を検索します。",
                InputSchema = new
                {
                    type = "object",
                    properties = new
                    {
                        sellingCompany = new { type = "string", description = "販売会社名での検索" },
                        store = new { type = "string", description = "店舗名での検索" },
                        areaGroup = new { type = "string", description = "エリアグループ名での検索" },
                        customerName = new { type = "string", description = "顧客名での検索" },
                        phoneNumber = new { type = "string", description = "電話番号での検索" },
                        email = new { type = "string", description = "メールアドレスでの検索" },
                        acceptanceDate = new { type = "string", description = "受付日での検索(YYYY-MM-DD形式)" },
                        reservationDate = new { type = "string", description = "予約日での検索(YYYY-MM-DD形式)" },
                        timeZone = new { type = "string", description = "時間帯での検索" },
                        model = new { type = "string", description = "車種名での検索" },
                        plateNumber = new { type = "string", description = "登録番号での検索" },
                        color = new { type = "string", description = "塗色名での検索" },
                        remarks = new { type = "string", description = "備考での検索" },
                        page = new { type = "integer", description = "ページ番号(デフォルト: 1)" },
                        limit = new { type = "integer", description = "1ページあたりの件数(デフォルト: 10)" }
                    }
                }
            },
            new ToolInfo
            {
                Name = "get_calendar",
                Description = "指定した日付の予約カレンダー情報と空き状況を取得します。",
                InputSchema = new
                {
                    type = "object",
                    properties = new
                    {
                        date = new { type = "string", description = "カレンダー表示する日付(YYYY-MM-DD形式)" },
                        areaGroupId = new { type = "integer", description = "エリアグループID(オプション)" },
                        storeId = new { type = "integer", description = "店舗ID(オプション)" }
                    },
                    required = new[] { "date" }
                }
            }
        };

        return new ToolsListResult { Tools = tools };
    }

    /// <summary>
    /// 指定されたツールを実行
    /// </summary>
    public async Task<ToolCallResult> CallToolAsync(string toolName, Dictionary<string, object>? arguments)
    {
        try
        {
            _logger.LogInformation("Calling tool: {ToolName} with arguments: {Arguments}", 
                toolName, JsonSerializer.Serialize(arguments));

            string result = toolName switch
            {
                "search_reservations" => await CallSearchReservationsAsync(arguments),
                "search_reservations_by_name" => await CallSearchReservationsByNameAsync(arguments),
                "get_calendar" => await CallGetCalendarAsync(arguments),
                _ => throw new ArgumentException($"Unknown tool: {toolName}")
            };

            return new ToolCallResult
            {
                Content = new List<ToolContent>
                {
                    new ToolContent { Type = "text", Text = result }
                },
                IsError = false
            };
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error calling tool: {ToolName}", toolName);
            return new ToolCallResult
            {
                Content = new List<ToolContent>
                {
                    new ToolContent { Type = "text", Text = $"Error: {ex.Message}" }
                },
                IsError = true
            };
        }
    }

    /// <summary>
    /// 予約検索ツール実行
    /// </summary>
    private async Task<string> CallSearchReservationsAsync(Dictionary<string, object>? arguments)
    {
        var request = new ReservationSearchRequest();

        if (arguments != null)
        {
            if (arguments.TryGetValue("CustomerName", out var customerName))
                request.CustomerName = customerName?.ToString();
            if (arguments.TryGetValue("CustomerNameKana", out var customerNameKana))
                request.CustomerNameKana = customerNameKana?.ToString();
            if (arguments.TryGetValue("PhoneNumber", out var phoneNumber))
                request.PhoneNumber = phoneNumber?.ToString();
            if (arguments.TryGetValue("Email", out var email))
                request.Email = email?.ToString();
            if (arguments.TryGetValue("ReceptionDate", out var receptionDate))
                request.ReceptionDate = receptionDate?.ToString();
            if (arguments.TryGetValue("AreaGroupId", out var areaGroupId) && int.TryParse(areaGroupId?.ToString(), out var areaGroupIdInt))
                request.AreaGroupId = areaGroupIdInt;
            if (arguments.TryGetValue("StoreId", out var storeId) && int.TryParse(storeId?.ToString(), out var storeIdInt))
                request.StoreId = storeIdInt;
            if (arguments.TryGetValue("Status", out var status))
                request.Status = status?.ToString();
            if (arguments.TryGetValue("page", out var page) && int.TryParse(page?.ToString(), out var pageInt))
                request.Page = pageInt;
            if (arguments.TryGetValue("perPage", out var perPage) && int.TryParse(perPage?.ToString(), out var perPageInt))
                request.PerPage = perPageInt;

            // 予約日の設定
            if (arguments.TryGetValue("ReservationDate", out var reservationDate))
            {
                var dateStr = reservationDate?.ToString();
                if (!string.IsNullOrEmpty(dateStr))
                {
                    request.ReservationDate = new ReservationDateRange
                    {
                        Start = dateStr,
                        End = dateStr
                    };
                }
            }

            // 時間帯の設定
            if (arguments.TryGetValue("TimeZone", out var timeZone))
            {
                var timeZoneStr = timeZone?.ToString();
                if (!string.IsNullOrEmpty(timeZoneStr))
                {
                    request.TimeZone = new[] { timeZoneStr };
                }
            }
        }

        return await _webApiClient.SearchReservationsAsync(request);
    }

    /// <summary>
    /// 名前による予約検索ツール実行
    /// </summary>
    private async Task<string> CallSearchReservationsByNameAsync(Dictionary<string, object>? arguments)
    {
        var request = new ReservationSearchByNameRequest();

        if (arguments != null)
        {
            if (arguments.TryGetValue("SellingCompany", out var sellingCompany))
                request.SellingCompany = sellingCompany?.ToString();
            if (arguments.TryGetValue("Store", out var store))
                request.Store = store?.ToString();
            if (arguments.TryGetValue("AreaGroup", out var areaGroup))
                request.AreaGroup = areaGroup?.ToString();
            if (arguments.TryGetValue("CustomerName", out var customerName))
                request.CustomerName = customerName?.ToString();
            if (arguments.TryGetValue("PhoneNumber", out var phoneNumber))
                request.PhoneNumber = phoneNumber?.ToString();
            if (arguments.TryGetValue("Email", out var email))
                request.Email = email?.ToString();
            if (arguments.TryGetValue("AcceptanceDate", out var acceptanceDate))
                request.AcceptanceDate = acceptanceDate?.ToString();
            if (arguments.TryGetValue("Model", out var model))
                request.Model = model?.ToString();
            if (arguments.TryGetValue("PlateNumber", out var plateNumber))
                request.PlateNumber = plateNumber?.ToString();
            if (arguments.TryGetValue("Color", out var color))
                request.Color = color?.ToString();
            if (arguments.TryGetValue("Remarks", out var remarks))
                request.Remarks = remarks?.ToString();
            if (arguments.TryGetValue("page", out var page) && int.TryParse(page?.ToString(), out var pageInt))
                request.Page = pageInt;
            if (arguments.TryGetValue("limit", out var limit) && int.TryParse(limit?.ToString(), out var limitInt))
                request.Limit = limitInt;

            // 予約日の設定
            if (arguments.TryGetValue("ReservationDate", out var reservationDate))
            {
                var dateStr = reservationDate?.ToString();
                var storeValue = arguments.TryGetValue("Store", out var storeArg) ? storeArg?.ToString() : null;
                
                if (!string.IsNullOrEmpty(dateStr))
                {
                    request.ReservationDate = new ReservationDateRange
                    {
                        Store = storeValue,
                        Start = dateStr,
                        End = dateStr
                    };
                }
            }

            // 時間帯の設定
            if (arguments.TryGetValue("TimeZone", out var timeZone))
            {
                var timeZoneStr = timeZone?.ToString();
                if (!string.IsNullOrEmpty(timeZoneStr))
                {
                    request.TimeZone = new[] { timeZoneStr };
                }
            }
        }

        return await _webApiClient.SearchReservationsByNameAsync(request);
    }

    /// <summary>
    /// カレンダー取得ツール実行
    /// </summary>
    private async Task<string> CallGetCalendarAsync(Dictionary<string, object>? arguments)
    {
        var request = new CalendarRequest();

        if (arguments != null)
        {
            if (arguments.TryGetValue("date", out var date))
                request.Date = date?.ToString() ?? string.Empty;
            if (arguments.TryGetValue("AreaGroupId", out var areaGroupId) && int.TryParse(areaGroupId?.ToString(), out var areaGroupIdInt))
                request.AreaGroupId = areaGroupIdInt;
        }

        if (string.IsNullOrEmpty(request.Date))
        {
            throw new ArgumentException("date parameter is required");
        }

        return await _webApiClient.GetCalendarAsync(request);
    }
}

WebApiClient.cs

using System.Text.Json;
using KkcMcpServerApi.Models;

namespace KkcMcpServerApi.Services;

/// <summary>
/// WebAPIクライアントのインターフェース
/// </summary>
public interface IWebApiClient
{
    Task<string> SearchReservationsAsync(ReservationSearchRequest request);
    Task<string> SearchReservationsByNameAsync(ReservationSearchByNameRequest request);
    Task<string> GetCalendarAsync(CalendarRequest request);
}

/// <summary>
/// kkc-webapi を呼び出すクライアント
/// </summary>
public class WebApiClient : IWebApiClient
{
    private readonly HttpClient _httpClient;
    private readonly ILogger<WebApiClient> _logger;
    private readonly JsonSerializerOptions _jsonOptions;

    public WebApiClient(HttpClient httpClient, ILogger<WebApiClient> logger)
    {
        _httpClient = httpClient;
        _logger = logger;
        _jsonOptions = new JsonSerializerOptions
        {
            // Laravel APIはPascalCaseを期待しているため、PropertyNamingPolicyを設定しない
            WriteIndented = true
        };
    }

    /// <summary>
    /// 予約検索API呼び出し
    /// </summary>
    public async Task<string> SearchReservationsAsync(ReservationSearchRequest request)
    {
        try
        {
            var json = JsonSerializer.Serialize(request, _jsonOptions);
            var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
            
            _logger.LogInformation("Calling reservations/search API with: {Request}", json);
            
            var response = await _httpClient.PostAsync("/api/v1/reservations/search", content);
            
            if (!response.IsSuccessStatusCode)
            {
                var errorContent = await response.Content.ReadAsStringAsync();
                _logger.LogError("API call failed with status {StatusCode}: {Error}", 
                    response.StatusCode, errorContent);
                throw new HttpRequestException($"API call failed: {response.StatusCode} - {errorContent}");
            }
            
            var result = await response.Content.ReadAsStringAsync();
            _logger.LogInformation("API response received: {Response}", result);
            
            return result;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error calling reservations search API");
            throw;
        }
    }

    /// <summary>
    /// 名前による予約検索API呼び出し
    /// </summary>
    public async Task<string> SearchReservationsByNameAsync(ReservationSearchByNameRequest request)
    {
        try
        {
            var json = JsonSerializer.Serialize(request, _jsonOptions);
            var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
            
            _logger.LogInformation("Calling reservations/search-by-name API with: {Request}", json);
            
            var response = await _httpClient.PostAsync("/api/v1/reservations/search-by-name", content);
            
            if (!response.IsSuccessStatusCode)
            {
                var errorContent = await response.Content.ReadAsStringAsync();
                _logger.LogError("API call failed with status {StatusCode}: {Error}", 
                    response.StatusCode, errorContent);
                throw new HttpRequestException($"API call failed: {response.StatusCode} - {errorContent}");
            }
            
            var result = await response.Content.ReadAsStringAsync();
            _logger.LogInformation("API response received: {Response}", result);
            
            return result;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error calling reservations search-by-name API");
            throw;
        }
    }

    /// <summary>
    /// カレンダーAPI呼び出し
    /// </summary>
    public async Task<string> GetCalendarAsync(CalendarRequest request)
    {
        try
        {
            var json = JsonSerializer.Serialize(request, _jsonOptions);
            var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
            
            _logger.LogInformation("Calling reservations/calendar API with: {Request}", json);
            
            var response = await _httpClient.PostAsync("/api/v1/reservations/calendar", content);
            
            if (!response.IsSuccessStatusCode)
            {
                var errorContent = await response.Content.ReadAsStringAsync();
                _logger.LogError("API call failed with status {StatusCode}: {Error}", 
                    response.StatusCode, errorContent);
                throw new HttpRequestException($"API call failed: {response.StatusCode} - {errorContent}");
            }
            
            var result = await response.Content.ReadAsStringAsync();
            _logger.LogInformation("API response received: {Response}", result);
            
            return result;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error calling calendar API");
            throw;
        }
    }
}
カテゴリー: 開発 パーマリンク