MCP explained: build a Model Context Protocol server in C#

Short answer: the Model Context Protocol (MCP) is an open standard, introduced by Anthropic in November 2024, for connecting AI applications to tools and data. You write an MCP server that exposes a few capabilities (functions to call, data to read, prompt templates), and any MCP client, such as Cursor, OpenCode or an IDE assistant, can discover and use them. It does for AI integrations what a common API standard does for services: write the integration once, use it from every compatible app. In .NET, the official ModelContextProtocol package turns a static class into an MCP server in about thirty lines.

A real example: "what's the status of order AB12CD34EF?"

You run a shop and live in your editor. Several times a day you want to know an order's status, or how many orders are pending. Without MCP, you open the admin panel, or you paste data into a chat by hand.

With an MCP server that exposes two read-only tools, get_order_status and count_orders, you ask your AI assistant in plain language and it calls the tool itself. The same server works in Cursor, in OpenCode, and in any other MCP client, without changing a line.

The building blocks

An MCP server can offer three kinds of things:

  • Tools: functions the model can call, with a name, a description and a JSON Schema for the arguments. This is what most servers start with.
  • Resources: data the client can read, identified by a URI (a file, a record, a report).
  • Prompts: reusable prompt templates the user can pick in the client.

Messages are JSON-RPC 2.0. A server talks to its client over a transport: stdio (the client starts your server as a process and talks through stdin/stdout, ideal for local tools) or Streamable HTTP (the server runs as a web service, for shared or remote use).

The flow is always the same: the client connects, asks the server what it offers (tools/list), shows those tools to the model, and when the model decides to use one, the client sends tools/call and passes the result back to the model.

Runnable code: an MCP server in C#

Create a console app (dotnet new console), add dotnet add package ModelContextProtocol and dotnet add package Microsoft.Extensions.Hosting, and replace Program.cs:

using System.ComponentModel;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Server;

var builder = Host.CreateApplicationBuilder(args);

// stdout carries the MCP protocol itself, so every log line must go to stderr.
builder.Logging.AddConsole(options => options.LogToStandardErrorThreshold = LogLevel.Trace);

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

await builder.Build().RunAsync();

[McpServerToolType]
public static class OrderTools
{
    // A stand-in for your database or internal API.
    private static readonly Dictionary<string, (string Status, string Product)> Orders = new()
    {
        ["AB12CD34EF"] = ("Approved", "DiskWatch"),
        ["GH56JK78MN"] = ("Pending", "Peyk"),
    };

    [McpServerTool, Description("Returns the status and product of a shop order. Read-only.")]
    public static string GetOrderStatus([Description("The 10-character order code, e.g. AB12CD34EF.")] string code) =>
        Orders.TryGetValue(code.Trim().ToUpperInvariant(), out var order)
            ? $"Order {code}: {order.Status} ({order.Product})"
            : $"No order with code {code}.";

    [McpServerTool, Description("Counts orders by status. Read-only.")]
    public static string CountOrders() =>
        string.Join(", ", Orders.Values.GroupBy(o => o.Status).Select(g => $"{g.Key}: {g.Count()}"));
}

The parts that matter:

  • [McpServerToolType] and [McpServerTool] mark the class and the methods to expose. WithToolsFromAssembly() finds them, and the SDK publishes them with snake_case names (get_order_status, count_orders).
  • [Description] is the documentation the model reads. It decides when to call a tool from its description, so say what it does, what it returns and whether it changes anything.
  • Parameter descriptions become the JSON Schema the client sends to the model. Typed parameters (string, int, enums) give you validation for free.
  • Logs go to stderr. With the stdio transport, anything printed to stdout that isn't protocol breaks the connection. This is the most common first bug.

Connecting it to a client

For OpenCode, register the server with the command that starts it:

opencode mcp add orders -- dotnet run --project /path/to/OrderServer

Then ask "what's the status of order AB12CD34EF?" and approve the tool call. Cursor and other clients take the same command and arguments in their MCP settings. You can also test without any AI app: send the JSON-RPC initialize, tools/list and tools/call messages to the process yourself and read the replies, which is a good way to see how simple the protocol is.

Security: treat tools like a public API

An MCP tool is an API that a model decides to call, using text that may come from users, emails or web pages. Design it accordingly:

  • Start read-only. Add write tools only when there is a clear need, and keep them narrow (mark_order_shipped, not run_sql).
  • Least privilege. Give the server its own database user or API key with the minimum rights.
  • Validate every argument as if it came from the internet, because indirectly it can.
  • Keep a human in the loop for anything irreversible; MCP clients ask for approval before calling tools, so don't encourage users to turn that off for write tools.
  • Use only servers you trust. A third-party MCP server runs with your permissions.

Pros and cons of MCP

Pros

  • One server works across many AI apps; no per-app plugins.
  • Clear separation: your server owns data access and permissions, the client owns the model and the user experience.
  • The official C# SDK makes a server a small, ordinary .NET app you can test and deploy like any other.

Cons

  • Another moving part to secure, deploy and monitor, especially for remote HTTP servers.
  • Tool descriptions are effectively prompts; vague ones lead to wrong or missed calls.
  • Many tools at once cost context and confuse the model; expose a few well-named ones.

Where it fits

MCP is the standard way to give a model tools. The loop that decides when to call them is covered in agents and tool calling in .NET, and the full path in the AI engineering roadmap.


Want your internal systems reachable from the AI agent or your team's AI tools, safely? See how I can help.

Frequently asked questions

What is the difference between MCP and tool calling?

Tool calling is how a model asks your code to run a function inside one application. MCP is a standard protocol that lets any compatible AI app discover and call tools on a separate server, so you write the integration once instead of once per app.

Is an MCP server safe to connect to my database?

It is as safe as the tools you expose. Start read-only, give the server its own database user with minimal rights, validate every argument, and require human approval in the client for anything that changes data.

Which apps can use my MCP server?

Any MCP client, for example Cursor, OpenCode and many IDE assistants. A local server runs over stdio; to share one with a team or the web, host it over HTTP.