跳到正文
DEV Community· Rahmat Afridi·· 3 小時前AI 評分57

如何透過安全 MCP Server 向 AI 智能體開放既有 ASP.NET Core API

Expose Your Existing ASP.NET Core APIs to AI Agents with a Secure MCP Server

AI 導讀

文章示範如何在 ASP.NET Core 以官方 ModelContextProtocol C# SDK(ModelContextProtocol.AspNetCore 2.2.0)建立 MCP Server,將既有服務封裝成供 AI 智能體呼叫的工具。示例先提供只讀訂單查詢,並以 JWT、端點和工具層級授權限制存取;亦示範用 MCP client 測試工具清單及呼叫結果。

正文

Most .NET teams already have solid ASP.NET Core APIs that power their business logic, databases, and workflows. The new challenge is letting AI agents call those same capabilities without giving them unrestricted access or forcing a full rewrite. The Model Context Protocol (MCP) solves this by providing a standard way for agents to discover and invoke tools. With the official ModelContextProtocol C# SDK, you can host an MCP server directly in ASP.NET Core, wrap your existing services as tools, and control exactly what agents can see and do.

This article shows a practical pattern: take a typical order-management API, expose a small set of read-only tools over MCP, lock it down with authentication and authorization, and verify the surface with an MCP client. The approach works for any existing service layer you already trust.

Why a Separate MCP Surface Beats Auto-Exposing Every Endpoint

Agents are not ordinary API clients. They decide which tools to call based on descriptions and schemas, then chain those calls. Exposing every controller action as a tool quickly produces a noisy, dangerous surface. A better design is a thin MCP server that sits beside your application (or inside the same host) and offers only the operations that are safe and useful for agents.

Start with read-only lookups that answer familiar questions: “What is the status of order 12345?” or “List open orders for customer C-789.” Write clear, specific descriptions so the model chooses the right tool. Later you can add carefully guarded write tools that require explicit human approval or elevated roles. The official SDK (ModelContextProtocol.AspNetCore 2.2.0 at the time of writing) makes this straightforward with attribute-based tools and first-class ASP.NET Core integration.

Setting Up the MCP Server

Create or open an ASP.NET Core web project and add the package:

dotnet add package ModelContextProtocol.AspNetCore --version 2.2.0

In Program.cs register the server. The SDK’s HTTP transport is stateless by default in v2, which is ideal for load-balanced deployments:

using ModelContextProtocol.Server;

var builder = WebApplication.CreateBuilder(args);

// Your existing services
builder.Services.AddScoped<IOrderService, OrderService>();

builder.Services.AddMcpServer()
    .WithHttpTransport()
    .AddAuthorizationFilters()
    .WithToolsFromAssembly();

var app = builder.Build();

app.MapMcp("/mcp");

app.Run();

WithToolsFromAssembly discovers every class marked [McpServerToolType] and every method marked [McpServerTool]. You can also register individual types with .WithTools<OrderTools>() if you prefer explicit control.

Wrapping Existing Services as Tools

Keep business logic in the services you already have. The tool methods simply call them and return results the agent can use. Inject the service (or any other DI-registered dependency) directly:

using Microsoft.AspNetCore.Authorization;
using ModelContextProtocol.Server;
using System.ComponentModel;
using System.Security.Claims;

[McpServerToolType]
public class OrderTools
{
    private readonly IOrderService _orders;

    public OrderTools(IOrderService orders) => _orders = orders;

    [McpServerTool, Description(
        "Gets the current status and key details of a single order by its ID. " +
        "Use this when the user asks about a specific order number.")]
    public async Task<string> GetOrderStatus(
        [Description("The unique order identifier")] string orderId)
    {
        var order = await _orders.GetByIdAsync(orderId);
        if (order is null)
            return $"Order {orderId} was not found.";

        return $"Order {order.Id}: Status={order.Status}, Total={order.Total:C}, Customer={order.CustomerId}";
    }

    [McpServerTool, Description(
        "Lists open orders for a given customer. Returns at most 20 recent open orders.")]
    public async Task<string> ListOpenOrders(
        [Description("The customer identifier")] string customerId)
    {
        var orders = await _orders.GetOpenOrdersAsync(customerId, limit: 20);
        if (orders.Count == 0)
            return $"No open orders found for customer {customerId}.";

        return string.Join("\n", orders.Select(o =>
            $"{o.Id}: {o.Status} - {o.Total:C}"));
    }
}

The [Description] attributes become the tool descriptions the agent sees. Write them the way you would write a good API comment—clear purpose, expected inputs, and any limits. The SDK generates the JSON schema from the method signature automatically.

