|
| 1 | +# MCP in .NET — First stdio MCP Server with Read-Only Tools |
| 2 | + |
| 3 | +Full tutorial: [Build Your First MCP Server in C# and .NET](https://www.dotnet-guide.com/tutorials/dotnet-ai/mcp-server-csharp-dotnet/) |
| 4 | + |
| 5 | +This companion sample demonstrates a small, deterministic Model Context Protocol (MCP) workflow in .NET 10. It uses a real MCP client and server over **stdio**, exposes exactly three read-only tools, and verifies discovery and invocation without requiring an LLM, cloud API, API key, database, Docker container, or external service. |
| 6 | + |
| 7 | +> This sample is an independent educational reference from dotnet-guide.com. It is not an official Microsoft or Model Context Protocol specification/sample. |
| 8 | +
|
| 9 | +## What this sample demonstrates |
| 10 | + |
| 11 | +- a .NET 10 MCP server hosted with `Microsoft.Extensions.Hosting`; |
| 12 | +- `ModelContextProtocol` 2.2.0; |
| 13 | +- `AddMcpServer()`, `WithStdioServerTransport()`, and assembly-based tool discovery; |
| 14 | +- `[McpServerToolType]` and `[McpServerTool]` tool declarations; |
| 15 | +- dependency-injected JSON-backed data stores; |
| 16 | +- a real `StdioClientTransport` client; |
| 17 | +- `McpClient.CreateAsync(...)`; |
| 18 | +- tool discovery with `ListToolsAsync()`; |
| 19 | +- direct protocol calls with `CallToolAsync(...)`; |
| 20 | +- protocol-level rejection of an unknown tool; |
| 21 | +- `McpClientTool` compatibility with `Microsoft.Extensions.AI.AIFunction`; |
| 22 | +- deterministic tests that require no model provider. |
| 23 | + |
| 24 | +The three exposed tools are: |
| 25 | + |
| 26 | +```text |
| 27 | +get_service_health |
| 28 | +get_recent_deployment |
| 29 | +get_runbook |
| 30 | +``` |
| 31 | + |
| 32 | +The data is synthetic and deterministic. `get_service_health` and `get_recent_deployment` return operational evidence records; `get_runbook` returns investigation guidance. Runbook guidance is deliberately kept conceptually separate from incident evidence. |
| 33 | + |
| 34 | +## Architecture |
| 35 | + |
| 36 | +```text |
| 37 | +McpClientDemo |
| 38 | + | |
| 39 | + | stdio |
| 40 | + v |
| 41 | +FirstMcpServer |
| 42 | + | |
| 43 | + +-- get_service_health |
| 44 | + | -> ServiceHealthStore |
| 45 | + | |
| 46 | + +-- get_recent_deployment |
| 47 | + | -> DeploymentStore |
| 48 | + | |
| 49 | + +-- get_runbook |
| 50 | + -> RunbookStore |
| 51 | + | |
| 52 | + v |
| 53 | + deterministic JSON data |
| 54 | +``` |
| 55 | + |
| 56 | +See [`docs/architecture.md`](docs/architecture.md) for the focused architecture notes and [`docs/protocol-evidence.md`](docs/protocol-evidence.md) for the verification/evidence boundary. |
| 57 | + |
| 58 | +## Project structure |
| 59 | + |
| 60 | +```text |
| 61 | +FirstMcpServer.slnx |
| 62 | +global.json |
| 63 | +verified-environment.json |
| 64 | +docs/ |
| 65 | +scripts/ |
| 66 | + verify.ps1 |
| 67 | +src/ |
| 68 | + FirstMcpServer/ |
| 69 | + Data/ |
| 70 | + Models/ |
| 71 | + Stores/ |
| 72 | + Tools/ |
| 73 | + McpClientDemo/ |
| 74 | +tests/ |
| 75 | + FirstMcpServer.Tests/ |
| 76 | +``` |
| 77 | + |
| 78 | +`src/FirstMcpServer` is the MCP server. `src/McpClientDemo` launches that server as a child process over stdio, discovers the tools, invokes all three, and verifies an invalid tool call is rejected. The test project covers the deterministic store and tool layers. |
| 79 | + |
| 80 | +## Prerequisites |
| 81 | + |
| 82 | +- .NET SDK **10.0.401** or a compatible .NET 10 environment that honors this sample's `global.json`; |
| 83 | +- PowerShell only if you want to run the convenience verification script. |
| 84 | + |
| 85 | +No OpenAI, Azure OpenAI, OpenRouter, Ollama, API key, database, or Docker installation is required. |
| 86 | + |
| 87 | +## Restore, build, and test |
| 88 | + |
| 89 | +From this sample directory: |
| 90 | + |
| 91 | +```powershell |
| 92 | +dotnet restore .\FirstMcpServer.slnx |
| 93 | +dotnet build .\FirstMcpServer.slnx --configuration Release --no-restore |
| 94 | +dotnet test .\tests\FirstMcpServer.Tests\FirstMcpServer.Tests.csproj --configuration Release --no-build |
| 95 | +``` |
| 96 | + |
| 97 | +The verified local baseline is **13/13 deterministic tests passing**, with a Release build at **0 warnings and 0 errors**. |
| 98 | + |
| 99 | +## Run the real MCP stdio demo |
| 100 | + |
| 101 | +Build first, then run: |
| 102 | + |
| 103 | +```powershell |
| 104 | +dotnet run ` |
| 105 | + --project .\src\McpClientDemo\McpClientDemo.csproj ` |
| 106 | + --configuration Release ` |
| 107 | + --no-build ` |
| 108 | + -- ` |
| 109 | + .\src\FirstMcpServer\bin\Release\net10.0\FirstMcpServer.dll |
| 110 | +``` |
| 111 | + |
| 112 | +The demo launches `FirstMcpServer`, performs the MCP handshake, lists the exposed tools, calls all three tools, deliberately calls `does_not_exist`, confirms the protocol rejects it, and finishes with: |
| 113 | + |
| 114 | +```text |
| 115 | +VERIFY|STDIO_END_TO_END|PASS|... |
| 116 | +FINAL|PASS |
| 117 | +``` |
| 118 | + |
| 119 | +For the complete automated local check: |
| 120 | + |
| 121 | +```powershell |
| 122 | +powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\scripts\verify.ps1 |
| 123 | +``` |
| 124 | + |
| 125 | +That script restores, builds, runs the deterministic tests, captures the package graph, runs the real MCP stdio client/server path, and updates the verification documents. |
| 126 | + |
| 127 | +## Why stdout and stderr matter |
| 128 | + |
| 129 | +With stdio transport, **stdout belongs to the MCP protocol**. Ordinary application diagnostics must not be mixed into that protocol stream. This sample configures host logging so normal diagnostics go to stderr while MCP messages continue over the transport channel. |
| 130 | + |
| 131 | +`McpClientDemo` captures the server's stderr separately and prefixes those lines with `SERVER_STDERR|`. Seeing normal hosting or MCP diagnostic lines there is expected and does not mean the verification failed. |
| 132 | + |
| 133 | +## Environment and secret boundary |
| 134 | + |
| 135 | +The client does not blindly pass every parent-process environment variable to the child MCP server. It disables broad environment inheritance and uses the SDK's safe default environment set required to launch the child process. |
| 136 | + |
| 137 | +This sample has no provider key and no `.env` requirement. Do not add real credentials to source, documentation, tests, committed JSON, CI configuration, or pull-request text. |
| 138 | + |
| 139 | +## Verification |
| 140 | + |
| 141 | +The committed evidence separates what was actually proved from what is merely possible. |
| 142 | + |
| 143 | +| Check | Verified result | |
| 144 | +| --- | --- | |
| 145 | +| .NET SDK baseline | 10.0.401 | |
| 146 | +| Target framework | net10.0 | |
| 147 | +| `ModelContextProtocol` | 2.2.0 | |
| 148 | +| `Microsoft.Extensions.Hosting` | 10.0.12 | |
| 149 | +| `xunit.v3.mtp-v2` | 4.0.1 | |
| 150 | +| Release build | PASS — 0 warnings, 0 errors | |
| 151 | +| Deterministic tests | PASS — 13/13 | |
| 152 | +| MCP stdio client/server | PASS | |
| 153 | +| Discovered tools | PASS — exactly 3 | |
| 154 | +| All three MCP tool calls | PASS | |
| 155 | +| Unknown-tool rejection | PASS | |
| 156 | +| `McpClientTool` -> `AIFunction` assignability | PASS | |
| 157 | +| LLM/cloud API call | Not required / not performed | |
| 158 | + |
| 159 | +See [`docs/verified-environment.md`](docs/verified-environment.md), [`verified-environment.json`](verified-environment.json), and [`docs/sample-runs.md`](docs/sample-runs.md) for the captured evidence. |
| 160 | + |
| 161 | +The verified local run date is recorded in those generated evidence files rather than hard-coded here. |
| 162 | + |
| 163 | +## Important boundary |
| 164 | + |
| 165 | +This first MCP sample intentionally does **not** implement or verify: |
| 166 | + |
| 167 | +- Agent Framework integration; |
| 168 | +- LLM-driven tool selection; |
| 169 | +- Streamable HTTP or remote MCP hosting; |
| 170 | +- OAuth, authentication, or authorization; |
| 171 | +- MCP resources; |
| 172 | +- MCP prompts; |
| 173 | +- sampling; |
| 174 | +- elicitation; |
| 175 | +- MCP Tasks; |
| 176 | +- progress notifications; |
| 177 | +- write-capable or destructive tools; |
| 178 | +- databases or Redis; |
| 179 | +- RAG/vector search; |
| 180 | +- multi-agent orchestration; |
| 181 | +- Docker/Kubernetes/cloud deployment; |
| 182 | +- production readiness. |
| 183 | + |
| 184 | +Those are valid follow-up topics, but adding them here would blur the purpose of this first protocol-focused sample. |
| 185 | + |
| 186 | +## License |
| 187 | + |
| 188 | +Repository-owned sample code is covered by the repository's [MIT License](../../LICENSE). |
0 commit comments