Building an MCP Server in C#

Building an MCP Server in C#
09/2026
Kauthar A. | Solution Engineer

TL;DR

The Model Context Protocol (MCP) reduces the integration effort between AI applications and internal systems from M×N to M+N. This article walks through, step by step, how to build an MCP server in C#: from the first tool through testing with the MCP Inspector to running it securely. Security is the crucial part: least privilege, no tools for free-form SQL, and approvals for write operations. A working server is quick to build; a production-ready one needs a well-thought-out permission model.

Anyone who wants to connect an internal system to an AI assistant knows the problem: you write one integration for Claude Desktop, a second for your own application, a third for the next tool. With M systems and N applications, you end up with M×N integrations to maintain.

The Model Context Protocol (MCP) solves exactly that. It standardizes the interface between AI applications and external systems, turning M×N into M+N: every system gets a server, every application gets a client, and both sides speak the same protocol.

‍

Who does what

Three terms worth keeping straight:

  • Host: the application the model runs in (Claude Desktop, Claude Code, your own app)
  • Client: the protocol connection the host sets up per server
  • Server: your code that offers capabilities

A server exposes three kinds of primitives: tools (actions the model can execute), resources (data it can read), and prompts (prepared templates). Tools are enough to get started — they cover most use cases.

‍

Setup

The foundation is the official MCP SDK for .NET, which you can add to a C# project via NuGet.

dotnet new console -n Devware.Mcp.Inventory
cd Devware.Mcp.Inventory
dotnet add package ModelContextProtocol --prerelease
dotnet add package Microsoft.Extensions.Hosting

The Program.cs stays pleasantly short:

var builder = Host.CreateApplicationBuilder(args);

// Important: stdout belongs to the protocol. Logging must go to stderr.
builder.Logging.AddConsole(options =>
    options.LogToStandardErrorThreshold = LogLevel.Trace);

builder.Services
    .AddMcpServer()
    .WithStdioServerTransport()
    .WithToolsFromAssembly();

await builder.Build().RunAsync();

That comment isn't a minor detail — it's the mistake everyone makes exactly once. With the stdio transport, protocol messages travel over standard output. A single Console.WriteLine corrupts the message stream, and the host just reports that the connection dropped. Logging belongs on stderr.

‍

Your first tool

Tools are methods with attributes. The SDK generates the schema from the method signature:

[McpServerToolType]
public sealed class InventoryTools
{
    [McpServerTool(Name = "get_stock_level")]
    [Description("Liefert den aktuellen Lagerbestand zu einer Artikelnummer.")]
    public static async Task<string> GetStockLevel(
        InventoryDbContext db,
        [Description("Artikelnummer im Format 'A-10432'")] string sku,
        CancellationToken ct)
    {
        var item = await db.Items
            .AsNoTracking()
            .FirstOrDefaultAsync(i => i.Sku == sku, ct);

        return item is null
            ? $"Kein Artikel mit der Nummer {sku} gefunden."
            : $"{item.Name}: {item.Quantity} Stück, Lagerort {item.Location}.";
    }
}

Two things here matter more than they look.

The Description attributes are your actual API documentation — for the model. They end up in the prompt. A vague description leads to the tool being called at the wrong time or with nonsensical parameters. Write them like documentation for a new colleague: what does the tool do, when do you use it, what format do the parameters have?

Dependency injection works in tool parameters. The InventoryDbContext is resolved by the container, sku comes from the model. So you don't need to build service-locator constructs — you just work with the DI container you already have.

The return value is also worth noting: not a JSON dump of the entity, but a readable sentence. The model processes text — the clearer the answer, the less it has to interpret. And the not-found case returns a usable message instead of null.

‍

Testing and debugging

For trying things out, there's the MCP Inspector, a web tool that attaches to your server as a client:

npx @modelcontextprotocol/inspector dotnet run

You see the registered tools, can call them manually, and watch the message traffic. That's much faster than going through an actual host. And if a tool doesn't show up, the problem is usually in the assembly registration or a missing attribute.

‍

Security comes before deployment

The point where MCP projects typically go wrong: the server runs with your permissions, but the model decides when which tool gets called. And the input it reacts to can come from a document someone else wrote.

That leads to three rules:

Least privilege at the database level. A dedicated DB user with read access to exactly the tables the server needs — not the application user.

No tool that accepts free-form SQL. An ExecuteSqlRaw with a model-generated string is a SQL injection with extra steps. Model narrow, specific tools instead.

Write operations need approval. The model can read on its own. Anything that changes data, sends something, or moves money belongs behind an explicit confirmation, whether in the host or in your own application.

‍

Transport and operations

The stdio transport is the right choice for servers running locally alongside the host: no network, no authentication, process isolation provided by the operating system.

As soon as multiple users need to share the same server, you need the HTTP transport, and with it authentication, authorization, and everything a normal web service needs. The good news: from here it's ASP.NET Core again, with your existing policies. The bad news: an MCP server without auth on the network is an open interface to your data.

‍

Conclusion

Building a working server takes about half a day. The real effort for a server you can run in production lies in the permission model — something to plan up front, not bolt on afterward.

‍

Kauthar A. | Solution Engineer
Kauthar A. | Solution Engineer

AI Integration

We clarify classification, labeling, and data flows at the start of the project, not at sign-off. Built on a .NET foundation, since 2004, ISO 27001 and ISO 9001 certified.
Pfeil-Icon – Mehr erfahren

Mehr zum Thema

Arrow pointing right (link)
Why AI Projects Fail: It Is Not About the Technology
09/2026

Why AI Projects Fail: It Is Not About the Technology

Blue arrow pointing right (link)
AI in Software Development: Which Tools We Use and How Your Data Is Protected
09/2026

AI in Software Development: Which Tools We Use and How Your Data Is Protected

Blue arrow pointing right (link)
Feeding Customer Data into AI Systems: When It's Allowed and How to Build It
09/2026

Feeding Customer Data into AI Systems: When It's Allowed and How to Build It

Blue arrow pointing right (link)
Compliance Evidence from Your Software Service Provider: What to Ask For
09/2026

Compliance Evidence from Your Software Service Provider: What to Ask For

Blue arrow pointing right (link)