Skip to content

Commit 7e4b75e

Browse files
Merge pull request #29 from dotnet-guide-com/feature/mcp-server-csharp-dotnet
Add first MCP server sample in C# and .NET
2 parents 136e96b + ffc681a commit 7e4b75e

37 files changed

Lines changed: 1779 additions & 8 deletions

‎.github/workflows/build-samples.yml‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ on:
77
- "dotnet-ai/provider-agnostic-chat-gateway/**"
88
- "dotnet-ai/hybrid-search-ef-core-pgvector/**"
99
- "dotnet-ai/agent-framework-incident-triage/**"
10+
- "dotnet-ai/mcp-server-csharp-dotnet/**"
1011
- "dotnet-aspire/orchestrate-distributed-system/**"
1112
- "software-architecture/architecture-testing-dotnet/**"
1213
- "distributed-systems/transactional-outbox-ef-core/**"
@@ -39,6 +40,7 @@ on:
3940
- "dotnet-ai/provider-agnostic-chat-gateway/**"
4041
- "dotnet-ai/hybrid-search-ef-core-pgvector/**"
4142
- "dotnet-ai/agent-framework-incident-triage/**"
43+
- "dotnet-ai/mcp-server-csharp-dotnet/**"
4244
- "dotnet-aspire/orchestrate-distributed-system/**"
4345
- "software-architecture/architecture-testing-dotnet/**"
4446
- "distributed-systems/transactional-outbox-ef-core/**"
@@ -2052,3 +2054,51 @@ jobs:
20522054
dotnet-ai/agent-framework-incident-triage/AgentFrameworkIncidentTriage.slnx
20532055
--configuration Release
20542056
--no-build
2057+
2058+
test-mcp-server-csharp-dotnet:
2059+
name: Test first MCP server sample
2060+
runs-on: ubuntu-latest
2061+
2062+
defaults:
2063+
run:
2064+
working-directory: dotnet-ai/mcp-server-csharp-dotnet
2065+
2066+
steps:
2067+
- name: Check out repository
2068+
uses: actions/checkout@v5
2069+
2070+
- name: Install .NET 10 SDK
2071+
uses: actions/setup-dotnet@v5
2072+
with:
2073+
dotnet-version: "10.0.401"
2074+
2075+
- name: Show selected .NET SDK
2076+
run: dotnet --version
2077+
2078+
- name: Restore
2079+
run: >
2080+
dotnet restore
2081+
FirstMcpServer.slnx
2082+
2083+
- name: Build
2084+
run: >
2085+
dotnet build
2086+
FirstMcpServer.slnx
2087+
--configuration Release
2088+
--no-restore
2089+
2090+
- name: Test deterministic layers
2091+
run: >
2092+
dotnet test
2093+
tests/FirstMcpServer.Tests/FirstMcpServer.Tests.csproj
2094+
--configuration Release
2095+
--no-build
2096+
2097+
- name: Verify MCP stdio end to end
2098+
run: >
2099+
dotnet run
2100+
--project src/McpClientDemo/McpClientDemo.csproj
2101+
--configuration Release
2102+
--no-build
2103+
--
2104+
src/FirstMcpServer/bin/Release/net10.0/FirstMcpServer.dll

