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.
Key Takeaways
- Use the MCP .NET SDK with stdio transport for local desktop clients.
- Register tools with typed schemas so clients discover capabilities at runtime.
- Pass secrets via environment variables—never embed tokens in source or logs.
- Stdio avoids TLS complexity; HTTP fits shared remote deployment.
- Containerize when you need reproducible deps—not because stdio requires it.
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
FAQ
How do I build an MCP server with stdio in .NET?
Add the official MCP .NET SDK, implement tool handlers that expose your domain operations, and host the server using stdio transport so the client launches your process and communicates over stdin and stdout. Configure Claude Desktop or Cursor with a JSON block pointing at your published executable and required environment variables. Stdio avoids HTTP certificates and suits local development and single-user setups.
Why choose stdio over HTTP for an MCP server?
Stdio is the simplest path for desktop AI clients that spawn subprocesses—no ports, reverse proxies, or TLS to manage. HTTP transports fit shared team servers and remote deployment. Start with stdio to prove tools and auth; graduate to Streamable HTTP when multiple users or hosts need the same backend.
How do I pass secrets to a stdio MCP server safely?
Use environment variables referenced in the client config—not hard-coded tokens in source. Claude Desktop supports env blocks per server entry. Never log secrets; redact tool arguments in diagnostics. Rotate credentials if configs leak into screenshots or shared dotfiles.
Can I run the same .NET MCP server in Docker with stdio?
Yes. Containerize the published binary and point the client at docker run with interactive stdio flags. Docker adds isolation and reproducible dependencies; see the Docker guide for Claude Desktop wiring patterns. Remote multi-user production usually moves to Streamable HTTP instead.
