Build an MCP Server in .NET: Step‑by‑Step Guide

Build an MCP Server in .NET: Step‑by‑Step Guide

Learn to build a production‑ready MCP server in .NET: tools, prompts, STDIO vs SSE, VS Code integration, auth, and packaging tips.

.NET Development Artificial Intelligence·By amarozka · September 9, 2025

Build an MCP Server in .NET: Step‑by‑Step Guide

Are you sure your LLM “tools” aren’t secretly one‑off hacks? In this post I’ll show you how to wire them up properly with MCP – and build a working .NET server in minutes.

Why MCP (and why now)?

If you’ve ever glued an LLM to a database or a SaaS API, you probably hand‑rolled a mini‑protocol: ad‑hoc JSON in, string out, and a prayer. That doesn’t scale. Model Context Protocol (MCP) standardizes how AI apps discover and call your capabilities (“tools”), read contextual resources, and consume reusable prompts – over well‑defined transports (stdio or HTTP/SSE). The result: one integration, many clients (Copilot Chat in VS Code, Claude Desktop, and others).

Prerequisites

  • .NET 8+ (works great). For the official MCP templates and some newest goodies, use .NET 10 preview.
  • VS Code with GitHub Copilot Chat (Agent Mode) or any MCP‑capable host.
  • Basic C# and console/ASP.NET Core experience.

Quick MCP mental model

  • Server: your code exposing tools/resources/prompts.
  • Client: a process that connects to your server.
  • Host: the app that “owns” the client (e.g., VS Code Copilot). One host ↔ many clients ↔ many servers.
  • Transports:
    • STDIO for local dev – fast and simple.
    • Streamable HTTP (SSE) for remote servers, auth headers, multiple clients.

Path A – Minimal STDIO server (pure console)

We’ll start with a tiny server that exposes two tools: echo and reverse_echo.

1) Create the project & add packages

mkdir MyMcpServer && cd MyMcpServer
dotnet new console -n MyMcpServer
cd MyMcpServer
# MCP SDK + hosting
dotnet add package ModelContextProtocol --prerelease
dotnet add package Microsoft.Extensions.Hosting

2) Program.cs – boot the MCP server

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

var builder = Host.CreateApplicationBuilder(args);

// Route logs to stderr so MCP hosts don't parse them as JSON-RPC
builder.Logging.AddConsole(o => o.LogToStandardErrorThreshold = LogLevel.Trace);

builder.Services
    .AddMcpServer()
    .WithStdioServerTransport()  // run as a local process via stdio
    .WithToolsFromAssembly()     // discover [McpServerTool] in this assembly
    .WithPromptsFromAssembly();  // discover [McpServerPrompt]

await builder.Build().RunAsync();

// ===== Tools =====
[McpServerToolType]
public static class EchoTools
{
    [McpServerTool, Description("Echoes the message back to the client.")]
    public static string Echo([Description("Message to echo")] string message)
        => $"Hello from .NET: {message}";

    [McpServerTool(Name = "reverse_echo"), Description("Returns the reversed message.")]
    public static string ReverseEcho([Description("Message to reverse")] string message)
        => new string(message.Reverse().ToArray());
}

// ===== Prompts =====
[McpServerPromptType]
public static class DemoPrompts
{
    [McpServerPrompt, Description("Create a succinct summary prompt for given text.")]
    public static ChatMessage Summarize([Description("Text to summarize")] string content)
        => new(ChatRole.User, $"Please summarize in one sentence: {content}");
}

What this does:

  • Spins up an MCP server over STDIO.
  • Discovers any public static methods marked with [McpServerTool] (inside a [McpServerToolType] class) and exposes them as tools.
  • Discovers [McpServerPrompt] for reusable prompts.

Tip: Tools can use DI: add HttpClient, config, or even the server instance (IMcpServer) as parameters.

3) Run it in VS Code (Copilot Agent Mode)

Create .vscode/mcp.json alongside your .sln/.csproj:

{
  "servers": {
    "MyMcpServer": {
      "type": "stdio",
      "command": "dotnet",
      "args": ["run", "--project", "./MyMcpServer.csproj"]
    }
  }
}

