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 toolDescription— 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
- Build the project:
dotnet build - Add the config above to
claude_desktop_config.jsonwith your project path - Fully quit Claude Desktop and relaunch (config is read at startup)
- Open Claude and check the MCP/tools indicator (hammer icon) —
my-mcp-servershould appear as connected - Send a test prompt, for example: "Use the echo tool to repeat the phrase hello mcp"
- Confirm Claude calls
Echoand returns the echoed text
Verify and troubleshoot
- Claude logs: Help → View Logs (or Developer settings) — look for MCP connection errors
- Wrong path: ensure the
--projectpath points to the folder containingMyMcpServer.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
- What is the MCP Protocol? — transports, tools, and client setup
- Run MCP Server in Docker — containerize this stdio server
- Build MCP Server with Streamable HTTP — recommended remote HTTP MCP
- Build MCP Server with SSE Transport — legacy HTTP+SSE
- Server-Sent Events (SSE) and EventSource — SSE protocol deep dive