If you’re building AI agents that need to interact with real systems — databases, APIs, internal tools — you’ve probably run into the same wall I did. I’m talking about the mess of hardcoded JSON schemas in system prompts, brittle API wrappers, and production incidents that happen when the LLM invents a parameter name that doesn’t exist. An MCP server (Model Context Protocol server) is the structured solution to that problem, and as an AI engineer working in Sydney, it’s now a core part of how I build agent systems.

Let me walk you through what MCP servers are, why they matter, and how to build one in C# using the official SDK — with real, working code.

The Problem an MCP Server Solves

When you build an AI agent that needs to do things — query a database, call an API, read a file — you have to teach the model what tools exist, what they accept, and what they return. The old approach was: write it into the system prompt and pray.

The problem is that system prompts don’t have a schema. If your tool expects a date in ISO 8601 format and the model sends "tomorrow", your app crashes. If you rename a parameter, you need to update prompts scattered across multiple deployments. There’s no contract between the AI and your code.

MCP fixes this by defining a standard protocol — a contract — between AI models (clients) and the external capabilities they need (servers). An MCP server advertises what tools it offers, what inputs each tool expects, and what it returns. The model calls tools through MCP the same way a browser calls APIs through HTTP: with a defined interface that both sides agree on in advance.

This isn’t just theoretical tidiness. It’s what makes AI agents maintainable in production.

How an MCP Server Works

Think of MCP like a USB-C standard for AI tools. Before USB-C, every device had its own charger. After, one cable works everywhere. MCP does the same thing for AI capabilities.

Here’s the flow when an AI agent uses an MCP tool:

  1. The agent (MCP client) connects to your MCP server at startup.
  2. The server sends a capabilities manifest — a list of tools it provides, with their names, descriptions, and input schemas.
  3. The agent’s host (Claude, ChatGPT, your own orchestrator) reads this manifest and makes the tools available to the model.
  4. When the model decides to call a tool, it sends a structured request through the client.
  5. Your server executes the tool and returns a structured response.
  6. The model reads the result and continues reasoning.

The beauty of step 2 is that the model now has a machine-readable description of your tools — not a paragraph of English in a system prompt, but a typed schema. Fewer hallucinations. Cleaner architecture. Tools you can version and test independently.

The Three Things an MCP Server Can Expose

MCP servers can expose three types of capabilities:

Tools — Functions the model can call. Think of these as your API endpoints. A tool takes typed inputs and returns a result. This is what most people think of when they hear “MCP server.”

Resources — Read-only data the model can access. Like files, database records, or live feeds. The model can browse available resources and read their contents.

Prompts — Reusable prompt templates the model can invoke. Useful for standardising how certain tasks are framed across your application.

For most enterprise use cases, you’ll mostly be building Tools. Resources matter when you want the model to explore data rather than query it directly. Prompts are useful in multi-agent workflows.

Architecture Overview

