diff --git a/.github/workflows/build-samples.yml b/.github/workflows/build-samples.yml
index 8644b5e..93e3ec4 100644
--- a/.github/workflows/build-samples.yml
+++ b/.github/workflows/build-samples.yml
@@ -7,6 +7,7 @@ on:
- "dotnet-ai/provider-agnostic-chat-gateway/**"
- "dotnet-ai/hybrid-search-ef-core-pgvector/**"
- "dotnet-ai/agent-framework-incident-triage/**"
+ - "dotnet-ai/mcp-server-csharp-dotnet/**"
- "dotnet-aspire/orchestrate-distributed-system/**"
- "software-architecture/architecture-testing-dotnet/**"
- "distributed-systems/transactional-outbox-ef-core/**"
@@ -39,6 +40,7 @@ on:
- "dotnet-ai/provider-agnostic-chat-gateway/**"
- "dotnet-ai/hybrid-search-ef-core-pgvector/**"
- "dotnet-ai/agent-framework-incident-triage/**"
+ - "dotnet-ai/mcp-server-csharp-dotnet/**"
- "dotnet-aspire/orchestrate-distributed-system/**"
- "software-architecture/architecture-testing-dotnet/**"
- "distributed-systems/transactional-outbox-ef-core/**"
@@ -2052,3 +2054,51 @@ jobs:
dotnet-ai/agent-framework-incident-triage/AgentFrameworkIncidentTriage.slnx
--configuration Release
--no-build
+
+ test-mcp-server-csharp-dotnet:
+ name: Test first MCP server sample
+ runs-on: ubuntu-latest
+
+ defaults:
+ run:
+ working-directory: dotnet-ai/mcp-server-csharp-dotnet
+
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v5
+
+ - name: Install .NET 10 SDK
+ uses: actions/setup-dotnet@v5
+ with:
+ dotnet-version: "10.0.401"
+
+ - name: Show selected .NET SDK
+ run: dotnet --version
+
+ - name: Restore
+ run: >
+ dotnet restore
+ FirstMcpServer.slnx
+
+ - name: Build
+ run: >
+ dotnet build
+ FirstMcpServer.slnx
+ --configuration Release
+ --no-restore
+
+ - name: Test deterministic layers
+ run: >
+ dotnet test
+ tests/FirstMcpServer.Tests/FirstMcpServer.Tests.csproj
+ --configuration Release
+ --no-build
+
+ - name: Verify MCP stdio end to end
+ run: >
+ dotnet run
+ --project src/McpClientDemo/McpClientDemo.csproj
+ --configuration Release
+ --no-build
+ --
+ src/FirstMcpServer/bin/Release/net10.0/FirstMcpServer.dll
diff --git a/README.md b/README.md
index dc0b307..602f919 100644
--- a/README.md
+++ b/README.md
@@ -12,6 +12,7 @@ Each sample folder contains a focused implementation of one tutorial topic. The
| [`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/) |
| [`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/) |
| [`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/) |
+| [`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/) |
| [`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/) |
| [`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/) |
| [`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
dotnet test
```
+### First MCP server in C# and .NET
+
+```powershell
+cd dotnet-ai\mcp-server-csharp-dotnet
+dotnet restore .\FirstMcpServer.slnx
+dotnet build .\FirstMcpServer.slnx --configuration Release --no-restore
+dotnet test .\tests\FirstMcpServer.Tests\FirstMcpServer.Tests.csproj --configuration Release --no-build
+dotnet run --project .\src\McpClientDemo\McpClientDemo.csproj --configuration Release --no-build -- .\src\FirstMcpServer\bin\Release\net10.0\FirstMcpServer.dll
+```
## Repository structure
```text
@@ -94,18 +104,28 @@ tutorials/
| | |-- docs/
| | |-- src/
| | `-- tests/
-| `-- hybrid-search-ef-core-pgvector/
-| |-- HybridSearch.slnx
-| |-- docker-compose.yml
-| |-- README.md
-| |-- src/HybridSearch.Api/
-| |-- tests/HybridSearch.Api.Tests/
-| `-- scripts/verify.ps1| `-- agent-framework-incident-triage/
-| |-- AgentFrameworkIncidentTriage.slnx
+| |-- hybrid-search-ef-core-pgvector/
+| | |-- HybridSearch.slnx
+| | |-- docker-compose.yml
+| | |-- README.md
+| | |-- src/HybridSearch.Api/
+| | |-- tests/HybridSearch.Api.Tests/
+| | `-- scripts/verify.ps1
+| |-- agent-framework-incident-triage/
+| | |-- AgentFrameworkIncidentTriage.slnx
+| | |-- README.md
+| | |-- global.json
+| | |-- verified-environment.json
+| | |-- docs/
+| | |-- src/
+| | `-- tests/
+| `-- mcp-server-csharp-dotnet/
+| |-- FirstMcpServer.slnx
| |-- README.md
| |-- global.json
| |-- verified-environment.json
| |-- docs/
+| |-- scripts/
| |-- src/
| `-- tests/
|-- dotnet-aspire/
diff --git a/dotnet-ai/mcp-server-csharp-dotnet/.gitignore b/dotnet-ai/mcp-server-csharp-dotnet/.gitignore
new file mode 100644
index 0000000..b48e083
--- /dev/null
+++ b/dotnet-ai/mcp-server-csharp-dotnet/.gitignore
@@ -0,0 +1,33 @@
+# Build outputs
+bin/
+obj/
+TestResults/
+
+# Local verification output
+verification-results/
+
+# IDE / user state
+.vs/
+.vscode/
+.idea/
+*.user
+*.suo
+
+# Secrets / local overrides
+.env
+.env.*
+secrets/
+*.pfx
+*.p12
+
+# Logs / temp
+*.log
+*.tmp
+*.bak
+*~
+
+# Builder safety marker (never publish)
+.dotnet-guide-builder-marker
+
+_verification-metadata-patch-backup/
+_publication-hardening-backup/
diff --git a/dotnet-ai/mcp-server-csharp-dotnet/FirstMcpServer.slnx b/dotnet-ai/mcp-server-csharp-dotnet/FirstMcpServer.slnx
new file mode 100644
index 0000000..f9fa509
--- /dev/null
+++ b/dotnet-ai/mcp-server-csharp-dotnet/FirstMcpServer.slnx
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/dotnet-ai/mcp-server-csharp-dotnet/README.md b/dotnet-ai/mcp-server-csharp-dotnet/README.md
new file mode 100644
index 0000000..d1e2b65
--- /dev/null
+++ b/dotnet-ai/mcp-server-csharp-dotnet/README.md
@@ -0,0 +1,188 @@
+# MCP in .NET — First stdio MCP Server with Read-Only Tools
+
+Full tutorial: [Build Your First MCP Server in C# and .NET](https://www.dotnet-guide.com/tutorials/dotnet-ai/mcp-server-csharp-dotnet/)
+
+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.
+
+> This sample is an independent educational reference from dotnet-guide.com. It is not an official Microsoft or Model Context Protocol specification/sample.
+
+## What this sample demonstrates
+
+- a .NET 10 MCP server hosted with `Microsoft.Extensions.Hosting`;
+- `ModelContextProtocol` 2.2.0;
+- `AddMcpServer()`, `WithStdioServerTransport()`, and assembly-based tool discovery;
+- `[McpServerToolType]` and `[McpServerTool]` tool declarations;
+- dependency-injected JSON-backed data stores;
+- a real `StdioClientTransport` client;
+- `McpClient.CreateAsync(...)`;
+- tool discovery with `ListToolsAsync()`;
+- direct protocol calls with `CallToolAsync(...)`;
+- protocol-level rejection of an unknown tool;
+- `McpClientTool` compatibility with `Microsoft.Extensions.AI.AIFunction`;
+- deterministic tests that require no model provider.
+
+The three exposed tools are:
+
+```text
+get_service_health
+get_recent_deployment
+get_runbook
+```
+
+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.
+
+## Architecture
+
+```text
+McpClientDemo
+ |
+ | stdio
+ v
+FirstMcpServer
+ |
+ +-- get_service_health
+ | -> ServiceHealthStore
+ |
+ +-- get_recent_deployment
+ | -> DeploymentStore
+ |
+ +-- get_runbook
+ -> RunbookStore
+ |
+ v
+ deterministic JSON data
+```
+
+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.
+
+## Project structure
+
+```text
+FirstMcpServer.slnx
+global.json
+verified-environment.json
+docs/
+scripts/
+ verify.ps1
+src/
+ FirstMcpServer/
+ Data/
+ Models/
+ Stores/
+ Tools/
+ McpClientDemo/
+tests/
+ FirstMcpServer.Tests/
+```
+
+`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.
+
+## Prerequisites
+
+- .NET SDK **10.0.401** or a compatible .NET 10 environment that honors this sample's `global.json`;
+- PowerShell only if you want to run the convenience verification script.
+
+No OpenAI, Azure OpenAI, OpenRouter, Ollama, API key, database, or Docker installation is required.
+
+## Restore, build, and test
+
+From this sample directory:
+
+```powershell
+dotnet restore .\FirstMcpServer.slnx
+dotnet build .\FirstMcpServer.slnx --configuration Release --no-restore
+dotnet test .\tests\FirstMcpServer.Tests\FirstMcpServer.Tests.csproj --configuration Release --no-build
+```
+
+The verified local baseline is **13/13 deterministic tests passing**, with a Release build at **0 warnings and 0 errors**.
+
+## Run the real MCP stdio demo
+
+Build first, then run:
+
+```powershell
+dotnet run `
+ --project .\src\McpClientDemo\McpClientDemo.csproj `
+ --configuration Release `
+ --no-build `
+ -- `
+ .\src\FirstMcpServer\bin\Release\net10.0\FirstMcpServer.dll
+```
+
+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:
+
+```text
+VERIFY|STDIO_END_TO_END|PASS|...
+FINAL|PASS
+```
+
+For the complete automated local check:
+
+```powershell
+powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\scripts\verify.ps1
+```
+
+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.
+
+## Why stdout and stderr matter
+
+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.
+
+`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.
+
+## Environment and secret boundary
+
+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.
+
+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.
+
+## Verification
+
+The committed evidence separates what was actually proved from what is merely possible.
+
+| Check | Verified result |
+| --- | --- |
+| .NET SDK baseline | 10.0.401 |
+| Target framework | net10.0 |
+| `ModelContextProtocol` | 2.2.0 |
+| `Microsoft.Extensions.Hosting` | 10.0.12 |
+| `xunit.v3.mtp-v2` | 4.0.1 |
+| Release build | PASS — 0 warnings, 0 errors |
+| Deterministic tests | PASS — 13/13 |
+| MCP stdio client/server | PASS |
+| Discovered tools | PASS — exactly 3 |
+| All three MCP tool calls | PASS |
+| Unknown-tool rejection | PASS |
+| `McpClientTool` -> `AIFunction` assignability | PASS |
+| LLM/cloud API call | Not required / not performed |
+
+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.
+
+The verified local run date is recorded in those generated evidence files rather than hard-coded here.
+
+## Important boundary
+
+This first MCP sample intentionally does **not** implement or verify:
+
+- Agent Framework integration;
+- LLM-driven tool selection;
+- Streamable HTTP or remote MCP hosting;
+- OAuth, authentication, or authorization;
+- MCP resources;
+- MCP prompts;
+- sampling;
+- elicitation;
+- MCP Tasks;
+- progress notifications;
+- write-capable or destructive tools;
+- databases or Redis;
+- RAG/vector search;
+- multi-agent orchestration;
+- Docker/Kubernetes/cloud deployment;
+- production readiness.
+
+Those are valid follow-up topics, but adding them here would blur the purpose of this first protocol-focused sample.
+
+## License
+
+Repository-owned sample code is covered by the repository's [MIT License](../../LICENSE).
diff --git a/dotnet-ai/mcp-server-csharp-dotnet/SCOPE.md b/dotnet-ai/mcp-server-csharp-dotnet/SCOPE.md
new file mode 100644
index 0000000..aa8eae8
--- /dev/null
+++ b/dotnet-ai/mcp-server-csharp-dotnet/SCOPE.md
@@ -0,0 +1,40 @@
+# MCP #1 Scope
+
+## In scope
+
+- .NET 10 / SDK 10.0.401
+- ModelContextProtocol 2.2.0
+- stdio transport
+- MCP client/server handshake
+- attribute-based tool discovery
+- dependency injection into MCP tools
+- three deterministic read-only tools
+- local JSON-backed data stores
+- direct MCP tool invocation
+- unknown-tool rejection
+- deterministic tests
+- local end-to-end verification
+- CI-suitable verification without secrets
+
+## Out of scope
+
+- Agent Framework
+- IChatClient model execution
+- LLM-driven tool selection
+- OpenAI / Azure OpenAI / OpenRouter / Ollama
+- Streamable HTTP / remote MCP hosting
+- OAuth / authentication / authorization
+- MCP resources
+- MCP prompts
+- sampling
+- elicitation
+- MCP Tasks
+- progress notifications
+- databases / Redis
+- write-capable or destructive tools
+- automatic remediation
+- RAG / vector search
+- multi-agent orchestration
+- Docker / Kubernetes / cloud deployment
+
+The goal is to teach and verify the MCP protocol boundary before adding model-driven behavior.
diff --git a/dotnet-ai/mcp-server-csharp-dotnet/docs/architecture.md b/dotnet-ai/mcp-server-csharp-dotnet/docs/architecture.md
new file mode 100644
index 0000000..e37a6fc
--- /dev/null
+++ b/dotnet-ai/mcp-server-csharp-dotnet/docs/architecture.md
@@ -0,0 +1,25 @@
+# Architecture
+
+```text
+McpClientDemo
+ |
+ | stdio
+ v
+FirstMcpServer
+ |
+ +-- get_service_health
+ | -> ServiceHealthStore
+ |
+ +-- get_recent_deployment
+ | -> DeploymentStore
+ |
+ +-- get_runbook
+ -> RunbookStore
+ |
+ v
+ deterministic JSON data
+```
+
+The MCP server owns trusted tool registration and deterministic local data. The client can discover and invoke only the tools exposed by the server.
+
+The sample deliberately uses stdio so protocol behavior can be demonstrated without networking, authentication, hosting infrastructure, or an LLM.
diff --git a/dotnet-ai/mcp-server-csharp-dotnet/docs/package-graph.txt b/dotnet-ai/mcp-server-csharp-dotnet/docs/package-graph.txt
new file mode 100644
index 0000000..8197f9c
--- /dev/null
+++ b/dotnet-ai/mcp-server-csharp-dotnet/docs/package-graph.txt
@@ -0,0 +1,112 @@
+ Determining projects to restore...
+ All projects are up-to-date for restore.
+Project 'FirstMcpServer' has the following package references
+ [net10.0]:
+ Top-level Package Requested Resolved
+ > Microsoft.Extensions.Hosting 10.0.12 10.0.12
+ > ModelContextProtocol 2.2.0 2.2.0
+
+ Transitive Package Resolved
+ > Microsoft.Extensions.AI.Abstractions 10.8.3
+ > Microsoft.Extensions.Caching.Abstractions 10.0.10
+ > Microsoft.Extensions.Configuration 10.0.12
+ > Microsoft.Extensions.Configuration.Abstractions 10.0.12
+ > Microsoft.Extensions.Configuration.Binder 10.0.12
+ > Microsoft.Extensions.Configuration.CommandLine 10.0.12
+ > Microsoft.Extensions.Configuration.EnvironmentVariables 10.0.12
+ > Microsoft.Extensions.Configuration.FileExtensions 10.0.12
+ > Microsoft.Extensions.Configuration.Json 10.0.12
+ > Microsoft.Extensions.Configuration.UserSecrets 10.0.12
+ > Microsoft.Extensions.DependencyInjection 10.0.12
+ > Microsoft.Extensions.DependencyInjection.Abstractions 10.0.12
+ > Microsoft.Extensions.Diagnostics 10.0.12
+ > Microsoft.Extensions.Diagnostics.Abstractions 10.0.12
+ > Microsoft.Extensions.FileProviders.Abstractions 10.0.12
+ > Microsoft.Extensions.FileProviders.Physical 10.0.12
+ > Microsoft.Extensions.FileSystemGlobbing 10.0.12
+ > Microsoft.Extensions.Hosting.Abstractions 10.0.12
+ > Microsoft.Extensions.Logging 10.0.12
+ > Microsoft.Extensions.Logging.Abstractions 10.0.12
+ > Microsoft.Extensions.Logging.Configuration 10.0.12
+ > Microsoft.Extensions.Logging.Console 10.0.12
+ > Microsoft.Extensions.Logging.Debug 10.0.12
+ > Microsoft.Extensions.Logging.EventLog 10.0.12
+ > Microsoft.Extensions.Logging.EventSource 10.0.12
+ > Microsoft.Extensions.Options 10.0.12
+ > Microsoft.Extensions.Options.ConfigurationExtensions 10.0.12
+ > Microsoft.Extensions.Primitives 10.0.12
+ > ModelContextProtocol.Core 2.2.0
+ > System.Diagnostics.EventLog 10.0.12
+
+Project 'McpClientDemo' has the following package references
+ [net10.0]:
+ Top-level Package Requested Resolved
+ > ModelContextProtocol 2.2.0 2.2.0
+
+ Transitive Package Resolved
+ > Microsoft.Extensions.AI.Abstractions 10.8.3
+ > Microsoft.Extensions.Caching.Abstractions 10.0.10
+ > Microsoft.Extensions.Configuration.Abstractions 10.0.10
+ > Microsoft.Extensions.DependencyInjection.Abstractions 10.0.10
+ > Microsoft.Extensions.Diagnostics.Abstractions 10.0.10
+ > Microsoft.Extensions.FileProviders.Abstractions 10.0.10
+ > Microsoft.Extensions.Hosting.Abstractions 10.0.10
+ > Microsoft.Extensions.Logging.Abstractions 10.0.10
+ > Microsoft.Extensions.Options 10.0.10
+ > Microsoft.Extensions.Primitives 10.0.10
+ > ModelContextProtocol.Core 2.2.0
+
+Project 'FirstMcpServer.Tests' has the following package references
+ [net10.0]:
+ Top-level Package Requested Resolved
+ > xunit.v3.mtp-v2 4.0.1 4.0.1
+
+ Transitive Package Resolved
+ > Microsoft.ApplicationInsights 2.23.0
+ > Microsoft.Bcl.AsyncInterfaces 6.0.0
+ > Microsoft.Extensions.AI.Abstractions 10.8.3
+ > Microsoft.Extensions.Caching.Abstractions 10.0.10
+ > Microsoft.Extensions.Configuration 10.0.12
+ > Microsoft.Extensions.Configuration.Abstractions 10.0.12
+ > Microsoft.Extensions.Configuration.Binder 10.0.12
+ > Microsoft.Extensions.Configuration.CommandLine 10.0.12
+ > Microsoft.Extensions.Configuration.EnvironmentVariables 10.0.12
+ > Microsoft.Extensions.Configuration.FileExtensions 10.0.12
+ > Microsoft.Extensions.Configuration.Json 10.0.12
+ > Microsoft.Extensions.Configuration.UserSecrets 10.0.12
+ > Microsoft.Extensions.DependencyInjection 10.0.12
+ > Microsoft.Extensions.DependencyInjection.Abstractions 10.0.12
+ > Microsoft.Extensions.Diagnostics 10.0.12
+ > Microsoft.Extensions.Diagnostics.Abstractions 10.0.12
+ > Microsoft.Extensions.FileProviders.Abstractions 10.0.12
+ > Microsoft.Extensions.FileProviders.Physical 10.0.12
+ > Microsoft.Extensions.FileSystemGlobbing 10.0.12
+ > Microsoft.Extensions.Hosting 10.0.12
+ > Microsoft.Extensions.Hosting.Abstractions 10.0.12
+ > Microsoft.Extensions.Logging 10.0.12
+ > Microsoft.Extensions.Logging.Abstractions 10.0.12
+ > Microsoft.Extensions.Logging.Configuration 10.0.12
+ > Microsoft.Extensions.Logging.Console 10.0.12
+ > Microsoft.Extensions.Logging.Debug 10.0.12
+ > Microsoft.Extensions.Logging.EventLog 10.0.12
+ > Microsoft.Extensions.Logging.EventSource 10.0.12
+ > Microsoft.Extensions.Options 10.0.12
+ > Microsoft.Extensions.Options.ConfigurationExtensions 10.0.12
+ > Microsoft.Extensions.Primitives 10.0.12
+ > Microsoft.Testing.Extensions.Telemetry 2.4.0
+ > Microsoft.Testing.Extensions.TrxReport.Abstractions 2.4.0
+ > Microsoft.Testing.Platform 2.4.0
+ > Microsoft.Testing.Platform.MSBuild 2.4.0
+ > Microsoft.Win32.Registry 5.0.0
+ > ModelContextProtocol 2.2.0
+ > ModelContextProtocol.Core 2.2.0
+ > System.Diagnostics.EventLog 10.0.12
+ > System.Security.AccessControl 6.0.1
+ > xunit.analyzers 2.1.0
+ > xunit.v3.assert 4.0.1
+ > xunit.v3.common 4.0.1
+ > xunit.v3.core.mtp-v2 4.0.1
+ > xunit.v3.extensibility.core 4.0.1
+ > xunit.v3.runner.common 4.0.1
+ > xunit.v3.runner.inproc.console 4.0.1
+
diff --git a/dotnet-ai/mcp-server-csharp-dotnet/docs/protocol-evidence.md b/dotnet-ai/mcp-server-csharp-dotnet/docs/protocol-evidence.md
new file mode 100644
index 0000000..0270019
--- /dev/null
+++ b/dotnet-ai/mcp-server-csharp-dotnet/docs/protocol-evidence.md
@@ -0,0 +1,12 @@
+# Protocol Evidence
+
+The verification workflow distinguishes several evidence levels.
+
+- **COMPILE VERIFIED** — the pinned project/package/API surface compiles.
+- **DETERMINISTICALLY TESTED** — store and tool behavior passes local automated tests.
+- **MCP STDIO E2E VERIFIED** — a real MCP client launches a real MCP server, discovers the tools, invokes them over stdio, and observes expected results.
+- **CI VERIFIED** — reserved for a later GitHub Actions run after publication.
+
+The sample does not claim LLM-driven tool selection, HTTP transport, authentication, production readiness, or coverage of all MCP features.
+
+`McpClientTool` being assignable to `Microsoft.Extensions.AI.AIFunction` is recorded as an extension point only; the sample does not use an LLM.
diff --git a/dotnet-ai/mcp-server-csharp-dotnet/docs/sample-runs.md b/dotnet-ai/mcp-server-csharp-dotnet/docs/sample-runs.md
new file mode 100644
index 0000000..003abe3
--- /dev/null
+++ b/dotnet-ai/mcp-server-csharp-dotnet/docs/sample-runs.md
@@ -0,0 +1,23 @@
+# Sample Runs
+
+Captured from the successful local MCP stdio verification on 2026-10-05.
+
+` ext
+VERIFY|SERVER_DLL|PASS|\src\FirstMcpServer\bin\Release\net10.0\FirstMcpServer.dll
+RUNTIME|TransportType|ModelContextProtocol.Client.StdioClientTransport
+RUNTIME|ClientType|ModelContextProtocol.Client.McpClientImpl
+RUNTIME|ToolCount|3
+TOOL|get_recent_deployment|ModelContextProtocol.Client.McpClientTool|Returns deterministic synthetic evidence for the most recent known deployment of a service.
+TOOL|get_runbook|ModelContextProtocol.Client.McpClientTool|Returns deterministic runbook guidance for a known service. Guidance is not incident evidence.
+TOOL|get_service_health|ModelContextProtocol.Client.McpClientTool|Returns deterministic synthetic service-health evidence for a known service.
+VERIFY|TOOL_DISCOVERY|PASS|get_recent_deployment,get_runbook,get_service_health
+VERIFY|AIFUNCTION_ASSIGNABILITY|PASS|McpClientTool derives from AIFunction
+VERIFY|SERVICE_HEALTH_CALL|PASS|{"evidenceId":"E-101","service":"checkout-api","observedAt":"2026-09-24T14:12:00Z","status":"degraded","errorRate":18.7,"errorSpikeStarted":"2026-09-24T14:09:00Z","databasePoolUtilization":96}
+VERIFY|DEPLOYMENT_CALL|PASS|{"evidenceId":"E-201","service":"checkout-api","deployment":"deploy-1842","deploymentStatus":"succeeded","completedAt":"2026-09-24T14:02:00Z","previousDeployment":"deploy-1839","changeSummary":"pricing-rule refresh and structured logging update","rollbackPerformed":false}
+VERIFY|RUNBOOK_CALL|PASS|{"runbookId":"RB-CHECKOUT-03","service":"checkout-api","title":"Checkout API degraded-service investigation","guidance":["Compare error-rate onset with recent deployments.","Inspect database connection-pool utilization.","Check connection counts before and after deployment.","Review traces for timeout or connection-acquisition failures.","Compare configuration with the previous known-good release."],"caution":"Runbook guidance suggests investigation steps; it does not establish root cause."}
+VERIFY|UNKNOWN_TOOL_REJECTION|PASS|McpProtocolException: Request failed (remote): Unknown tool: 'does_not_exist'
+VERIFY|STDIO_END_TO_END|PASS|Client launched server, discovered exactly three tools, and invoked all three over MCP stdio
+FINAL|PASS
+`
+
+Server diagnostic lines are omitted from this curated view. The raw local output remains in erification-results/mcp-client-output.txt.
\ No newline at end of file
diff --git a/dotnet-ai/mcp-server-csharp-dotnet/docs/verified-environment.md b/dotnet-ai/mcp-server-csharp-dotnet/docs/verified-environment.md
new file mode 100644
index 0000000..3241281
--- /dev/null
+++ b/dotnet-ai/mcp-server-csharp-dotnet/docs/verified-environment.md
@@ -0,0 +1,32 @@
+# Verified Environment
+
+- Verification timestamp: 2026-10-05T21:36:33+05:30
+- .NET SDK: 10.0.401
+- Target framework: net10.0
+- ModelContextProtocol: 2.2.0
+- Microsoft.Extensions.Hosting: 10.0.12
+- xunit.v3.mtp-v2: 4.0.1
+- Transport: stdio
+- Release build: PASS
+- Deterministic tests: 13/13 passed; 0 failed; 0 skipped
+- MCP stdio end-to-end: PASS
+- Runtime transport type: ModelContextProtocol.Client.StdioClientTransport
+- Runtime client type: ModelContextProtocol.Client.McpClientImpl
+- Discovered tool count: 3
+
+## Verified behaviors
+
+- real MCP client/server handshake;
+- exactly three tool definitions discovered;
+- get_service_health invoked over MCP stdio;
+- get_recent_deployment invoked over MCP stdio;
+- get_runbook invoked over MCP stdio;
+- unknown tool rejected;
+- McpClientTool assignable to AIFunction;
+- no LLM or cloud API required.
+
+## Evidence boundary
+
+This verification does not establish HTTP transport behavior, authentication/OAuth, LLM-driven tool selection, Agent Framework integration, production readiness, or coverage of MCP features outside this sample.
+
+Raw local outputs are under erification-results/ and are intentionally gitignored.
\ No newline at end of file
diff --git a/dotnet-ai/mcp-server-csharp-dotnet/global.json b/dotnet-ai/mcp-server-csharp-dotnet/global.json
new file mode 100644
index 0000000..195b536
--- /dev/null
+++ b/dotnet-ai/mcp-server-csharp-dotnet/global.json
@@ -0,0 +1,10 @@
+{
+ "sdk": {
+ "version": "10.0.401",
+ "rollForward": "latestPatch",
+ "allowPrerelease": false
+ },
+ "test": {
+ "runner": "Microsoft.Testing.Platform"
+ }
+}
diff --git a/dotnet-ai/mcp-server-csharp-dotnet/scripts/verify.ps1 b/dotnet-ai/mcp-server-csharp-dotnet/scripts/verify.ps1
new file mode 100644
index 0000000..336e039
--- /dev/null
+++ b/dotnet-ai/mcp-server-csharp-dotnet/scripts/verify.ps1
@@ -0,0 +1,388 @@
+[CmdletBinding()]
+param()
+
+Set-StrictMode -Version Latest
+$ErrorActionPreference = 'Stop'
+$ProgressPreference = 'SilentlyContinue'
+
+$RequiredSdk = '10.0.401'
+$TargetFramework = 'net10.0'
+$McpVersion = '2.2.0'
+$HostingVersion = '10.0.12'
+$XunitVersion = '4.0.1'
+
+$Root = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)
+$SolutionPath = Join-Path $Root 'FirstMcpServer.slnx'
+$ServerProject = Join-Path $Root 'src\FirstMcpServer\FirstMcpServer.csproj'
+$ClientProject = Join-Path $Root 'src\McpClientDemo\McpClientDemo.csproj'
+$TestProject = Join-Path $Root 'tests\FirstMcpServer.Tests\FirstMcpServer.Tests.csproj'
+$ServerDll = Join-Path $Root 'src\FirstMcpServer\bin\Release\net10.0\FirstMcpServer.dll'
+
+$Results = Join-Path $Root 'verification-results'
+$PackageGraphFile = Join-Path $Root 'docs\package-graph.txt'
+$RawClientFile = Join-Path $Results 'mcp-client-output.txt'
+$RawTestFile = Join-Path $Results 'test-output.txt'
+$RawBuildFile = Join-Path $Results 'build-output.txt'
+$VerifiedMd = Join-Path $Root 'docs\verified-environment.md'
+$SampleRunsMd = Join-Path $Root 'docs\sample-runs.md'
+$VerifiedJson = Join-Path $Root 'verified-environment.json'
+
+function Write-Section([string]$Text) {
+ Write-Host ''
+ Write-Host ('=' * 78) -ForegroundColor DarkGray
+ Write-Host $Text -ForegroundColor Cyan
+ Write-Host ('=' * 78) -ForegroundColor DarkGray
+}
+
+function Write-Step([string]$Text) {
+ Write-Host "[STEP] $Text" -ForegroundColor Yellow
+}
+
+function Write-Pass([string]$Text) {
+ Write-Host "[PASS] $Text" -ForegroundColor Green
+}
+
+function Write-Fail([string]$Text) {
+ Write-Host "[FAIL] $Text" -ForegroundColor Red
+}
+
+function Write-Utf8NoBom([string]$Path, [string]$Content) {
+ $parent = Split-Path -Parent $Path
+ if ($parent -and -not (Test-Path $parent)) {
+ New-Item -ItemType Directory -Force -Path $parent | Out-Null
+ }
+ [System.IO.File]::WriteAllText($Path, $Content, [System.Text.UTF8Encoding]::new($false))
+}
+
+function Invoke-Native {
+ param(
+ [Parameter(Mandatory=$true)][string]$FilePath,
+ [Parameter(Mandatory=$false)][string[]]$Arguments = @(),
+ [Parameter(Mandatory=$false)][string]$WorkingDirectory = $Root,
+ [Parameter(Mandatory=$false)][switch]$Capture,
+ [Parameter(Mandatory=$false)][string]$CapturePath
+ )
+
+ Push-Location $WorkingDirectory
+ try {
+ if ($Capture) {
+ $output = & $FilePath @Arguments 2>&1
+ $exitCode = $LASTEXITCODE
+ $text = ($output | ForEach-Object { $_.ToString() }) -join [Environment]::NewLine
+
+ if ($CapturePath) {
+ Write-Utf8NoBom $CapturePath ($text + [Environment]::NewLine)
+ }
+
+ $output | ForEach-Object { Write-Host $_ }
+
+ if ($exitCode -ne 0) {
+ throw "Command failed with exit code ${exitCode}: $FilePath $($Arguments -join ' ')"
+ }
+
+ return $text
+ }
+
+ & $FilePath @Arguments
+ $exitCode = $LASTEXITCODE
+ if ($exitCode -ne 0) {
+ throw "Command failed with exit code ${exitCode}: $FilePath $($Arguments -join ' ')"
+ }
+ }
+ finally {
+ Pop-Location
+ }
+}
+
+Write-Section 'FIRST MCP SERVER - LOCAL VERIFICATION'
+Write-Host "Root: $Root"
+Write-Host "Required SDK: $RequiredSdk"
+Write-Host "Target framework: $TargetFramework"
+Write-Host "ModelContextProtocol: $McpVersion"
+Write-Host "Microsoft.Extensions.Hosting: $HostingVersion"
+Write-Host "xunit.v3.mtp-v2: $XunitVersion"
+Write-Host 'Transport: stdio'
+Write-Host 'LLM/API calls: NONE'
+
+try {
+ if (Test-Path $Results) {
+ Remove-Item -Recurse -Force $Results
+ }
+ New-Item -ItemType Directory -Force -Path $Results | Out-Null
+
+ Write-Step 'Check required .NET SDK'
+ $installedSdks = & dotnet --list-sdks 2>&1
+ if ($LASTEXITCODE -ne 0) {
+ throw 'dotnet --list-sdks failed.'
+ }
+
+ if (-not ($installedSdks | Where-Object { $_ -match "^$([regex]::Escape($RequiredSdk))\s" })) {
+ throw "Required .NET SDK $RequiredSdk is not installed."
+ }
+
+ $selectedSdk = Invoke-Native -FilePath 'dotnet' -Arguments @('--version') -Capture
+ if ($selectedSdk.Trim() -ne $RequiredSdk) {
+ throw "global.json selected '$($selectedSdk.Trim())' instead of '$RequiredSdk'."
+ }
+ Write-Pass ".NET SDK $RequiredSdk selected"
+
+ Write-Step 'Restore solution'
+ Invoke-Native -FilePath 'dotnet' -Arguments @('restore', $SolutionPath)
+ Write-Pass 'Restore PASS'
+
+ Write-Step 'Build solution in Release'
+ $buildText = Invoke-Native -FilePath 'dotnet' -Arguments @('build', $SolutionPath, '-c', 'Release', '--no-restore') -Capture -CapturePath $RawBuildFile
+ if (-not (Test-Path $ServerDll)) {
+ throw "Expected server DLL was not produced: $ServerDll"
+ }
+ Write-Pass 'Release build PASS'
+
+ Write-Step 'Run deterministic test project'
+ $testText = Invoke-Native -FilePath 'dotnet' -Arguments @('test', $TestProject, '-c', 'Release', '--no-build') -Capture -CapturePath $RawTestFile
+ Write-Pass 'Deterministic tests PASS'
+
+ $testTotal = $null
+ $testPassed = $null
+ $testFailed = $null
+ $testSkipped = $null
+
+ # .NET 10 + Microsoft Testing Platform emits:
+ # Test run summary: Passed!
+ # total: 13
+ # failed: 0
+ # succeeded: 13
+ # skipped: 0
+ # Parse the four summary lines independently so CRLF/wording changes in the
+ # heading do not erase otherwise valid counts.
+ $testLines = $testText -split '\r?\n'
+
+ foreach ($line in $testLines) {
+ if ($null -eq $testTotal -and $line -match '^\s*total:\s*(\d+)\s*$') {
+ $testTotal = [int]$Matches[1]
+ continue
+ }
+ if ($null -eq $testFailed -and $line -match '^\s*failed:\s*(\d+)\s*$') {
+ $testFailed = [int]$Matches[1]
+ continue
+ }
+ if ($null -eq $testPassed -and $line -match '^\s*succeeded:\s*(\d+)\s*$') {
+ $testPassed = [int]$Matches[1]
+ continue
+ }
+ if ($null -eq $testSkipped -and $line -match '^\s*skipped:\s*(\d+)\s*$') {
+ $testSkipped = [int]$Matches[1]
+ continue
+ }
+ }
+
+ if ($null -eq $testTotal -or
+ $null -eq $testFailed -or
+ $null -eq $testPassed -or
+ $null -eq $testSkipped) {
+ throw 'Deterministic tests passed, but the verifier could not parse the Microsoft Testing Platform summary counts.'
+ }
+
+ if ($testFailed -ne 0 -or ($testPassed + $testFailed + $testSkipped) -ne $testTotal) {
+ throw "Parsed test counts are inconsistent: total=$testTotal passed=$testPassed failed=$testFailed skipped=$testSkipped."
+ }
+
+ Write-Step 'Capture package graph'
+ Invoke-Native -FilePath 'dotnet' -Arguments @('list', $SolutionPath, 'package', '--include-transitive') -Capture -CapturePath $PackageGraphFile | Out-Null
+ Write-Pass 'Package graph captured'
+
+ Write-Step 'Run real MCP stdio client/server end-to-end verification'
+ $clientText = Invoke-Native -FilePath 'dotnet' -Arguments @(
+ 'run',
+ '--project', $ClientProject,
+ '-c', 'Release',
+ '--no-build',
+ '--',
+ $ServerDll
+ ) -Capture -CapturePath $RawClientFile
+
+ $requiredPassKeys = @(
+ 'SERVER_DLL',
+ 'TOOL_DISCOVERY',
+ 'AIFUNCTION_ASSIGNABILITY',
+ 'SERVICE_HEALTH_CALL',
+ 'DEPLOYMENT_CALL',
+ 'RUNBOOK_CALL',
+ 'UNKNOWN_TOOL_REJECTION',
+ 'STDIO_END_TO_END'
+ )
+
+ foreach ($key in $requiredPassKeys) {
+ if ($clientText -notmatch "(?m)^VERIFY\|$([regex]::Escape($key))\|PASS\|") {
+ throw "Expected PASS marker was not found for $key."
+ }
+ }
+
+ if ($clientText -notmatch '(?m)^FINAL\|PASS$') {
+ throw 'McpClientDemo did not emit FINAL|PASS.'
+ }
+
+ Write-Pass 'MCP stdio E2E PASS'
+
+ $clientLines = $clientText -split '\r?\n'
+
+ $runtimeTransportLine = $clientLines |
+ Where-Object { $_ -match '^RUNTIME\|TransportType\|(.+)$' } |
+ Select-Object -First 1
+ $runtimeClientLine = $clientLines |
+ Where-Object { $_ -match '^RUNTIME\|ClientType\|(.+)$' } |
+ Select-Object -First 1
+ $toolCountLine = $clientLines |
+ Where-Object { $_ -match '^RUNTIME\|ToolCount\|(\d+)$' } |
+ Select-Object -First 1
+
+ if (-not $runtimeTransportLine -or -not $runtimeClientLine -or -not $toolCountLine) {
+ throw 'MCP E2E passed, but runtime metadata lines could not be parsed.'
+ }
+
+ [void]($runtimeTransportLine -match '^RUNTIME\|TransportType\|(.+)$')
+ $runtimeTransport = $Matches[1].Trim()
+
+ [void]($runtimeClientLine -match '^RUNTIME\|ClientType\|(.+)$')
+ $runtimeClient = $Matches[1].Trim()
+
+ [void]($toolCountLine -match '^RUNTIME\|ToolCount\|(\d+)$')
+ $toolCount = [int]$Matches[1]
+
+ if ($toolCount -ne 3) {
+ throw "Expected exactly 3 discovered tools, but parsed $toolCount."
+ }
+
+ $evidenceLines = $clientLines |
+ Where-Object { $_ -match '^(RUNTIME|TOOL|VERIFY|FINAL)\|' }
+
+ $timestamp = (Get-Date).ToString('yyyy-MM-ddTHH:mm:ssK')
+ $dateOnly = (Get-Date).ToString('yyyy-MM-dd')
+
+ $testSummaryText = "$testPassed/$testTotal passed; $testFailed failed; $testSkipped skipped"
+
+ $verifiedMdContent = @"
+# Verified Environment
+
+- Verification timestamp: $timestamp
+- .NET SDK: $RequiredSdk
+- Target framework: $TargetFramework
+- ModelContextProtocol: $McpVersion
+- Microsoft.Extensions.Hosting: $HostingVersion
+- xunit.v3.mtp-v2: $XunitVersion
+- Transport: stdio
+- Release build: PASS
+- Deterministic tests: $testSummaryText
+- MCP stdio end-to-end: PASS
+- Runtime transport type: $runtimeTransport
+- Runtime client type: $runtimeClient
+- Discovered tool count: $toolCount
+
+## Verified behaviors
+
+- real MCP client/server handshake;
+- exactly three tool definitions discovered;
+- `get_service_health` invoked over MCP stdio;
+- `get_recent_deployment` invoked over MCP stdio;
+- `get_runbook` invoked over MCP stdio;
+- unknown tool rejected;
+- `McpClientTool` assignable to `AIFunction`;
+- no LLM or cloud API required.
+
+## Evidence boundary
+
+This verification does not establish HTTP transport behavior, authentication/OAuth, LLM-driven tool selection, Agent Framework integration, production readiness, or coverage of MCP features outside this sample.
+
+Raw local outputs are under `verification-results/` and are intentionally gitignored.
+"@
+ Write-Utf8NoBom $VerifiedMd $verifiedMdContent
+
+ $sampleRun = @"
+# Sample Runs
+
+Captured from the successful local MCP stdio verification on $dateOnly.
+
+```text
+$($evidenceLines -join [Environment]::NewLine)
+```
+
+Server diagnostic lines are omitted from this curated view. The raw local output remains in `verification-results/mcp-client-output.txt`.
+"@
+ Write-Utf8NoBom $SampleRunsMd $sampleRun
+
+ $jsonObject = [ordered]@{
+ status = 'PASS'
+ verificationTimestamp = $timestamp
+ sdk = $RequiredSdk
+ targetFramework = $TargetFramework
+ packages = [ordered]@{
+ ModelContextProtocol = $McpVersion
+ MicrosoftExtensionsHosting = $HostingVersion
+ xunitV3MtpV2 = $XunitVersion
+ }
+ transport = 'stdio'
+ tests = [ordered]@{
+ status = 'PASS'
+ total = $testTotal
+ passed = $testPassed
+ failed = $testFailed
+ skipped = $testSkipped
+ }
+ runtime = [ordered]@{
+ transportType = $runtimeTransport
+ clientType = $runtimeClient
+ discoveredToolCount = $toolCount
+ }
+ checks = [ordered]@{
+ restore = 'PASS'
+ releaseBuild = 'PASS'
+ deterministicTests = 'PASS'
+ toolDiscovery = 'PASS'
+ aiFunctionAssignability = 'PASS'
+ serviceHealthCall = 'PASS'
+ deploymentCall = 'PASS'
+ runbookCall = 'PASS'
+ unknownToolRejection = 'PASS'
+ stdioEndToEnd = 'PASS'
+ }
+ exclusions = @(
+ 'LLM-driven tool selection',
+ 'Agent Framework',
+ 'Streamable HTTP',
+ 'OAuth/authentication/authorization',
+ 'MCP resources/prompts/sampling/elicitation/tasks',
+ 'write-capable tools',
+ 'database/Redis',
+ 'cloud deployment',
+ 'production readiness'
+ )
+ }
+
+ Write-Utf8NoBom $VerifiedJson ($jsonObject | ConvertTo-Json -Depth 8)
+
+ Write-Section 'FINAL RESULT'
+ Write-Host 'FIRST MCP SERVER VERIFICATION: PASS' -ForegroundColor Green
+ Write-Host ''
+ Write-Host "Project: $Root"
+ Write-Host "Build: PASS"
+ Write-Host "Tests: $testSummaryText"
+ Write-Host "MCP stdio E2E: PASS"
+ Write-Host "Discovered tools: $toolCount"
+ Write-Host "Verified JSON: $VerifiedJson"
+ Write-Host "Verified Markdown: $VerifiedMd"
+ Write-Host "Sample run: $SampleRunsMd"
+ Write-Host "Package graph: $PackageGraphFile"
+ Write-Host ''
+ Write-Host 'Please send the complete console output back to ChatGPT.' -ForegroundColor Cyan
+ exit 0
+}
+catch {
+ Write-Section 'FINAL RESULT'
+ Write-Fail 'FIRST MCP SERVER VERIFICATION: FAIL'
+ Write-Host $_.Exception.Message -ForegroundColor Red
+ Write-Host ''
+ Write-Host "Project has been preserved for diagnosis: $Root"
+ Write-Host "Local raw outputs, if created: $Results"
+ Write-Host ''
+ Write-Host 'Please send the complete console output back to ChatGPT.' -ForegroundColor Cyan
+ exit 1
+}
diff --git a/dotnet-ai/mcp-server-csharp-dotnet/src/FirstMcpServer/Data/deployments.json b/dotnet-ai/mcp-server-csharp-dotnet/src/FirstMcpServer/Data/deployments.json
new file mode 100644
index 0000000..89cd32b
--- /dev/null
+++ b/dotnet-ai/mcp-server-csharp-dotnet/src/FirstMcpServer/Data/deployments.json
@@ -0,0 +1,22 @@
+[
+ {
+ "evidenceId": "E-201",
+ "service": "checkout-api",
+ "deployment": "deploy-1842",
+ "deploymentStatus": "succeeded",
+ "completedAt": "2026-09-24T14:02:00Z",
+ "previousDeployment": "deploy-1839",
+ "changeSummary": "pricing-rule refresh and structured logging update",
+ "rollbackPerformed": false
+ },
+ {
+ "evidenceId": "E-202",
+ "service": "catalog-api",
+ "deployment": "deploy-3104",
+ "deploymentStatus": "succeeded",
+ "completedAt": "2026-09-24T13:45:00Z",
+ "previousDeployment": "deploy-3099",
+ "changeSummary": "catalog metadata refresh",
+ "rollbackPerformed": false
+ }
+]
diff --git a/dotnet-ai/mcp-server-csharp-dotnet/src/FirstMcpServer/Data/runbooks.json b/dotnet-ai/mcp-server-csharp-dotnet/src/FirstMcpServer/Data/runbooks.json
new file mode 100644
index 0000000..b33bb3b
--- /dev/null
+++ b/dotnet-ai/mcp-server-csharp-dotnet/src/FirstMcpServer/Data/runbooks.json
@@ -0,0 +1,26 @@
+[
+ {
+ "runbookId": "RB-CHECKOUT-03",
+ "service": "checkout-api",
+ "title": "Checkout API degraded-service investigation",
+ "guidance": [
+ "Compare error-rate onset with recent deployments.",
+ "Inspect database connection-pool utilization.",
+ "Check connection counts before and after deployment.",
+ "Review traces for timeout or connection-acquisition failures.",
+ "Compare configuration with the previous known-good release."
+ ],
+ "caution": "Runbook guidance suggests investigation steps; it does not establish root cause."
+ },
+ {
+ "runbookId": "RB-CATALOG-02",
+ "service": "catalog-api",
+ "title": "Catalog API health investigation",
+ "guidance": [
+ "Confirm upstream dependency health.",
+ "Review recent deployment changes.",
+ "Inspect request latency and error-rate trends."
+ ],
+ "caution": "Runbook guidance is operational guidance, not incident evidence."
+ }
+]
diff --git a/dotnet-ai/mcp-server-csharp-dotnet/src/FirstMcpServer/Data/services.json b/dotnet-ai/mcp-server-csharp-dotnet/src/FirstMcpServer/Data/services.json
new file mode 100644
index 0000000..2ca9712
--- /dev/null
+++ b/dotnet-ai/mcp-server-csharp-dotnet/src/FirstMcpServer/Data/services.json
@@ -0,0 +1,20 @@
+[
+ {
+ "evidenceId": "E-101",
+ "service": "checkout-api",
+ "observedAt": "2026-09-24T14:12:00Z",
+ "status": "degraded",
+ "errorRate": 18.7,
+ "errorSpikeStarted": "2026-09-24T14:09:00Z",
+ "databasePoolUtilization": 96.0
+ },
+ {
+ "evidenceId": "E-102",
+ "service": "catalog-api",
+ "observedAt": "2026-09-24T14:12:00Z",
+ "status": "healthy",
+ "errorRate": 0.2,
+ "errorSpikeStarted": "",
+ "databasePoolUtilization": 41.0
+ }
+]
diff --git a/dotnet-ai/mcp-server-csharp-dotnet/src/FirstMcpServer/FirstMcpServer.csproj b/dotnet-ai/mcp-server-csharp-dotnet/src/FirstMcpServer/FirstMcpServer.csproj
new file mode 100644
index 0000000..067f056
--- /dev/null
+++ b/dotnet-ai/mcp-server-csharp-dotnet/src/FirstMcpServer/FirstMcpServer.csproj
@@ -0,0 +1,21 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+
+
diff --git a/dotnet-ai/mcp-server-csharp-dotnet/src/FirstMcpServer/Models/DeploymentRecord.cs b/dotnet-ai/mcp-server-csharp-dotnet/src/FirstMcpServer/Models/DeploymentRecord.cs
new file mode 100644
index 0000000..d070253
--- /dev/null
+++ b/dotnet-ai/mcp-server-csharp-dotnet/src/FirstMcpServer/Models/DeploymentRecord.cs
@@ -0,0 +1,13 @@
+namespace FirstMcpServer.Models;
+
+public sealed record DeploymentRecord
+{
+ public string EvidenceId { get; init; } = string.Empty;
+ public string Service { get; init; } = string.Empty;
+ public string Deployment { get; init; } = string.Empty;
+ public string DeploymentStatus { get; init; } = string.Empty;
+ public string CompletedAt { get; init; } = string.Empty;
+ public string PreviousDeployment { get; init; } = string.Empty;
+ public string ChangeSummary { get; init; } = string.Empty;
+ public bool RollbackPerformed { get; init; }
+}
diff --git a/dotnet-ai/mcp-server-csharp-dotnet/src/FirstMcpServer/Models/RunbookRecord.cs b/dotnet-ai/mcp-server-csharp-dotnet/src/FirstMcpServer/Models/RunbookRecord.cs
new file mode 100644
index 0000000..263bf0a
--- /dev/null
+++ b/dotnet-ai/mcp-server-csharp-dotnet/src/FirstMcpServer/Models/RunbookRecord.cs
@@ -0,0 +1,10 @@
+namespace FirstMcpServer.Models;
+
+public sealed record RunbookRecord
+{
+ public string RunbookId { get; init; } = string.Empty;
+ public string Service { get; init; } = string.Empty;
+ public string Title { get; init; } = string.Empty;
+ public IReadOnlyList Guidance { get; init; } = [];
+ public string Caution { get; init; } = string.Empty;
+}
diff --git a/dotnet-ai/mcp-server-csharp-dotnet/src/FirstMcpServer/Models/ServiceHealthRecord.cs b/dotnet-ai/mcp-server-csharp-dotnet/src/FirstMcpServer/Models/ServiceHealthRecord.cs
new file mode 100644
index 0000000..b0ac035
--- /dev/null
+++ b/dotnet-ai/mcp-server-csharp-dotnet/src/FirstMcpServer/Models/ServiceHealthRecord.cs
@@ -0,0 +1,12 @@
+namespace FirstMcpServer.Models;
+
+public sealed record ServiceHealthRecord
+{
+ public string EvidenceId { get; init; } = string.Empty;
+ public string Service { get; init; } = string.Empty;
+ public string ObservedAt { get; init; } = string.Empty;
+ public string Status { get; init; } = string.Empty;
+ public double ErrorRate { get; init; }
+ public string ErrorSpikeStarted { get; init; } = string.Empty;
+ public double DatabasePoolUtilization { get; init; }
+}
diff --git a/dotnet-ai/mcp-server-csharp-dotnet/src/FirstMcpServer/Program.cs b/dotnet-ai/mcp-server-csharp-dotnet/src/FirstMcpServer/Program.cs
new file mode 100644
index 0000000..afbfd69
--- /dev/null
+++ b/dotnet-ai/mcp-server-csharp-dotnet/src/FirstMcpServer/Program.cs
@@ -0,0 +1,24 @@
+using FirstMcpServer.Stores;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+var builder = Host.CreateApplicationBuilder(args);
+
+// stdio is the MCP protocol channel. Keep ordinary diagnostics off stdout.
+builder.Logging.ClearProviders();
+builder.Logging.AddConsole(options =>
+{
+ options.LogToStandardErrorThreshold = LogLevel.Trace;
+});
+
+builder.Services.AddSingleton();
+builder.Services.AddSingleton();
+builder.Services.AddSingleton();
+
+builder.Services
+ .AddMcpServer()
+ .WithStdioServerTransport()
+ .WithToolsFromAssembly();
+
+await builder.Build().RunAsync();
diff --git a/dotnet-ai/mcp-server-csharp-dotnet/src/FirstMcpServer/Stores/DeploymentStore.cs b/dotnet-ai/mcp-server-csharp-dotnet/src/FirstMcpServer/Stores/DeploymentStore.cs
new file mode 100644
index 0000000..869d077
--- /dev/null
+++ b/dotnet-ai/mcp-server-csharp-dotnet/src/FirstMcpServer/Stores/DeploymentStore.cs
@@ -0,0 +1,34 @@
+using FirstMcpServer.Models;
+
+namespace FirstMcpServer.Stores;
+
+public sealed class DeploymentStore
+{
+ private readonly IReadOnlyList _records;
+
+ public DeploymentStore()
+ : this(JsonStoreLoader.Load("deployments.json"))
+ {
+ }
+
+ private DeploymentStore(IReadOnlyList records)
+ {
+ _records = records;
+ }
+
+ public static DeploymentStore FromRecords(IEnumerable records)
+ => new(records.ToArray());
+
+ public DeploymentRecord? GetRecent(string serviceName)
+ {
+ string normalized = Normalize(serviceName);
+
+ return _records
+ .Where(x => string.Equals(x.Service, normalized, StringComparison.OrdinalIgnoreCase))
+ .OrderByDescending(x => x.CompletedAt, StringComparer.Ordinal)
+ .FirstOrDefault();
+ }
+
+ private static string Normalize(string value)
+ => value?.Trim() ?? string.Empty;
+}
diff --git a/dotnet-ai/mcp-server-csharp-dotnet/src/FirstMcpServer/Stores/JsonStoreLoader.cs b/dotnet-ai/mcp-server-csharp-dotnet/src/FirstMcpServer/Stores/JsonStoreLoader.cs
new file mode 100644
index 0000000..2e6734f
--- /dev/null
+++ b/dotnet-ai/mcp-server-csharp-dotnet/src/FirstMcpServer/Stores/JsonStoreLoader.cs
@@ -0,0 +1,26 @@
+using System.Text.Json;
+
+namespace FirstMcpServer.Stores;
+
+internal static class JsonStoreLoader
+{
+ internal static readonly JsonSerializerOptions Options = new()
+ {
+ PropertyNameCaseInsensitive = true,
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ WriteIndented = false
+ };
+
+ public static IReadOnlyList Load(string fileName)
+ {
+ string path = Path.Combine(AppContext.BaseDirectory, "Data", fileName);
+
+ if (!File.Exists(path))
+ {
+ throw new FileNotFoundException($"Required deterministic data file was not found: {path}", path);
+ }
+
+ using FileStream stream = File.OpenRead(path);
+ return JsonSerializer.Deserialize>(stream, Options) ?? [];
+ }
+}
diff --git a/dotnet-ai/mcp-server-csharp-dotnet/src/FirstMcpServer/Stores/RunbookStore.cs b/dotnet-ai/mcp-server-csharp-dotnet/src/FirstMcpServer/Stores/RunbookStore.cs
new file mode 100644
index 0000000..3d81bd6
--- /dev/null
+++ b/dotnet-ai/mcp-server-csharp-dotnet/src/FirstMcpServer/Stores/RunbookStore.cs
@@ -0,0 +1,32 @@
+using FirstMcpServer.Models;
+
+namespace FirstMcpServer.Stores;
+
+public sealed class RunbookStore
+{
+ private readonly IReadOnlyList _records;
+
+ public RunbookStore()
+ : this(JsonStoreLoader.Load("runbooks.json"))
+ {
+ }
+
+ private RunbookStore(IReadOnlyList records)
+ {
+ _records = records;
+ }
+
+ public static RunbookStore FromRecords(IEnumerable records)
+ => new(records.ToArray());
+
+ public RunbookRecord? Get(string serviceName)
+ {
+ string normalized = Normalize(serviceName);
+
+ return _records.FirstOrDefault(
+ x => string.Equals(x.Service, normalized, StringComparison.OrdinalIgnoreCase));
+ }
+
+ private static string Normalize(string value)
+ => value?.Trim() ?? string.Empty;
+}
diff --git a/dotnet-ai/mcp-server-csharp-dotnet/src/FirstMcpServer/Stores/ServiceHealthStore.cs b/dotnet-ai/mcp-server-csharp-dotnet/src/FirstMcpServer/Stores/ServiceHealthStore.cs
new file mode 100644
index 0000000..8a692b5
--- /dev/null
+++ b/dotnet-ai/mcp-server-csharp-dotnet/src/FirstMcpServer/Stores/ServiceHealthStore.cs
@@ -0,0 +1,34 @@
+using FirstMcpServer.Models;
+
+namespace FirstMcpServer.Stores;
+
+public sealed class ServiceHealthStore
+{
+ private readonly IReadOnlyList _records;
+
+ public ServiceHealthStore()
+ : this(JsonStoreLoader.Load("services.json"))
+ {
+ }
+
+ private ServiceHealthStore(IReadOnlyList records)
+ {
+ _records = records;
+ }
+
+ public static ServiceHealthStore FromRecords(IEnumerable records)
+ => new(records.ToArray());
+
+ public ServiceHealthRecord? Get(string serviceName)
+ {
+ string normalized = Normalize(serviceName);
+
+ return _records
+ .Where(x => string.Equals(x.Service, normalized, StringComparison.OrdinalIgnoreCase))
+ .OrderByDescending(x => x.ObservedAt, StringComparer.Ordinal)
+ .FirstOrDefault();
+ }
+
+ private static string Normalize(string value)
+ => value?.Trim() ?? string.Empty;
+}
diff --git a/dotnet-ai/mcp-server-csharp-dotnet/src/FirstMcpServer/Tools/DeploymentTools.cs b/dotnet-ai/mcp-server-csharp-dotnet/src/FirstMcpServer/Tools/DeploymentTools.cs
new file mode 100644
index 0000000..cdebcd3
--- /dev/null
+++ b/dotnet-ai/mcp-server-csharp-dotnet/src/FirstMcpServer/Tools/DeploymentTools.cs
@@ -0,0 +1,28 @@
+using FirstMcpServer.Stores;
+using ModelContextProtocol.Server;
+using System.ComponentModel;
+
+namespace FirstMcpServer.Tools;
+
+[McpServerToolType]
+public static class DeploymentTools
+{
+ [McpServerTool(
+ Name = "get_recent_deployment",
+ ReadOnly = true,
+ Destructive = false,
+ Idempotent = true,
+ OpenWorld = false)]
+ [Description("Returns deterministic synthetic evidence for the most recent known deployment of a service.")]
+ public static string GetRecentDeployment(
+ DeploymentStore store,
+ [Description("Service name, for example checkout-api.")] string serviceName)
+ {
+ string normalized = serviceName?.Trim() ?? string.Empty;
+ var record = store.GetRecent(normalized);
+
+ return record is null
+ ? ToolJson.Serialize(new { found = false, service = normalized })
+ : ToolJson.Serialize(record);
+ }
+}
diff --git a/dotnet-ai/mcp-server-csharp-dotnet/src/FirstMcpServer/Tools/RunbookTools.cs b/dotnet-ai/mcp-server-csharp-dotnet/src/FirstMcpServer/Tools/RunbookTools.cs
new file mode 100644
index 0000000..f89896c
--- /dev/null
+++ b/dotnet-ai/mcp-server-csharp-dotnet/src/FirstMcpServer/Tools/RunbookTools.cs
@@ -0,0 +1,28 @@
+using FirstMcpServer.Stores;
+using ModelContextProtocol.Server;
+using System.ComponentModel;
+
+namespace FirstMcpServer.Tools;
+
+[McpServerToolType]
+public static class RunbookTools
+{
+ [McpServerTool(
+ Name = "get_runbook",
+ ReadOnly = true,
+ Destructive = false,
+ Idempotent = true,
+ OpenWorld = false)]
+ [Description("Returns deterministic runbook guidance for a known service. Guidance is not incident evidence.")]
+ public static string GetRunbook(
+ RunbookStore store,
+ [Description("Service name, for example checkout-api.")] string serviceName)
+ {
+ string normalized = serviceName?.Trim() ?? string.Empty;
+ var record = store.Get(normalized);
+
+ return record is null
+ ? ToolJson.Serialize(new { found = false, service = normalized })
+ : ToolJson.Serialize(record);
+ }
+}
diff --git a/dotnet-ai/mcp-server-csharp-dotnet/src/FirstMcpServer/Tools/ServiceHealthTools.cs b/dotnet-ai/mcp-server-csharp-dotnet/src/FirstMcpServer/Tools/ServiceHealthTools.cs
new file mode 100644
index 0000000..3e73348
--- /dev/null
+++ b/dotnet-ai/mcp-server-csharp-dotnet/src/FirstMcpServer/Tools/ServiceHealthTools.cs
@@ -0,0 +1,28 @@
+using FirstMcpServer.Stores;
+using ModelContextProtocol.Server;
+using System.ComponentModel;
+
+namespace FirstMcpServer.Tools;
+
+[McpServerToolType]
+public static class ServiceHealthTools
+{
+ [McpServerTool(
+ Name = "get_service_health",
+ ReadOnly = true,
+ Destructive = false,
+ Idempotent = true,
+ OpenWorld = false)]
+ [Description("Returns deterministic synthetic service-health evidence for a known service.")]
+ public static string GetServiceHealth(
+ ServiceHealthStore store,
+ [Description("Service name, for example checkout-api.")] string serviceName)
+ {
+ string normalized = serviceName?.Trim() ?? string.Empty;
+ var record = store.Get(normalized);
+
+ return record is null
+ ? ToolJson.Serialize(new { found = false, service = normalized })
+ : ToolJson.Serialize(record);
+ }
+}
diff --git a/dotnet-ai/mcp-server-csharp-dotnet/src/FirstMcpServer/Tools/ToolJson.cs b/dotnet-ai/mcp-server-csharp-dotnet/src/FirstMcpServer/Tools/ToolJson.cs
new file mode 100644
index 0000000..27ea76d
--- /dev/null
+++ b/dotnet-ai/mcp-server-csharp-dotnet/src/FirstMcpServer/Tools/ToolJson.cs
@@ -0,0 +1,15 @@
+using System.Text.Json;
+
+namespace FirstMcpServer.Tools;
+
+internal static class ToolJson
+{
+ internal static readonly JsonSerializerOptions Options = new()
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ WriteIndented = false
+ };
+
+ public static string Serialize(T value)
+ => JsonSerializer.Serialize(value, Options);
+}
diff --git a/dotnet-ai/mcp-server-csharp-dotnet/src/McpClientDemo/McpClientDemo.csproj b/dotnet-ai/mcp-server-csharp-dotnet/src/McpClientDemo/McpClientDemo.csproj
new file mode 100644
index 0000000..b085608
--- /dev/null
+++ b/dotnet-ai/mcp-server-csharp-dotnet/src/McpClientDemo/McpClientDemo.csproj
@@ -0,0 +1,14 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/dotnet-ai/mcp-server-csharp-dotnet/src/McpClientDemo/Program.cs b/dotnet-ai/mcp-server-csharp-dotnet/src/McpClientDemo/Program.cs
new file mode 100644
index 0000000..dfd7ada
--- /dev/null
+++ b/dotnet-ai/mcp-server-csharp-dotnet/src/McpClientDemo/Program.cs
@@ -0,0 +1,143 @@
+using Microsoft.Extensions.AI;
+using ModelContextProtocol.Client;
+using ModelContextProtocol.Protocol;
+
+static void Check(bool condition, string key, string detail)
+{
+ if (!condition)
+ {
+ Console.WriteLine($"VERIFY|{key}|FAIL|{detail}");
+ throw new InvalidOperationException($"Verification failed: {key}: {detail}");
+ }
+
+ Console.WriteLine($"VERIFY|{key}|PASS|{detail}");
+}
+
+static string TextOf(CallToolResult result)
+ => result.Content.OfType().FirstOrDefault()?.Text ?? string.Empty;
+
+if (args.Length != 1)
+{
+ Console.Error.WriteLine("Usage: McpClientDemo ");
+ return 2;
+}
+
+string serverDll = Path.GetFullPath(args[0]);
+Check(File.Exists(serverDll), "SERVER_DLL", serverDll);
+
+var safeEnvironment = StdioClientTransportOptions.GetDefaultEnvironmentVariables();
+
+var transport = new StdioClientTransport(new StdioClientTransportOptions
+{
+ Name = "DOTNET-GUIDE-First-MCP-Server",
+ Command = "dotnet",
+ Arguments = [serverDll],
+ WorkingDirectory = Path.GetDirectoryName(serverDll),
+ InheritEnvironmentVariables = false,
+ EnvironmentVariables = safeEnvironment,
+ ShutdownTimeout = TimeSpan.FromSeconds(10),
+ StandardErrorLines = line => Console.WriteLine($"SERVER_STDERR|{line}")
+});
+
+Console.WriteLine($"RUNTIME|TransportType|{transport.GetType().FullName}");
+
+await using McpClient client = await McpClient.CreateAsync(transport);
+Console.WriteLine($"RUNTIME|ClientType|{client.GetType().FullName}");
+
+IList tools = await client.ListToolsAsync();
+Console.WriteLine($"RUNTIME|ToolCount|{tools.Count}");
+
+foreach (var tool in tools.OrderBy(t => t.Name, StringComparer.Ordinal))
+{
+ Console.WriteLine($"TOOL|{tool.Name}|{tool.GetType().FullName}|{tool.Description}");
+}
+
+string[] expectedTools =
+[
+ "get_service_health",
+ "get_recent_deployment",
+ "get_runbook"
+];
+
+string[] discovered = tools.Select(t => t.Name).OrderBy(n => n, StringComparer.Ordinal).ToArray();
+
+Check(
+ tools.Count == expectedTools.Length &&
+ expectedTools.All(name => tools.Any(t => string.Equals(t.Name, name, StringComparison.Ordinal))),
+ "TOOL_DISCOVERY",
+ string.Join(",", discovered));
+
+Check(
+ typeof(AIFunction).IsAssignableFrom(typeof(McpClientTool)),
+ "AIFUNCTION_ASSIGNABILITY",
+ "McpClientTool derives from AIFunction");
+
+CallToolResult healthResult = await client.CallToolAsync(
+ "get_service_health",
+ new Dictionary { ["serviceName"] = "checkout-api" },
+ cancellationToken: CancellationToken.None);
+string healthText = TextOf(healthResult);
+Check(
+ healthResult.IsError is not true &&
+ healthText.Contains("\"evidenceId\":\"E-101\"", StringComparison.Ordinal) &&
+ healthText.Contains("\"status\":\"degraded\"", StringComparison.Ordinal) &&
+ healthText.Contains("\"errorRate\":18.7", StringComparison.Ordinal),
+ "SERVICE_HEALTH_CALL",
+ healthText);
+
+CallToolResult deploymentResult = await client.CallToolAsync(
+ "get_recent_deployment",
+ new Dictionary { ["serviceName"] = "checkout-api" },
+ cancellationToken: CancellationToken.None);
+string deploymentText = TextOf(deploymentResult);
+Check(
+ deploymentResult.IsError is not true &&
+ deploymentText.Contains("\"evidenceId\":\"E-201\"", StringComparison.Ordinal) &&
+ deploymentText.Contains("\"deployment\":\"deploy-1842\"", StringComparison.Ordinal) &&
+ deploymentText.Contains("\"previousDeployment\":\"deploy-1839\"", StringComparison.Ordinal),
+ "DEPLOYMENT_CALL",
+ deploymentText);
+
+CallToolResult runbookResult = await client.CallToolAsync(
+ "get_runbook",
+ new Dictionary { ["serviceName"] = "checkout-api" },
+ cancellationToken: CancellationToken.None);
+string runbookText = TextOf(runbookResult);
+Check(
+ runbookResult.IsError is not true &&
+ runbookText.Contains("\"runbookId\":\"RB-CHECKOUT-03\"", StringComparison.Ordinal) &&
+ runbookText.Contains("does not establish root cause", StringComparison.Ordinal),
+ "RUNBOOK_CALL",
+ runbookText);
+
+bool unknownRejected = false;
+string unknownDetail = string.Empty;
+
+try
+{
+ CallToolResult unknown = await client.CallToolAsync(
+ "does_not_exist",
+ new Dictionary(),
+ cancellationToken: CancellationToken.None);
+
+ if (unknown.IsError is true)
+ {
+ unknownRejected = true;
+ unknownDetail = TextOf(unknown);
+ }
+ else
+ {
+ unknownDetail = "Unknown tool unexpectedly returned a non-error result.";
+ }
+}
+catch (Exception ex)
+{
+ unknownRejected = true;
+ unknownDetail = $"{ex.GetType().Name}: {ex.Message}";
+}
+
+Check(unknownRejected, "UNKNOWN_TOOL_REJECTION", unknownDetail);
+
+Console.WriteLine("VERIFY|STDIO_END_TO_END|PASS|Client launched server, discovered exactly three tools, and invoked all three over MCP stdio");
+Console.WriteLine("FINAL|PASS");
+return 0;
diff --git a/dotnet-ai/mcp-server-csharp-dotnet/tests/FirstMcpServer.Tests/DeploymentStoreTests.cs b/dotnet-ai/mcp-server-csharp-dotnet/tests/FirstMcpServer.Tests/DeploymentStoreTests.cs
new file mode 100644
index 0000000..351d118
--- /dev/null
+++ b/dotnet-ai/mcp-server-csharp-dotnet/tests/FirstMcpServer.Tests/DeploymentStoreTests.cs
@@ -0,0 +1,49 @@
+using FirstMcpServer.Models;
+using FirstMcpServer.Stores;
+
+namespace FirstMcpServer.Tests;
+
+public sealed class DeploymentStoreTests
+{
+ private static DeploymentStore CreateStore()
+ => DeploymentStore.FromRecords(
+ [
+ new DeploymentRecord
+ {
+ EvidenceId = "E-201",
+ Service = "checkout-api",
+ Deployment = "deploy-1842",
+ DeploymentStatus = "succeeded",
+ CompletedAt = "2026-09-24T14:02:00Z",
+ PreviousDeployment = "deploy-1839",
+ ChangeSummary = "pricing-rule refresh and structured logging update",
+ RollbackPerformed = false
+ }
+ ]);
+
+ [Fact]
+ public void GetRecent_ReturnsKnownDeployment()
+ {
+ var record = CreateStore().GetRecent("checkout-api");
+
+ Assert.NotNull(record);
+ Assert.Equal("E-201", record.EvidenceId);
+ Assert.Equal("deploy-1842", record.Deployment);
+ Assert.Equal("deploy-1839", record.PreviousDeployment);
+ }
+
+ [Fact]
+ public void GetRecent_NormalizesWhitespaceAndCase()
+ {
+ var record = CreateStore().GetRecent(" CHECKOUT-API ");
+
+ Assert.NotNull(record);
+ Assert.Equal("checkout-api", record.Service);
+ }
+
+ [Fact]
+ public void GetRecent_ReturnsNullForUnknownService()
+ {
+ Assert.Null(CreateStore().GetRecent("missing-api"));
+ }
+}
diff --git a/dotnet-ai/mcp-server-csharp-dotnet/tests/FirstMcpServer.Tests/FirstMcpServer.Tests.csproj b/dotnet-ai/mcp-server-csharp-dotnet/tests/FirstMcpServer.Tests/FirstMcpServer.Tests.csproj
new file mode 100644
index 0000000..7c0bb54
--- /dev/null
+++ b/dotnet-ai/mcp-server-csharp-dotnet/tests/FirstMcpServer.Tests/FirstMcpServer.Tests.csproj
@@ -0,0 +1,25 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+ true
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet-ai/mcp-server-csharp-dotnet/tests/FirstMcpServer.Tests/RunbookStoreTests.cs b/dotnet-ai/mcp-server-csharp-dotnet/tests/FirstMcpServer.Tests/RunbookStoreTests.cs
new file mode 100644
index 0000000..8c5e0c5
--- /dev/null
+++ b/dotnet-ai/mcp-server-csharp-dotnet/tests/FirstMcpServer.Tests/RunbookStoreTests.cs
@@ -0,0 +1,44 @@
+using FirstMcpServer.Models;
+using FirstMcpServer.Stores;
+
+namespace FirstMcpServer.Tests;
+
+public sealed class RunbookStoreTests
+{
+ private static RunbookStore CreateStore()
+ => RunbookStore.FromRecords(
+ [
+ new RunbookRecord
+ {
+ RunbookId = "RB-CHECKOUT-03",
+ Service = "checkout-api",
+ Title = "Checkout API degraded-service investigation",
+ Guidance = ["Inspect database connection-pool utilization."],
+ Caution = "Guidance does not establish root cause."
+ }
+ ]);
+
+ [Fact]
+ public void Get_ReturnsKnownRunbook()
+ {
+ var record = CreateStore().Get("checkout-api");
+
+ Assert.NotNull(record);
+ Assert.Equal("RB-CHECKOUT-03", record.RunbookId);
+ }
+
+ [Fact]
+ public void Get_NormalizesWhitespaceAndCase()
+ {
+ var record = CreateStore().Get(" CHECKOUT-API ");
+
+ Assert.NotNull(record);
+ Assert.Equal("checkout-api", record.Service);
+ }
+
+ [Fact]
+ public void Get_ReturnsNullForUnknownService()
+ {
+ Assert.Null(CreateStore().Get("missing-api"));
+ }
+}
diff --git a/dotnet-ai/mcp-server-csharp-dotnet/tests/FirstMcpServer.Tests/ServiceHealthStoreTests.cs b/dotnet-ai/mcp-server-csharp-dotnet/tests/FirstMcpServer.Tests/ServiceHealthStoreTests.cs
new file mode 100644
index 0000000..4f99098
--- /dev/null
+++ b/dotnet-ai/mcp-server-csharp-dotnet/tests/FirstMcpServer.Tests/ServiceHealthStoreTests.cs
@@ -0,0 +1,47 @@
+using FirstMcpServer.Models;
+using FirstMcpServer.Stores;
+
+namespace FirstMcpServer.Tests;
+
+public sealed class ServiceHealthStoreTests
+{
+ private static ServiceHealthStore CreateStore()
+ => ServiceHealthStore.FromRecords(
+ [
+ new ServiceHealthRecord
+ {
+ EvidenceId = "E-101",
+ Service = "checkout-api",
+ ObservedAt = "2026-09-24T14:12:00Z",
+ Status = "degraded",
+ ErrorRate = 18.7,
+ ErrorSpikeStarted = "2026-09-24T14:09:00Z",
+ DatabasePoolUtilization = 96
+ }
+ ]);
+
+ [Fact]
+ public void Get_ReturnsKnownService()
+ {
+ var record = CreateStore().Get("checkout-api");
+
+ Assert.NotNull(record);
+ Assert.Equal("E-101", record.EvidenceId);
+ Assert.Equal("degraded", record.Status);
+ }
+
+ [Fact]
+ public void Get_NormalizesWhitespaceAndCase()
+ {
+ var record = CreateStore().Get(" CHECKOUT-API ");
+
+ Assert.NotNull(record);
+ Assert.Equal("checkout-api", record.Service);
+ }
+
+ [Fact]
+ public void Get_ReturnsNullForUnknownService()
+ {
+ Assert.Null(CreateStore().Get("missing-api"));
+ }
+}
diff --git a/dotnet-ai/mcp-server-csharp-dotnet/tests/FirstMcpServer.Tests/ToolTests.cs b/dotnet-ai/mcp-server-csharp-dotnet/tests/FirstMcpServer.Tests/ToolTests.cs
new file mode 100644
index 0000000..6cedb21
--- /dev/null
+++ b/dotnet-ai/mcp-server-csharp-dotnet/tests/FirstMcpServer.Tests/ToolTests.cs
@@ -0,0 +1,87 @@
+using FirstMcpServer.Models;
+using FirstMcpServer.Stores;
+using FirstMcpServer.Tools;
+
+namespace FirstMcpServer.Tests;
+
+public sealed class ToolTests
+{
+ [Fact]
+ public void ServiceHealthTool_ReturnsEvidence()
+ {
+ var store = ServiceHealthStore.FromRecords(
+ [
+ new ServiceHealthRecord
+ {
+ EvidenceId = "E-101",
+ Service = "checkout-api",
+ ObservedAt = "2026-09-24T14:12:00Z",
+ Status = "degraded",
+ ErrorRate = 18.7,
+ ErrorSpikeStarted = "2026-09-24T14:09:00Z",
+ DatabasePoolUtilization = 96
+ }
+ ]);
+
+ string result = ServiceHealthTools.GetServiceHealth(store, "checkout-api");
+
+ Assert.Contains("\"evidenceId\":\"E-101\"", result);
+ Assert.Contains("\"status\":\"degraded\"", result);
+ }
+
+ [Fact]
+ public void DeploymentTool_ReturnsEvidence()
+ {
+ var store = DeploymentStore.FromRecords(
+ [
+ new DeploymentRecord
+ {
+ EvidenceId = "E-201",
+ Service = "checkout-api",
+ Deployment = "deploy-1842",
+ DeploymentStatus = "succeeded",
+ CompletedAt = "2026-09-24T14:02:00Z",
+ PreviousDeployment = "deploy-1839",
+ ChangeSummary = "pricing-rule refresh",
+ RollbackPerformed = false
+ }
+ ]);
+
+ string result = DeploymentTools.GetRecentDeployment(store, "checkout-api");
+
+ Assert.Contains("\"evidenceId\":\"E-201\"", result);
+ Assert.Contains("\"previousDeployment\":\"deploy-1839\"", result);
+ }
+
+ [Fact]
+ public void RunbookTool_ReturnsGuidance()
+ {
+ var store = RunbookStore.FromRecords(
+ [
+ new RunbookRecord
+ {
+ RunbookId = "RB-CHECKOUT-03",
+ Service = "checkout-api",
+ Title = "Checkout API degraded-service investigation",
+ Guidance = ["Inspect database connection-pool utilization."],
+ Caution = "Guidance does not establish root cause."
+ }
+ ]);
+
+ string result = RunbookTools.GetRunbook(store, "checkout-api");
+
+ Assert.Contains("\"runbookId\":\"RB-CHECKOUT-03\"", result);
+ Assert.Contains("does not establish root cause", result);
+ }
+
+ [Fact]
+ public void Tool_ReturnsFoundFalseForUnknownService()
+ {
+ var store = ServiceHealthStore.FromRecords([]);
+
+ string result = ServiceHealthTools.GetServiceHealth(store, "missing-api");
+
+ Assert.Contains("\"found\":false", result);
+ Assert.Contains("\"service\":\"missing-api\"", result);
+ }
+}
diff --git a/dotnet-ai/mcp-server-csharp-dotnet/verified-environment.json b/dotnet-ai/mcp-server-csharp-dotnet/verified-environment.json
new file mode 100644
index 0000000..7ffeb1c
--- /dev/null
+++ b/dotnet-ai/mcp-server-csharp-dotnet/verified-environment.json
@@ -0,0 +1,47 @@
+{
+ "status": "PASS",
+ "verificationTimestamp": "2026-10-05T21:36:33+05:30",
+ "sdk": "10.0.401",
+ "targetFramework": "net10.0",
+ "packages": {
+ "ModelContextProtocol": "2.2.0",
+ "MicrosoftExtensionsHosting": "10.0.12",
+ "xunitV3MtpV2": "4.0.1"
+ },
+ "transport": "stdio",
+ "tests": {
+ "status": "PASS",
+ "total": 13,
+ "passed": 13,
+ "failed": 0,
+ "skipped": 0
+ },
+ "runtime": {
+ "transportType": "ModelContextProtocol.Client.StdioClientTransport",
+ "clientType": "ModelContextProtocol.Client.McpClientImpl",
+ "discoveredToolCount": 3
+ },
+ "checks": {
+ "restore": "PASS",
+ "releaseBuild": "PASS",
+ "deterministicTests": "PASS",
+ "toolDiscovery": "PASS",
+ "aiFunctionAssignability": "PASS",
+ "serviceHealthCall": "PASS",
+ "deploymentCall": "PASS",
+ "runbookCall": "PASS",
+ "unknownToolRejection": "PASS",
+ "stdioEndToEnd": "PASS"
+ },
+ "exclusions": [
+ "LLM-driven tool selection",
+ "Agent Framework",
+ "Streamable HTTP",
+ "OAuth/authentication/authorization",
+ "MCP resources/prompts/sampling/elicitation/tasks",
+ "write-capable tools",
+ "database/Redis",
+ "cloud deployment",
+ "production readiness"
+ ]
+}
\ No newline at end of file