Adding Authentication and Authorization

Treat the MCP endpoint as a protected API. Configure JWT bearer authentication the same way you already do for your other APIs, then require authorization on the MCP mapping:

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Authority = builder.Configuration["Auth:Authority"];
        options.Audience = builder.Configuration["Auth:Audience"];
        options.TokenValidationParameters.ValidateIssuer = true;
        options.TokenValidationParameters.ValidateAudience = true;
    });

builder.Services.AddAuthorization();

// ... AddMcpServer as before ...

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

app.MapMcp("/mcp").RequireAuthorization();

For finer control, apply [Authorize] attributes to individual tools. Because we called AddAuthorizationFilters(), the SDK honors them: unauthorized tools disappear from tools/list results, and direct calls return a forbidden error.

[McpServerTool, Description("Cancels an order. Admin role required.")]
[Authorize(Roles = "Admin")]
public async Task<string> CancelOrder(
    ClaimsPrincipal user,
    [Description("The order to cancel")] string orderId,
    [Description("Reason for cancellation")] string reason)
{
    // Audit who called it
    var caller = user.Identity?.Name ?? "unknown";
    await _orders.CancelAsync(orderId, reason, caller);
    return $"Order {orderId} cancelled by {caller}.";
}

The SDK injects ClaimsPrincipal automatically when the parameter is present; it never appears in the tool schema the agent sees. This gives you both declarative role checks and the ability to log or further filter inside the method.

You can also set a policy on the endpoint itself:

app.MapMcp("/mcp").RequireAuthorization("McpToolsPolicy");

where the policy requires an authenticated user plus a specific scope claim:

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("McpToolsPolicy", policy => policy
        .RequireAuthenticatedUser()
        .RequireClaim("scope", "mcp.tools"));
});

Limiting What Agents Can Call

Several complementary techniques keep the surface small:

  • Only mark the methods you want with [McpServerTool]. Everything else stays private to your normal API.
  • Prefer read-only tools first. Write operations should carry [Authorize] and, for high-risk actions, an extra confirmation step or human-in-the-loop flow.
  • Use precise descriptions and parameter constraints so the model is less likely to invent calls.
  • Consider separate MCP endpoints or tool groups for different agent roles if your organization needs stronger isolation.
  • Log every tool invocation (the SDK’s request filters or ordinary ASP.NET Core logging make this easy) so you can audit what agents actually did.

Because the MCP server calls your existing service layer, all of the validation, business rules, and database constraints you already trust remain in force.

Testing with an MCP Client

You can verify the server without an LLM by using the SDK’s client. In a separate console project (or a test):

using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol;

var transport = new HttpClientTransport(new HttpClientTransportOptions
{
    Endpoint = new Uri("http://localhost:5000/mcp")
    // Add authentication headers here if required
});

await using var client = await McpClient.CreateAsync(transport);

var tools = await client.ListToolsAsync();
foreach (var tool in tools)
{
    Console.WriteLine($"{tool.Name}: {tool.Description}");
}

var result = await client.CallToolAsync(
    "GetOrderStatus",
    new Dictionary<string, object?> { ["orderId"] = "ORD-1001" });

var text = result.Content.OfType<TextContentBlock>().FirstOrDefault()?.Text;
Console.WriteLine(text);

If you configured authentication, supply a valid bearer token on the HttpClient used by the transport. The tools returned by ListToolsAsync are AIFunction instances, so you can pass them to an IChatClient (Microsoft.Extensions.AI) through ChatOptions.Tools and let an agent invoke them automatically.

Production Notes

  • Keep the MCP server stateless unless you specifically need server-to-client notifications.
  • Place it behind the same reverse proxy, rate limits, and WAF you use for the rest of your APIs.
  • Start with an internal-only deployment so you can observe real agent behavior before opening it more widely.
  • Monitor tool call volume and latency; agents can be chatty.
  • Version your tool contracts carefully—agents cache schemas and descriptions.

The combination of a thin, explicitly curated tool surface, ordinary ASP.NET Core authentication, and per-tool authorization gives you a controlled way to let AI agents work with systems you already run. You do not need to rebuild your APIs; you only need to decide which capabilities are safe to describe and expose.


About the author

Rahmat Afridi is an AI Integration Developer & Senior .NET Developer based in Islamabad, Pakistan, with experience since 2016 building practical systems in C#, ASP.NET Core, Azure, and modern AI integration patterns.

https://rahmatafridi.com

https://github.com/rahmatafridi

來源:DEV Community · dev.to