← Back to Writing
Article· 3 min read

Building Your First MCP Server in C# and .NET

MCP ServerModel Context Protocol.NET AIAI Developer ToolsEnterprise Systems
Architecture diagram showing Cursor, MCP client, MCP server, business service, and database layers

Summary

Tutorial: expose product catalog search to AI hosts with the ModelContextProtocol NuGet package — compare manual HTTP integration vs MCP tools using enterprise domain models and Cursor setup.

This tutorial adapts the Hands-On MCP for C# and .NET progression to a domain most enterprise teams already own: product catalog search. If you already understand why MCP matters conceptually, see From AI Model Consumer to AI Application Builder first.

What you will build: the same business capability twice — once with a hand-rolled HTTP client, once with an MCP tool — and see why the second path scales better for AI-assisted development.

Domain models

Start with small, explicit types. These mirror how you'd model a catalog service behind a commerce or operations platform.

public record Product(
    string Sku,
    string Name,
    string Category,
    decimal UnitPrice,
    int AvailableQuantity,
    string WarehouseId);

public record ProductSearchResult(
    IReadOnlyList<Product> Products,
    int TotalResults);

public interface IProductCatalogService
{
    Task<ProductSearchResult> SearchAsync(
        string query,
        string? category,
        CancellationToken ct = default);
}

Approach 1: Without MCP (manual HTTP integration)

Before MCP, every consumer — Blazor UI, background job, LLM host — reimplements the same glue code.

public class CatalogHttpClient(HttpClient http)
{
    private const string ApiKeyHeader = "X-Api-Key";

    public async Task<IReadOnlyList<Product>> SearchAsync(
        string query,
        string? category,
        CancellationToken ct = default)
    {
        http.DefaultRequestHeaders.TryAddWithoutValidation(
            ApiKeyHeader,
            Environment.GetEnvironmentVariable("CATALOG_API_KEY") ?? string.Empty);

        var url = $"/v1/products/search?q={Uri.EscapeDataString(query)}";
        if (!string.IsNullOrEmpty(category))
            url += $"&category={Uri.EscapeDataString(category)}";

        var response = await http.GetFromJsonAsync<VendorApiResponse>(url, ct)
            ?? throw new InvalidOperationException("Catalog API returned no response.");

        return response.Items
            .Select(i => new Product(
                i.Sku, i.Name, i.Category,
                i.Price, i.Qty, i.Warehouse))
            .ToList();
    }
}

file record VendorApiResponse(List<VendorProduct> Items);
file record VendorProduct(
    string Sku, string Name, string Category,
    decimal Price, int Qty, string Warehouse);

Problems for AI workflows

  • The LLM host must know URL shape, headers, and response mapping
  • Each new tool consumer duplicates integration code
  • No automatic JSON Schema for the model to reason about parameters
  • Errors are inconsistent across services
AspectManual HTTP client
BoilerplateHigh (URL, auth, mapping)
Schema for LLMManual / absent
New consumer costDuplicate adapter
Type safetyClient-side only

Approach 2: With MCP (declarative tool)

With the official ModelContextProtocol NuGet package, you expose the same service as a self-describing tool.

using ModelContextProtocol.Server;
using System.ComponentModel;

[McpServerToolType]
public sealed class SearchProductsTool(IProductCatalogService catalog)
{
    [McpServerTool, Description(
        "Search the product catalog by SKU or product name, optionally filtered by category.")]
    public async Task<ProductSearchResult> SearchProductsAsync(
        [Description("SKU or product name, e.g. WH-1000XM5 or wireless headphones.")]
        string query,

        [Description("Optional category filter, e.g. Electronics or Home.")]
        string? category,

        CancellationToken ct = default)
    {
        return await catalog.SearchAsync(query, category, ct);
    }
}

The SDK generates JSON Schema from your C# types and `[Description]` attributes at startup. Any MCP-compliant host (Cursor, Claude Desktop, custom agents) discovers the tool without a bespoke adapter.

AspectMCP tool
BoilerplateAttributes + business logic
Schema for LLMAutomatic
New consumer costZero — standard protocol
Type safetyFull stack

Side-by-side architecture

Same domain service. Different integration surface.

Register the server (minimal host)

For local development, register stdio transport so Cursor can launch the server as a subprocess:

var builder = Host.CreateApplicationBuilder(args);

builder.Services.AddSingleton<IProductCatalogService, MockProductCatalogService>();

builder.Services
    .AddMcpServer()
    .WithStdioServerTransport()
    .WithTools<SearchProductsTool>();

builder.Logging.AddConsole(o =>
    o.LogToStandardErrorThreshold = LogLevel.Trace);

await builder.Build().RunAsync();

Log to stderr so stdout stays clean for the MCP protocol stream.

Cursor configuration

Add the server to `.cursor/mcp.json`:

{
  "mcpServers": {
    "enterprise-catalog": {
      "command": "dotnet",
      "args": ["run", "--project", "src/CatalogMcpServer/CatalogMcpServer.csproj"],
      "env": {
        "CATALOG_API_KEY": "${env:CATALOG_API_KEY}"
      }
    }
  }
}

Restart MCP in Cursor settings, then try: *"Find wireless headphones under $200 with more than 10 units in stock."*

When to use which pattern

Stay on HTTP when you only have traditional REST consumers and no AI hosts.

Adopt MCP when:

  • IDE agents need live catalog, inventory, or customer context
  • You want one tool definition for multiple AI clients
  • Platform teams publish shared capabilities like internal NuGet packages

What I Learned Building MCP Servers

While building MCP servers for platform engineering workflows, I found that most teams start by exposing too many capabilities. The most successful implementations begin with narrowly scoped tools, strong descriptions, and approval gates around any action that modifies state.

For catalog search specifically: a read-only `search_products` tool is an ideal first MCP surface. It gives agents real context without write risk. Teams that skip straight to "generic REST proxy" tools usually regret it — the model gets too much power, schemas become vague, and audit trails fall apart.

Start with one read tool. Ship it to Cursor. Watch how engineers actually phrase requests. Then add write tools with guardrails.

Next in this series

References

Related reading