Before we touch code, here’s how the pieces connect. The diagram shows the three zones: the AI client (Claude Desktop or your app), the MCP server (our C# application), and the backend systems it talks to. Use the zoom controls inside the diagram to explore it in detail.

MCP Server architecture: AI client → JSON-RPC protocol → C# server → backend systems. Pan and zoom inside the diagram, or open full-screen in draw.io ↗

Building a Real MCP Server in C#

Enough theory. Let me show you how this looks in code using the official MCP C# SDK (ModelContextProtocol), which Microsoft co-maintains with Anthropic. This is production-ready code — not a toy example.

Step 1: Install the packages

dotnet add package ModelContextProtocol
dotnet add package Microsoft.Extensions.Hosting

This gives you the full MCP server runtime with the .NET generic host, dependency injection, and logging baked in.

Step 2: Project structure

MyMcpServer/
├── Program.cs
├── Tools/
│   └── ProductTools.cs
└── MyMcpServer.csproj

Step 3: Wire up the host in Program.cs

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using MyMcpServer.Tools;

var builder = Host.CreateApplicationBuilder(args);

builder.Services.AddMcpServer()
    .WithStdioServerTransport()   // Claude Desktop and most MCP clients use stdio
    .WithTools<ProductTools>();   // Register your tool class

// Log to stderr — stdout must stay clean for the MCP protocol
builder.Logging.AddConsole(options =>
{
    options.LogToStandardErrorThreshold = LogLevel.Trace;
});

// Your tools get full DI — inject anything here
builder.Services.AddSingleton<IProductRepository, SqlProductRepository>();

await builder.Build().RunAsync();

Three things worth noting. First, AddMcpServer() wires up the entire MCP runtime — you write zero protocol handling. Second, WithStdioServerTransport() is for local Claude Desktop use; for Azure deployments you swap it for the ModelContextProtocol.AspNetCore HTTP transport. Third, your tool classes get full dependency injection — inject your database, HTTP clients, loggers, anything.

Step 4: Define your tools with attributes

using ModelContextProtocol;
using ModelContextProtocol.Server;
using System.ComponentModel;

namespace MyMcpServer.Tools;

[McpServerToolType]
public sealed class ProductTools
{
    private readonly IProductRepository _repo;

    public ProductTools(IProductRepository repo)
    {
        _repo = repo;
    }

    [McpServerTool, Description("Search for products by name or SKU. Returns matching products with current stock levels.")]
    public async Task<string> SearchProducts(
        [Description("The product name or SKU to search for.")] string query,
        [Description("Maximum number of results to return. Defaults to 10.")] int limit = 10)
    {
        var results = await _repo.SearchAsync(query, limit);

        if (!results.Any())
            return $"No products found matching '{query}'.";

        return string.Join("\n---\n", results.Select(p =>
            $"SKU: {p.Sku}\nName: {p.Name}\nStock: {p.StockLevel}\nPrice: {p.Price:C}"));
    }

    [McpServerTool, Description("Get detailed information about a specific product by its SKU.")]
    public async Task<string> GetProductDetails(
        [Description("The product SKU (e.g. PROD-12345).")] string sku)
    {
        var product = await _repo.GetBySkuAsync(sku);

        if (product is null)
            return $"No product found with SKU '{sku}'.";

        return $"""
            SKU: {product.Sku}
            Name: {product.Name}
            Description: {product.Description}
            Stock Level: {product.StockLevel} units
            Price: {product.Price:C}
            Category: {product.Category}
            Last Updated: {product.UpdatedAt:yyyy-MM-dd HH:mm} UTC
            """;
    }
}

The [McpServerTool] attribute registers the method as an MCP tool. The [Description] attributes on both the method and its parameters become the typed schema that the AI model reads — this replaces the fragile English paragraphs in your system prompt. If the description is precise, the model uses the tool correctly. If the parameter description specifies the expected format, the model sends the right format. Every time.

What the AI Model Actually Sees

When Claude or any MCP client connects to this server, it automatically receives a capabilities manifest generated from your C# code:

{
  "tools": [
    {
      "name": "SearchProducts",
      "description": "Search for products by name or SKU. Returns matching products with current stock levels.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "query": {
            "type": "string",
            "description": "The product name or SKU to search for."
          },
          "limit": {
            "type": "integer",
            "description": "Maximum number of results to return. Defaults to 10.",
            "default": 10
          }
        },
        "required": ["query"]
      }
    }
  ]
}

The SDK generates this JSON schema from your C# method signatures automatically. No manual schema writing. No JSON maintenance. Rename a parameter in C#, the schema updates. Add a new tool method, it appears in the manifest. This is the developer experience that was missing before MCP.

Running It Locally with Claude Desktop

To test your MCP server with Claude Desktop, add it to your claude_desktop_config.json:

{
  "mcpServers": {
    "product-server": {
      "command": "dotnet",
      "args": ["run", "--project", "/path/to/MyMcpServer"]
    }
  }
}

Restart Claude Desktop, open a conversation, and ask: “What products do we have in the PROD-1 range?” Claude will automatically call SearchProducts, read the result, and answer from real data — not a hallucination.

That moment when you see the AI reach into your actual database and return a real answer is quietly satisfying in a way that’s hard to describe until you’ve experienced it.

When Should You Build an MCP Server?

Build one when:

  • You want the same capabilities available to multiple AI clients (Claude, Copilot, your internal tools) without duplicating integration code.
  • You’re building an AI agent that needs to interact with internal systems — databases, ERPs, CRMs — that aren’t publicly accessible.
  • You want to give your tools a proper versioned schema rather than relying on natural language in prompts.
  • You’re building a multi-agent system and need a reliable way for agents to share capabilities.

Don’t bother when you’re prototyping with a single model and a single capability. Use function calling directly. MCP pays off when you’re thinking about the long run — multiple clients, multiple tools, real maintainability.

The Bigger Picture

MCP is becoming the USB-C of AI tooling. Claude supports it natively. GitHub Copilot is adding support. OpenAI’s agent framework is converging on it. If you’re building AI systems that need to interact with the real world, this is the protocol worth learning now.

More importantly for us as engineers: it gives us a way to reason about AI tool integrations the same way we reason about APIs. Contract-first, typed, testable, versioned. That’s the kind of discipline that makes enterprise AI systems actually work in production.

I’ve been working with MCP servers on Azure-hosted agent systems, and the difference in maintainability compared to prompt-embedded tool descriptions is night and day. If you’re building anything beyond a demo, start here.


Working on an AI integration project in Sydney and want to talk through the architecture? Connect with me on LinkedIn — I’m always up for a conversation about what’s actually working in production.

Next up: How I Built an MCP Server for Enterprise Data — a Real-World Case Study, where I go deeper on authentication, error handling, and deploying to Azure Container Apps.