Learnixo
Back to blog
Backend Systemsintermediate

MCP (Model Context Protocol) in .NET: Exposing Tools to AI Agents

Build MCP servers in .NET to expose your APIs and data to AI assistants like Claude. Covers the protocol, tools, resources, prompts, .NET SDK setup, and real-world integration patterns.

LearnixoJune 4, 20266 min read
.NETC#MCPAIClaudeAgentsLLMTools
Share:𝕏

What is MCP?

Model Context Protocol (MCP) is an open standard (developed by Anthropic) that lets AI assistants like Claude connect to external tools and data sources through a consistent interface. Instead of every AI app building custom integrations, MCP provides a universal connector.

Claude / AI Agent
      │
      │  MCP Protocol (JSON-RPC over stdio or HTTP/SSE)
      │
      ▼
MCP Server (.NET)
  ├── Tools     ← actions the AI can invoke (create order, search products)
  ├── Resources ← data the AI can read (order history, documentation)
  └── Prompts   ← reusable prompt templates

Real use cases:

  • Expose your internal API to Claude for internal tooling
  • Let Claude query your database and create reports
  • Build an AI assistant that can place orders, check stock, run queries
  • Connect Claude Desktop to your development environment

Setup

Bash
dotnet add package ModelContextProtocol
dotnet add package ModelContextProtocol.AspNetCore  # for HTTP/SSE transport

Minimal MCP Server (stdio transport — for Claude Desktop)

C#
// Program.cs
using ModelContextProtocol.Server;
using ModelContextProtocol.Protocol.Types;

var builder = Host.CreateApplicationBuilder(args);

builder.Services
    .AddMcpServer()
    .WithStdioServerTransport()
    .WithTools<OrderTools>()
    .WithTools<ProductTools>()
    .WithResources<OrderResources>();

await builder.Build().RunAsync();

Defining Tools

Tools are actions the AI can call. Decorate a class with [McpServerToolType] and methods with [McpServerTool].

C#
[McpServerToolType]
public class OrderTools
{
    private readonly IOrderRepository _orders;
    private readonly IMediator _mediator;

    public OrderTools(IOrderRepository orders, IMediator mediator)
    {
        _orders  = orders;
        _mediator = mediator;
    }

    [McpServerTool, Description("Get an order by its ID")]
    public async Task<string> GetOrder(
        [Description("The order UUID")] string orderId,
        CancellationToken ct)
    {
        if (!Guid.TryParse(orderId, out var id))
            return "Invalid order ID format.";

        var order = await _orders.GetByIdAsync(OrderId.From(id), ct);
        if (order is null) return $"Order {orderId} not found.";

        return JsonSerializer.Serialize(new
        {
            id       = order.Id.Value,
            customer = order.CustomerId.Value,
            status   = order.Status.ToString(),
            total    = order.Total.Amount,
            currency = order.Total.Currency,
            lines    = order.Lines.Count,
            created  = order.CreatedAt
        }, new JsonSerializerOptions { WriteIndented = true });
    }

    [McpServerTool, Description("List orders for a customer. Returns the most recent orders.")]
    public async Task<string> ListCustomerOrders(
        [Description("Customer ID")] string customerId,
        [Description("Maximum number of orders to return (default 10)")] int limit = 10,
        CancellationToken ct = default)
    {
        var orders = await _orders.GetByCustomerAsync(
            CustomerId.From(Guid.Parse(customerId)), ct);

        var recent = orders.Take(limit).Select(o => new
        {
            id     = o.Id.Value,
            status = o.Status.ToString(),
            total  = $"{o.Total.Amount} {o.Total.Currency}",
            date   = o.CreatedAt.ToString("yyyy-MM-dd")
        });

        return JsonSerializer.Serialize(recent, new JsonSerializerOptions { WriteIndented = true });
    }

    [McpServerTool, Description("Create a new order for a customer")]
    public async Task<string> CreateOrder(
        [Description("Customer ID")] string customerId,
        [Description("Product ID")] string productId,
        [Description("Quantity")] int quantity,
        [Description("Unit price in GBP")] decimal unitPrice,
        CancellationToken ct = default)
    {
        var command = new CreateOrderCommand(
            Guid.Parse(customerId),
            Lines: [new OrderLineDto(Guid.Parse(productId), quantity, unitPrice)]);

        var orderId = await _mediator.Send(command, ct);
        return $"Order created successfully. Order ID: {orderId}";
    }