‎README.md‎

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Each sample folder contains a focused implementation of one tutorial topic. The
1212
| [`dotnet-ai/provider-agnostic-chat-gateway`](dotnet-ai/provider-agnostic-chat-gateway/) | .NET 10 `Microsoft.Extensions.AI` gateway with allow-listed `IChatClient` providers, bounded conversation state, SSE streaming, safe function calling, and deterministic tests | [Microsoft.Extensions.AI Tutorial: Build a Multi-Provider IChatClient Gateway](https://www.dotnet-guide.com/tutorials/dotnet-ai/provider-agnostic-chat-gateway/) |
1313
| [`dotnet-ai/hybrid-search-ef-core-pgvector`](dotnet-ai/hybrid-search-ef-core-pgvector/) | Verified .NET 10 API combining PostgreSQL full-text search (GIN) and pgvector cosine retrieval (HNSW), fused with Reciprocal Rank Fusion | [Hybrid Search in .NET with EF Core 10 and pgvector](https://www.dotnet-guide.com/tutorials/dotnet-ai/hybrid-search-ef-core-pgvector/) |
1414
| [`dotnet-ai/agent-framework-incident-triage`](dotnet-ai/agent-framework-incident-triage/) | Evidence-first Microsoft Agent Framework sample with read-only tools, deterministic incident evidence, `AgentSession` reuse, and correlation-vs-causation boundaries | [Microsoft Agent Framework Tutorial in C#: Build a Tool-Using AI Agent](https://www.dotnet-guide.com/tutorials/dotnet-ai/agent-framework-incident-triage/) |
15+
| [`dotnet-ai/mcp-server-csharp-dotnet`](dotnet-ai/mcp-server-csharp-dotnet/) | .NET 10 MCP stdio server with three read-only tools, deterministic JSON-backed data, real client discovery/invocation, protocol error verification, and deterministic tests | [Build Your First MCP Server in C# and .NET](https://www.dotnet-guide.com/tutorials/dotnet-ai/mcp-server-csharp-dotnet/) |
1516
| [`dotnet-aspire/orchestrate-distributed-system`](dotnet-aspire/orchestrate-distributed-system/) | Minimal Aspire AppHost coordinating a web project and API with service discovery and startup ordering | [Aspire in .NET: Orchestrate, Run, and Deploy a Distributed System from One App Host](https://www.dotnet-guide.com/tutorials/dotnet-aspire/orchestrate-distributed-system/) |
1617
| [`software-architecture/architecture-testing-dotnet`](software-architecture/architecture-testing-dotnet/) | Minimal NetArchTest.eNhancedEdition rule that prevents Domain from depending on outer layers | [Architecture Testing in .NET: Enforce Layer and Module Boundaries with NetArchTest and ArchUnitNET](https://www.dotnet-guide.com/tutorials/software-architecture/architecture-testing-dotnet/) |
1718
| [`distributed-systems/transactional-outbox-ef-core`](distributed-systems/transactional-outbox-ef-core/) | Minimal EF Core and SQLite demonstration that saves business state and an outbox message atomically, then publishes it through a one-shot relay | [Transactional Outbox Pattern in .NET with EF Core (.NET 10): Fix the Dual-Write Problem](https://www.dotnet-guide.com/tutorials/distributed-systems/transactional-outbox-ef-core/) |
@@ -81,6 +82,15 @@ dotnet restore
8182
dotnet test
8283
```
8384

85+
### First MCP server in C# and .NET
86+
87+
```powershell
88+
cd dotnet-ai\mcp-server-csharp-dotnet
89+
dotnet restore .\FirstMcpServer.slnx
90+
dotnet build .\FirstMcpServer.slnx --configuration Release --no-restore
91+
dotnet test .\tests\FirstMcpServer.Tests\FirstMcpServer.Tests.csproj --configuration Release --no-build
92+
dotnet run --project .\src\McpClientDemo\McpClientDemo.csproj --configuration Release --no-build -- .\src\FirstMcpServer\bin\Release\net10.0\FirstMcpServer.dll
93+
```
8494
## Repository structure
8595

8696
```text
@@ -94,18 +104,28 @@ tutorials/
94104
| | |-- docs/
95105
| | |-- src/
96106
| | `-- tests/
97-
| `-- hybrid-search-ef-core-pgvector/
98-
| |-- HybridSearch.slnx
99-
| |-- docker-compose.yml
100-
| |-- README.md
101-
| |-- src/HybridSearch.Api/
102-
| |-- tests/HybridSearch.Api.Tests/
103-
| `-- scripts/verify.ps1| `-- agent-framework-incident-triage/
104-
| |-- AgentFrameworkIncidentTriage.slnx
107+
| |-- hybrid-search-ef-core-pgvector/
108+
| | |-- HybridSearch.slnx
109+
| | |-- docker-compose.yml
110+
| | |-- README.md
111+
| | |-- src/HybridSearch.Api/
112+
| | |-- tests/HybridSearch.Api.Tests/
113+
| | `-- scripts/verify.ps1
114+
| |-- agent-framework-incident-triage/
115+
| | |-- AgentFrameworkIncidentTriage.slnx
116+
| | |-- README.md
117+
| | |-- global.json
118+
| | |-- verified-environment.json
119+
| | |-- docs/
120+
| | |-- src/
121+
| | `-- tests/
122+
| `-- mcp-server-csharp-dotnet/
123+
| |-- FirstMcpServer.slnx
105124
| |-- README.md
106125
| |-- global.json
107126
| |-- verified-environment.json
108127
| |-- docs/
128+
| |-- scripts/
109129
| |-- src/
110130
| `-- tests/
111131
|-- dotnet-aspire/
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Build outputs
2+
bin/
3+
obj/
4+
TestResults/
5+
6+
# Local verification output
7+
verification-results/
8+
9+
# IDE / user state
10+
.vs/
11+
.vscode/
12+
.idea/
13+
*.user
14+
*.suo
15+
16+
# Secrets / local overrides
17+
.env
18+
.env.*
19+
secrets/
20+
*.pfx
21+
*.p12
22+
23+
# Logs / temp
24+
*.log
25+
*.tmp
26+
*.bak
27+
*~
28+
29+
# Builder safety marker (never publish)
30+
.dotnet-guide-builder-marker
31+
32+
_verification-metadata-patch-backup/
33+
_publication-hardening-backup/
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<Solution>
2+
<Folder Name="/src/">
3+
<Project Path="src/FirstMcpServer/FirstMcpServer.csproj" />
4+
<Project Path="src/McpClientDemo/McpClientDemo.csproj" />
5+
</Folder>
6+
<Folder Name="/tests/">
7+
<Project Path="tests/FirstMcpServer.Tests/FirstMcpServer.Tests.csproj" />
8+
</Folder>
9+
</Solution>
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
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

Comments
 (0)