MCP

MCP w .NET: Jak zbudować serwer AI w ASP.NET Core

MCP w .NET: Jak zbudować serwer AI w ASP.NET Core

Model Context Protocol (MCP) to nowy sposób integracji AI z backendem, który daje pełną kontrolę nad tym, co model może zrobić w Twoim systemie.

W tym artykule pokażę Ci, jak krok po kroku zbudować serwer MCP w ASP.NET Core, zintegrować go z API oraz wykorzystać Aspire i MCP Inspector do debugowania i obserwowalności.

Bez teorii — czysta praktyka i kod, który możesz wykorzystać w produkcji.


🧱 Czym jest MCP i dlaczego ma znaczenie?

MCP (Model Context Protocol) to standard komunikacji między AI a Twoim backendem.

Zamiast:

  • wrzucać prompty „na dziko”
  • parsować tekst
  • zgadywać intencje modelu

Masz:

  • jasno zdefiniowane tools
  • kontrolowany input/output
  • przewidywalne wywołania

👉 W praktyce: AI przestaje być czarną skrzynką


🔥 MCP vs klasyczne API

PodejścieProblem
REST API + promptybrak kontroli
JSON parsingpodatne na błędy
MCPkontrola + bezpieczeństwo

👉 MCP działa jak anti-corruption layer dla AI


🏗 Tworzenie rozwiązania w ASP.NET Core + Aspire

Zaczynamy od stworzenia projektu w .NET Aspire.
W rezultacie powstało dla nas rozwiązanie Aspire obejmujące cztery projekty.

✔ Struktura rozwiązania

  • interfejs API (WeatherForecast)
  • AppHost (Aspire)
  • ServiceDefaults
  • interfejs internetowy Web

👉 Minimalny setup, ale pokazuje cały flow


✔ Konfiguracja Aspire

Nie będziemy korzystać z interfejsu internetowego, więc usunę ten projekt i zostawiamy tylko API, AppHost i ServiceDefaults.

W AppHost usuwamy kod odwołujący się do interfejsu internetowego.

// AppHost - to usuwamy
builder.AddProject<Projects.DevHobby_AspNetCoreMcp_Web>("webfrontend")
    .WithExternalHttpEndpoints()
    .WithHttpHealthCheck("/health")
    .WithReference(apiService)
    .WaitFor(apiService);

W efekcie host aplikacji ma do dyspozycji jedynie interfejs API.

var builder = DistributedApplication.CreateBuilder(args);

var apiService = builder.AddProject<Projects.DevHobby_AspNetCoreMcp_ApiService>("apiservice")
    .WithHttpHealthCheck("/health");

builder.Build().Run();

👉 Dzięki temu:

  • mamy service discovery
  • spójne health checki
  • łatwą orkiestrację

W tym interfejsie API znajduje się pojedyncza, minimalna ścieżka API o nazwie WeatherForecast, która nie przyjmuje żadnych parametrów. Nasze rozwiązanie jest więc naprawdę proste.


⚙️ Implementacja serwera MCP

Dodajmy nowy, pusty projekt ASP.NET Core Empty, w którym możemy umieścić nasz serwer MCP.

Dodajemy pakiet:

ModelContextProtocol.AspNetCore

Gdzie w klasie Program wklejmy kod z folderu Samples z repozytorium csharp-SDK.

✔ Konfiguracja Program.cs

using System.Net.Http.Headers;

var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();

builder.Services.AddMcpServer()
    .WithHttpTransport(o => o.Stateless = false)
    .WithTools<WeatherTools>();

// Configure HttpClientFactory for weather.gov API
builder.Services.AddHttpClient("WeatherApi", client =>
{
    client.BaseAddress = new Uri("https://api.weather.gov");
    client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("weather-tool", "1.0"));
});

var app = builder.Build();
app.MapDefaultEndpoints();
app.MapMcp();
app.Run();

Nie potrzebujemy żadnych danych telemetrycznych. Usuwamy. Wszystko to zawiera się w metodzie AddServiceDefaults i na razie zachowamy tylko narzędzie pogodowe.