    [McpServerTool, Description("Cancel an order. Only pending orders can be cancelled.")]
    public async Task<string> CancelOrder(
        [Description("The order ID to cancel")] string orderId,
        [Description("Reason for cancellation")] string reason,
        CancellationToken ct = default)
    {
        var order = await _orders.GetByIdAsync(OrderId.From(Guid.Parse(orderId)), ct);
        if (order is null) return $"Order {orderId} not found.";

        try
        {
            order.Cancel(reason);
            await _unitOfWork.SaveChangesAsync(ct);
            return $"Order {orderId} cancelled.";
        }
        catch (DomainException ex)
        {
            return $"Cannot cancel: {ex.Message}";
        }
    }
}

Defining Resources

Resources are data the AI can read — like files or database records.

C#
[McpServerResourceType]
public class OrderResources
{
    private readonly IOrderRepository _orders;

    public OrderResources(IOrderRepository orders) => _orders = orders;

    [McpServerResource(
        UriTemplate = "orders://recent",
        Name        = "Recent Orders",
        Description = "The 20 most recent orders across all customers",
        MimeType    = "application/json")]
    public async Task<string> GetRecentOrdersAsync(CancellationToken ct)
    {
        var orders = await _orders.GetRecentAsync(20, ct);
        return JsonSerializer.Serialize(orders.Select(o => new
        {
            id       = o.Id.Value,
            customer = o.CustomerId.Value,
            total    = o.Total.Amount,
            status   = o.Status.ToString()
        }), new JsonSerializerOptions { WriteIndented = true });
    }

    [McpServerResource(
        UriTemplate = "orders://{orderId}",
        Name        = "Order Details",
        Description = "Full details of a specific order",
        MimeType    = "application/json")]
    public async Task<string> GetOrderResourceAsync(string orderId, CancellationToken ct)
    {
        var order = await _orders.GetByIdAsync(OrderId.From(Guid.Parse(orderId)), ct);
        return order is null
            ? $"{{\"error\": \"Order {orderId} not found\"}}"
            : JsonSerializer.Serialize(order, new JsonSerializerOptions { WriteIndented = true });
    }
}

HTTP/SSE Transport (for web integration)

For remote MCP servers accessed over HTTP:

C#
// Program.cs — ASP.NET Core with SSE transport
builder.Services
    .AddMcpServer()
    .WithHttpSseServerTransport()
    .WithTools<OrderTools>();

app.MapMcp("/mcp");  // exposes /mcp/sse and /mcp/messages endpoints

Claude Desktop Integration

Add to Claude Desktop config (claude_desktop_config.json):

JSON
{
  "mcpServers": {
    "orderflow": {
      "command": "dotnet",
      "args": ["run", "--project", "C:/Projects/OrderFlow.McpServer"],
      "env": {
        "ConnectionStrings__Default": "Server=localhost;Database=OrderFlow;..."
      }
    }
  }
}

Now Claude can say "show me the pending orders" and invoke your ListCustomerOrders tool.


Security Considerations

C#
// Validate input — treat AI-generated parameters as untrusted
[McpServerTool, Description("Search products by name")]
public async Task<string> SearchProducts(
    [Description("Search term")] string searchTerm,
    CancellationToken ct)
{
    // Sanitise — don't pass raw AI input to SQL
    if (string.IsNullOrWhiteSpace(searchTerm) || searchTerm.Length > 100)
        return "Invalid search term.";

    // Use parameterised query — never string concatenation
    var products = await _db.Products
        .Where(p => p.Name.Contains(searchTerm))  // EF Core parameterises this
        .Take(20)
        .ToListAsync(ct);

    return JsonSerializer.Serialize(products);
}

// Limit destructive operations
[McpServerTool, Description("Delete an order — ADMIN ONLY")]
public async Task<string> DeleteOrder(string orderId, CancellationToken ct)
{
    // Check if MCP client is authorised for destructive operations
    if (!_authContext.HasPermission("orders:delete"))
        return "Insufficient permissions.";

    // ... proceed
}

Interview Questions

Q: What is MCP and why does it matter for .NET developers? Model Context Protocol is a standard for connecting AI models to external tools and data. For .NET developers it means you can expose your APIs, databases, and business logic to Claude or other AI assistants using a standard SDK, rather than building custom integrations for each AI product.

Q: What is the difference between an MCP Tool and an MCP Resource? Tools are actions the AI can invoke (create order, send email, search). Resources are data the AI can read (recent orders, documentation, configuration). Tools modify state; resources are read-only. This mirrors REST's POST vs GET semantics.

Q: What security risks exist when exposing tools to an AI? Prompt injection — a malicious string in user input could instruct the AI to call a destructive tool. Parameter manipulation — AI-generated parameters must be validated like any untrusted input. Privilege escalation — the AI should have minimum necessary permissions. Rate limiting — prevent the AI from calling expensive tools in a loop.

Enjoyed this article?

Explore the Backend Systems learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.