← All articles

Building MCP Server Using .NET

July 25, 2025 · 15 min read

This tutorial walks through building a Model Context Protocol (MCP) server in .NET using stdio transport — the simplest path for local integrations where the client spawns your server as a child process and exchanges JSON-RPC over stdin and stdout.

For remote HTTP-based MCP, see Build MCP Server with Streamable HTTP (recommended) or Build MCP Server with SSE Transport (legacy). To containerize the same stdio server, continue with Run MCP Server in Docker.

Prerequisites

  • .NET 8 SDK or later
  • Basic familiarity with C# and the .NET CLI
  • Optional: Claude Desktop for end-to-end testing
  • Recommended: What is the MCP Protocol? — stdio transport and how clients spawn servers

How stdio transport works

With stdio, there is no HTTP port and no URL. The MCP client (Claude Desktop, Cursor, VS Code, or a custom client) launches your executable and speaks JSON-RPC over the process pipes:

MCP client  ←JSON-RPC on stdout→  MCP Server (console app)
            ←JSON-RPC on stdin←

stdout carries the protocol stream exclusively. Any other output on stdout — including Console.WriteLine or default console logging — corrupts MCP. Write diagnostics to stderr only.

Complete example: Echo MCP server

Create the project

Stdio MCP servers run as a console app, not a web API. Create the project and add the required packages:

dotnet new console -n MyMcpServer
cd MyMcpServer
dotnet add package ModelContextProtocol
dotnet add package Microsoft.Extensions.Hosting

Restore locally to confirm the project compiles:

dotnet build

Configure Program.cs

Replace the contents of Program.cs:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var builder = Host.CreateEmptyApplicationBuilder(settings: null);

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

var app = builder.Build();

await app.RunAsync();

WithStdioServerTransport() wires the server to stdin/stdout.

If you prefer attribute-based discovery instead of registering a specific type, replace .WithTools<MyTools>() with .WithToolsFromAssembly() — the SDK scans the assembly for classes marked with [McpServerToolType].

Define tools

Create a Tools directory and add Tools/MyTools.cs:

using System.ComponentModel;
using ModelContextProtocol.Server;

namespace MyMcpServer.Tools;

[McpServerToolType]
public sealed class MyTools
{
    [McpServerTool, Description("Echoes the input back to the client.")]
    public static string Echo(string message) => message;
}

How it works

  • [McpServerToolType] — marks the class as containing MCP tools
  • [McpServerTool] — exposes the method as a callable tool
  • Description — documents the tool and its parameters for the model; clear descriptions improve when and how the LLM invokes the tool

Tool methods can be static or instance-based. Parameters are mapped from JSON arguments; return values are sent back as tool result content.

Run and smoke test

From the project root:

dotnet run

The process should start and wait — there is no URL to browse. The MCP client will launch this process and attach to stdio when configured.

Manual JSON-RPC test: with the server running, paste this line into the terminal and press Enter:

{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}

You should see a JSON response on stdout listing available tools (for example Echo). If nothing appears, the server may require an initialize request first — see the MCP specification.

Stop the server with Ctrl+C once verified.

Test with Claude Desktop

Claude Desktop spawns the MCP server as a subprocess. Configure it to run your .NET project with dotnet run.

Configure Claude Desktop

Edit claude_desktop_config.json:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json
{
  "mcpServers": {
    "my-mcp-server": {
      "command": "dotnet",
      "args": ["run", "--project", "/absolute/path/to/MyMcpServer"]
    }
  }
}

Replace /absolute/path/to/MyMcpServer with the full path to your .csproj directory. On Windows, use forward slashes or escaped backslashes in the path.

For production, you can point command at the published DLL instead:

{
  "mcpServers": {
    "my-mcp-server": {
      "command": "dotnet",
      "args": ["/absolute/path/to/MyMcpServer/bin/Release/net8.0/MyMcpServer.dll"]
    }
  }
}

Run dotnet publish -c Release first to produce the DLL.

Step-by-step

  1. Build the project: dotnet build
  2. Add the config above to claude_desktop_config.json with your project path
  3. Fully quit Claude Desktop and relaunch (config is read at startup)
  4. Open Claude and check the MCP/tools indicator (hammer icon) — my-mcp-server should appear as connected
  5. Send a test prompt, for example: "Use the echo tool to repeat the phrase hello mcp"
  6. Confirm Claude calls Echo and returns the echoed text

Verify and troubleshoot

  • Claude logs: Help → View Logs (or Developer settings) — look for MCP connection errors
  • Wrong path: ensure the --project path points to the folder containing MyMcpServer.csproj
  • Stale config: quit Claude completely (not just close the window), then relaunch
  • Logs on stdout: use stderr for logging; polluted stdout corrupts the protocol
  • Tools not discovered: confirm [McpServerToolType] and [McpServerTool] attributes are present and the project builds without errors

Best practices

  • Keep tool descriptions clear and specific — the model uses them to decide when to call each tool
  • Validate all inputs before side effects (database writes, API calls, file changes)
  • Log tool invocations to stderr for audit trails; never write diagnostics to stdout
  • Version your server alongside client configs so tool schemas stay in sync
  • Prefer stdio for local desktop integrations; use HTTP transport when the server must run remotely or serve multiple clients

Related reading

Related articles