💡 Dlaczego to podejście jest poprawne?

  • AddMcpServer() → rejestruje MCP pipeline
  • WithTools<T>() → jawna kontrola nad ekspozycją funkcji
  • HttpClientFactory → production-ready komunikacja

👉 Brak refleksji = większa kontrola i bezpieczeństwo


🧰 Tworzenie narzędzia (Tool) w MCP

Każda operacja dostępna dla AI to tool.
Dodajemy narzedzie WeatherTools które Pobieramy z repozytorium, folderu Tools


✔ Przykład: WeatherTools

using ModelContextProtocol;
using ModelContextProtocol.Server;
using System.ComponentModel;
using System.Globalization;
using System.Text.Json;

namespace DevHobby.AspNetCoreMcp.McpServer.Tools;

[McpServerToolType]
public sealed class WeatherTools
{
    private readonly IHttpClientFactory _httpClientFactory;

    public WeatherTools(IHttpClientFactory httpClientFactory)
    {
        _httpClientFactory = httpClientFactory;
    }

    [McpServerTool, Description("Get weather alerts for a US state.")]
    [McpMeta("category", "weather")]
    [McpMeta("dataSource", "weather.gov")]
    public async Task<string> GetAlerts(
        [Description("The US state to get alerts for. Use the 2 letter abbreviation for the state (e.g. NY).")] string state)
    {
        var client = _httpClientFactory.CreateClient("WeatherApi");
        using var responseStream = await client.GetStreamAsync($"/alerts/active/area/{state}");
        using var jsonDocument = await JsonDocument.ParseAsync(responseStream)
            ?? throw new McpException("No JSON returned from alerts endpoint");

        var alerts = jsonDocument.RootElement.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.")]
    [McpMeta("category", "weather")]
    [McpMeta("recommendedModel", "gpt-4")]
    public async Task<string> GetForecast(
        [Description("Latitude of the location.")] double latitude,
        [Description("Longitude of the location.")] double longitude)
    {
        var client = _httpClientFactory.CreateClient("WeatherApi");
        var pointUrl = string.Create(CultureInfo.InvariantCulture, $"/points/{latitude},{longitude}");

        using var locationResponseStream = await client.GetStreamAsync(pointUrl);
        using var locationDocument = await JsonDocument.ParseAsync(locationResponseStream);
        var forecastUrl = locationDocument?.RootElement.GetProperty("properties").GetProperty("forecast").GetString()
            ?? throw new McpException($"No forecast URL provided by {client.BaseAddress}points/{latitude},{longitude}");

        using var forecastResponseStream = await client.GetStreamAsync(forecastUrl);
        using var forecastDocument = await JsonDocument.ParseAsync(forecastResponseStream);
        var periods = forecastDocument?.RootElement.GetProperty("properties").GetProperty("periods").EnumerateArray()
            ?? throw new McpException("No JSON returned from forecast endpoint");

        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()}
                """));
    }
}

AppHost

Teraz dodamy aby serwer MCP mógł używać funkcji wykrywania usług do łączenia się z naszym interfejsem API.

builder.AddProject<Projects.DevHobby_AspNetCoreMcp_McpServer>("mcp-server")
    .WithHttpHealthCheck("/health")
    .WithReference(apiService);

McpServer

W pliku Program.cs serwera MCP dodam kod, który utworzy nazwanego HttpClient dla naszej usługi API.

builder.Services.AddHttpClient("SimpleWeather", client =>
    client.BaseAddress = new Uri("https://apiservice"));

GetSimpleWeather

Dodajemy nowe narzedzie dla naszego API

[McpServerTool, Description("Get current weather conditions")]
public async Task<string> GetSimpleWeather()
{
    var client = _httpClientFactory.CreateClient("SimpleWeather");
    var forecast = await client.GetFromJsonAsync<List<WeatherForecast>>("/weatherforecast");

    if (forecast == null)
        return "No weather data available.";

    return JsonSerializer.Serialize(forecast, _jsonOptions);
}

🧠 Co tu się naprawdę dzieje?

  • AI wywołuje tool → nie endpoint bezpośrednio
  • Ty kontrolujesz:
    • co może być wywołane
    • jak wygląda response
    • jakie są side-effecty

👉 To jest kluczowa różnica względem klasycznego API


✔ Model danych

public record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary, int TemperatureF);

Dodam zmienną JsonSerializerOptions, abyśmy nie musieli tworzyć nowej opcji przy każdym żądaniu.

private readonly JsonSerializerOptions _jsonOptions = new() { WriteIndented = true };

🔍 Debugowanie z Aspire i MCP Inspector

Dodajemy pakiet:

CommunityToolkit.Aspire.Hosting.McpInspector

✔ Integracja

W kodzie hosta aplikacji przechwycę odwołanie do niego w zmiennej.

var mcp = builder.AddProject<Projects.WebApplication1>("mcp-server")
     .WithHttpHealthCheck("/health")
     .WithReference(apiService);

Następnie możemy wywołać builder.AddMcpInspector,

builder.AddMcpInspector("mcp-inspector").WithMcpServer(mcp);

Zbudujmy i uruchommy to.


❗ Typowy problem

👉 Nie działa połączenie?

Najczęściej:

  • domyślna ścieżka /mcp
  • niepoprawny endpoint

✔ Fix:

.WithMcpServer(mcp, path: "");

Zbudujmy i uruchommy to po fix.


✔ Co daje MCP Inspector?

  • podgląd tools
  • ręczne wywołania
  • trace requestów
  • debug komunikacji

👉 To jest game-changer przy integracji AI


🧪 Testowanie MCP Tools

Tools to zwykłe klasy → możesz je normalnie testować

[Fact]
public async Task GetSimpleWeather_ShouldReturnData()
{
    var factory = Substitute.For<IHttpClientFactory>();
    var client = new HttpClient(new FakeHandler());
    factory.CreateClient("SimpleWeather").Returns(client);
    var tool = new WeatherTools(factory);

    var result = await tool.GetSimpleWeather();

    result.Should().NotBeNull();
}

✔ Dlaczego to dobre podejście?

  • brak zależności od AI
  • pełna kontrola testów
  • szybkie unit testy

👉 MCP nie komplikuje testowania — upraszcza je


🧩 Gdzie MCP ma sens w realnym projekcie?

Nie używaj MCP do wszystkiego.

👉 Najlepsze use-case’y:

  • integracje AI z domeną (orders, billing)
  • orchestration procesów
  • kontrola operacji (whitelist)
  • systemy wymagające bezpieczeństwa

❌ Kiedy to overkill?

  • proste CRUD API
  • brak integracji z AI
  • małe projekty

📌 Best Practices

✔ Jawnie rejestruj tools
✔ Używaj HttpClientFactory
✔ Nie expose’uj całej domeny
✔ Waliduj input
✔ Loguj każde wywołanie


🔗 Zobacz też


🚀 Podsumowanie

MCP zmienia sposób integracji AI z backendem:

👉 zamiast chaosu → kontrola
👉 zamiast promptów → kontrakty
👉 zamiast zgadywania → architektura

Jeśli budujesz systemy, które mają współpracować z AI — to jest kierunek, którego nie możesz ignorować.


📣 Call To Action

Jeśli ten materiał był pomocny:

👉 Zostaw komentarz – jakie use-case’y MCP widzisz u siebie?
👉 subskrybuj nasz kanał na YT!
👉 udostępnij artykuł komuś, kto zaczyna z programowaniem


Zobacz także — powiązane artykuły

👉 MCP w .NET (C#) – jak zbudować serwer AI krok po kroku

👉 Tworzenie klas i obiektów w C# — kompletny przewodnik

👉 Pattern Matching w C# – switch expressions i type patterns

Dołącz do “Od Zera do .NET Developera”

Zacznij swoją przygodę z programowaniem w oparciu o sprawdzone praktyki rynkowe. Wybierz kompletną ścieżkę rozwoju i zbuduj solidne fundamenty.

Dołącz do ścieżki teraz →
Masz pytania? Napisz: mariuszjurczenko@dev-hobby.pl

Dodaj komentarz