Now open Copilot Chat, toggle Agent Mode, enable your server in the Tools picker, and try:

“Using #reverse_echo, reverse ‘S.O.L.I.D.’ and show the result.”

Path B – Remote server (ASP.NET Core + SSE)

STDIO is perfect locally. For shared/team scenarios or multi‑client access, run your server over HTTP + SSE.

1) New web app + package

dotnet new web -n MyMcpSseServer
cd MyMcpSseServer
# ASP.NET Core transport for MCP
dotnet add package ModelContextProtocol.AspNetCore --prerelease

2) Program.cs – map the MCP endpoints

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

var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddMcpServer()
    .WithToolsFromAssembly()     // reuse our attribute-based tools
    .WithPromptsFromAssembly();

var app = builder.Build();

// Exposes /messages (POST) and /sse (GET) endpoints for MCP Streamable HTTP
app.MapMcp();

app.Run();

// Example tool (same pattern as before)
[McpServerToolType]
public static class TimeTools
{
    [McpServerTool, Description("Gets the current server time in ISO-8601.")]
    public static string Now() => DateTimeOffset.UtcNow.ToString("O");
}

3) Call it from an MCP host

Your host (e.g., VS Code Copilot) needs the server URL and optional headers:

{
  "servers": {
    "MyMcpSseServer": {
      "type": "sse",
      "url": "https://my-mcp.example.com",  
      "headers": { "Authorization": "Bearer ${env:MY_MCP_TOKEN}" }
    }
  }
}

The SSE transport lets multiple clients connect; add auth headers and rate limiting before exposing it publicly.

Making tools useful (DI, config, sampling, structured output)

Let’s fetch data and optionally ask the host model to summarize it (server‑side sampling via the MCP client).

using System.ComponentModel;
using System.Net.Http.Json;
using System.Text.Json;
using ModelContextProtocol.Server;

[McpServerToolType]
public static class WeatherTools
{
    [McpServerTool(Name = "weather_lookup"), Description("Look up fake weather and optionally summarize it via the host model.")]
    public static async Task<string> Lookup(
        HttpClient http,                   // injected
        IMcpServer server,                // injected: access host LLM via sampling
        [Description("City to query")] string city,
        [Description("Return summary from host model")] bool summarize = false,
        CancellationToken ct = default)
    {
        var data = await http.GetFromJsonAsync<WeatherDto>($"https://example.com/api/weather?city={Uri.EscapeDataString(city)}", ct)
                   ?? new WeatherDto { City = city, TempC = 21, Conditions = "balmy" }; // fallback demo

        if (!summarize) return JsonSerializer.Serialize(data);

        // Ask the host’s LLM to summarize (sampling)
        var chat = server.AsSamplingChatClient();
        var response = await chat.GetResponseAsync(new []
        {
            new ChatMessage(ChatRole.System, "You are a helpful assistant."),
            new ChatMessage(ChatRole.User, $"Summarize this weather as one sentence: {JsonSerializer.Serialize(data)}")
        }, new ChatOptions { MaxOutputTokens = 128 }, ct);

        return response.Text;
    }

    private sealed record WeatherDto
    {
        public string City { get; init; } = default!;
        public int TempC { get; init; }
        public string Conditions { get; init; } = default!;
    }
}

Highlights:

  • DI supplies HttpClient, IMcpServer automatically.
  • Sampling lets your server use the host’s LLM – no model SDK in your process.
  • Return JSON for structured output. Most hosts render it nicely or feed it back to the model.

Pro tip: add [Description] to every parameter; hosts surface it in UI and the LLM uses it to pick the right tool.

Configuration & secrets (portable servers)

Your server often needs keys, endpoints, or paths. Use environment variables in dev, wire them in host config.

Example: environment variable input

[McpServerToolType]
public static class CityTools
{
    [McpServerTool(Name = "city_weather"), Description("Weather using a configurable set of descriptors.")]
    public static string GetCityWeather(
        [Description("City name")] string city)
    {
        var choices = Environment.GetEnvironmentVariable("WEATHER_CHOICES") ?? "sunny,rainy,stormy";
        var pick = choices.Split(',')[Random.Shared.Next(0, choices.Split(',').Length)];
        return $"The weather in {city} is {pick}.";
    }
}

VS Code .vscode/mcp.json can pass env vars:

{
  "servers": {
    "MyMcpServer": {
      "type": "stdio",
      "command": "dotnet",
      "args": ["run", "--project", "./MyMcpServer.csproj"],
      "env": { "WEATHER_CHOICES": "sunny,humid,freezing" }
    }
  }
}

Observability & reliability

  • Logging: send normal app logs to stderr to avoid corrupting the JSON‑RPC stream.
  • Cancellation tokens: always accept CancellationToken to make tools cancellable.
  • Validation: validate inputs; fail fast with clear messages – hosts surface them to users.
  • Long‑running work: stream progress as text chunks (SSE) or emit notifications; consider splitting into smaller tools.

Packaging & distribution options

You have several ways to share your server:

  1. .NET Tool / Console
  • Publish self‑contained / Native AOT for tiny binaries.
  • Great for internal use launched via STDIO from hosts.
  1. Container image
  • Add container settings to your .csproj and dotnet publish /t:PublishContainer.
  • Run anywhere and point hosts to your SSE URL.
  1. NuGet package (MCP registry‑ready)
  • Package your server and include an MCP server.json (schema) describing inputs/env vars.
  • Hosts and registries can glean metadata and prefill UI.

Security basics (don’t skip!)

  • Remote servers: require auth – bearer tokens/API keys over HTTPS at minimum. Prefer OAuth when feasible.
  • Scope & least privilege: tools should do one thing well; avoid “god‑mode” operations.
  • Input hardening: every string the model sends you is user input. Validate, sanitize, time‑box, and rate limit.
  • Secrets: read from environment/secret stores; never echo back.

Treat your MCP server like any web API: authentication, authorization, throttling, and thorough logging.

Local vs Remote: which transport should I pick?

  • Pick STDIO when:
    • You’re developing locally.
    • The server should run on the user’s machine (filesystem, IDE tooling).
  • Pick SSE/HTTP when:
    • You need to share across a team or devices.
    • You want centralized infra, autoscaling, and standard HTTP auth.

It’s common to support both using the same tool classes.

Bonus: quick checklists

Server readiness

  • WithToolsFromAssembly() / WithPromptsFromAssembly()
  • Inputs documented with [Description]
  • Logging to stderr
  • CancellationToken in long operations
  • Error messages are friendly & actionable

SSE hardening

  • HTTPS only
  • Bearer/API‑Key header or OAuth
  • Rate limiting + timeouts
  • Request size limits
  • Structured, minimal responses

FAQ: Building an MCP Server in .NET

Do I need .NET 10 preview?

No – .NET 8+ is fine for the basic SDK. The official templates and some tooling niceties target preview builds; you can still wire everything manually as shown.

Can I expose files or data as “resources”?

Yes. MCP supports resources that clients can browse/read. Many hosts also let tools return resource links (URIs) alongside text so users can open or save them. Start with tools first; add resources when you have read‑heavy context.

How do I debug a running server from VS Code?

Attach to the server process (Agent Mode starts it). Set breakpoints in your tool methods; logs to stderr help too.

STDIO or SSE for production?

If you need multiple users or integration with existing auth gateways, SSE wins. If you’re extending a desktop/IDE, STDIO is trivial and fast.

How stable is the SDK?

It’s actively evolving. Keep packages updated, pin versions, and add small integration tests that call your tools end‑to‑end.

Conclusion: Ship tools once – use them everywhere

MCP turns your one‑off LLM adapters into a clean, reusable interface. With a few attributes and a single hosting call, you’ve got tools, prompts, and (optionally) resources available to any MCP‑capable host – locally via STDIO or remotely over SSE.

Pick one capability from your app (search, billing lookup, deploy status) and MCP‑ify it today. Then drop a comment: what will your first tool be?

Leave a Reply

Your email address will not be published. Required fields are marked *