Changed & New Files (9 total)

NexaFlowDemo.McpServer/
New console app project — real MCP server over stdio
McpServer/Program.cs
AddMcpServer() + WithStdioServerTransport() + WithToolsFromAssembly()
McpServer/Tools/ (9 classes)
[McpServerToolType]/[McpServerTool] replacing IMcpTool implementations
Web/Services/RealMcpClientService.cs
IMcpServerService via McpClient.CreateAsync + StdioClientTransport
Web/Services/McpServerOptions.cs
Config POCO for McpServer section in appsettings.json
Web/Program.cs
Replace 10 tool registrations with single RealMcpClientService
Web/appsettings.json
Add McpServer section with ServerDll path config
Tests/NexaFlowDemo.Tests.csproj
Add McpServer project reference; 27 new tool tests
Tests/MCP/McpServerToolTests.cs
27 unit tests calling tool methods directly (no subprocess)

Contents

  1. NexaFlowDemo.McpServer — new project
  2. McpServer/Program.cs — server host
  3. McpServer/Tools/ — attribute-based tool classes
  4. Web/Services/RealMcpClientService.cs — new client bridge
  5. Web/Program.cs — DI registration change
  6. Web/appsettings.json — McpServer section
  7. Tests — new McpServer tool tests

1. NexaFlowDemo.McpServer

src/NexaFlowDemo.McpServer/NexaFlowDemo.McpServer.csproj
New project — only exists in MCP Version. A separate .NET 9 console application that hosts the real MCP server. The Web project spawns it as a subprocess via RealMcpClientService. Added to the solution alongside Core, Web, and Tests.
ClaudeLiveKey  — Project does not exist MCP Version  — New console app project
(project does not exist)1<Project Sdk="Microsoft.NET.Sdk">
2 <PropertyGroup>
3 <OutputType>Exe</OutputType>
4 <TargetFramework>net9.0</TargetFramework>
5 </PropertyGroup>
6 <ItemGroup>
7 <PackageReference Include="ModelContextProtocol" Version="1.3.0" />
8 <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.11.0" />
9 <ProjectReference Include="..\NexaFlowDemo.Core\NexaFlowDemo.Core.csproj" />
10 </ItemGroup>
11</Project>

2. McpServer/Program.cs

src/NexaFlowDemo.McpServer/Program.cs
New file. Hosts the MCP server over stdin/stdout using the official SDK. WithToolsFromAssembly discovers all [McpServerToolType] classes automatically. Logging to stdout is suppressed to prevent corrupting the JSON-RPC stream.
ClaudeLiveKey  — File does not exist MCP Version  — New file
(file not present)1var host = Host.CreateDefaultBuilder(args)
2 .ConfigureLogging(l => l.AddFilter("Microsoft", LogLevel.Warning))
3 .ConfigureServices((ctx, services) =>
4 {
5 services
6 .AddMcpServer()
7 .WithStdioServerTransport()
8 .WithToolsFromAssembly(Assembly.GetExecutingAssembly(), jsonOptions);
9 // RAG dependencies for document_search tool
10 services.AddSingleton<IEmbeddingService>(new SimulatedDenseEmbeddingService(384));
11 services.AddSingleton<IVectorStore, InMemoryVectorStore>();
12 services.AddSingleton<IDocumentStore>(sp => new VectorDocumentStore(...));
13 services.AddSingleton<RagPipeline>();
14 services.AddHostedService<KnowledgeBaseSeeder>();
15 })
16 .Build();
17await host.RunAsync();

3. McpServer/Tools/ — Attribute-Based Tool Classes

src/NexaFlowDemo.McpServer/Tools/*.cs
Why it changed: The old approach required each tool to implement IMcpTool with a Definition property (manual JSON schema) and an InvokeAsync(Dictionary<string,string>) method (manual parameter parsing). The new approach uses [McpServerToolType]/[McpServerTool]/[Description] attributes — the SDK generates the schema and handles all serialisation. AgentDelegationTool is removed; the subprocess model provides equivalent separation.
ClaudeLiveKey  — IMcpTool interface, in Core/MCP/Tools/ MCP Version  — [McpServerToolType] attributes, in McpServer/Tools/
1// src/NexaFlowDemo.Core/MCP/Tools/InventoryLookupTool.cs1// src/NexaFlowDemo.McpServer/Tools/InventoryTools.cs
2public sealed class InventoryLookupTool : IMcpTool2[McpServerToolType]
3{3public static class InventoryTools
4 public McpToolDefinition Definition => new()4{
5 {5 [McpServerTool(Name = "inventory_lookup", ReadOnly = true)]
6 Name = "inventory_lookup",6 [Description("Look up current stock level for a SKU.")]
7 Description = "Look up current stock level for a SKU.",7 public static string LookupInventory(
8 Parameters = [new() { Name="sku", Type="string", Required=true,8 [Description("The product SKU code, e.g. NF-SKU-001")] string sku)
9 Description="The product SKU code" }]9 {
10 };10 // ... look up data ...
11 public Task<McpInvokeResponse> InvokeAsync(Dictionary<string,string> input)11 return JsonSerializer.Serialize(new { success = true, result = new { sku, ... } });
12 {12 }
13 var sku = input.GetValueOrDefault("sku", "");13}
14 return Task.FromResult(new McpInvokeResponse { Success = true, Result = new{...} });
15 }
16}
… 9 similar tool files in Core/MCP/Tools/ (total ~10 tools incl. AgentDelegationTool) …… 9 tool files in McpServer/Tools/ (no AgentDelegationTool) …

4. Web/Services/RealMcpClientService.cs

src/NexaFlowDemo.Web/Services/RealMcpClientService.cs
New file — only exists in MCP Version. Implements IMcpServerService using the official McpClient from the ModelContextProtocol NuGet package. Spawns the McpServer process via StdioClientTransport and routes all tool calls over real JSON-RPC 2.0. The old McpServerService (which held an in-process tool dictionary) is no longer registered.
ClaudeLiveKey  — McpServerService (in-process dictionary) MCP Version  — RealMcpClientService (subprocess + JSON-RPC)
1// McpServerService.cs — in Core/MCP/1// RealMcpClientService.cs — in Web/Services/
2public class McpServerService : IMcpServerService2public sealed class RealMcpClientService : IMcpServerService, IAsyncDisposable
3{3{
4 private readonly Dictionary<string, IMcpTool> _tools;4 private IMcpClient? _client;
5 public McpServerService(IEnumerable<IMcpTool> tools, ILogger<...> logger)5 private readonly SemaphoreSlim _initLock = new(1, 1);
6 {6
7 _tools = tools.ToDictionary(t => t.Definition.Name);7 private async Task EnsureInitializedAsync(CancellationToken ct)
8 }8 {
9 public async Task<McpInvokeResponse> InvokeAsync(string toolName, ...)9 if (_client != null) return;
10 {10 await _initLock.WaitAsync(ct);
11 if (!_tools.TryGetValue(toolName, out var tool))11 try {
12 return new McpInvokeResponse { Success = false, Error = "not found" };12 if (_client != null) return;
13 return await tool.InvokeAsync(input);13 var transport = new StdioClientTransport(new StdioClientTransportOptions
14 {
15 Command = "dotnet", Arguments = ["exec", ResolveServerCommand()],
16 Name = "NexaFlowDemoMcpServer"
17 });
18 _client = await McpClient.CreateAsync(transport, cancellationToken: ct);
19 } finally { _initLock.Release(); }
20 }
21
22 public async Task<McpInvokeResponse> InvokeAsync(string toolName, ...)
23 {
24 await EnsureInitializedAsync(ct);
25 var result = await _client!.CallToolAsync(toolName, args, ct);
26 var text = result.Content.OfType<TextContentBlock>().FirstOrDefault()?.Text ?? "";
27 return new McpInvokeResponse { Success = !result.IsError, Result = text };
28 }
14 }29}

5. Web/Program.cs — DI Registration Change

src/NexaFlowDemo.Web/Program.cs
Why it changed: Ten lines of tool registration (nine AddSingleton<IMcpTool, ...>() calls plus AddSingleton<IMcpServerService, McpServerService>()) are replaced by a single AddSingleton<IMcpServerService, RealMcpClientService>(). The tools now live in the McpServer project, so the Web project no longer needs to know about them individually.
ClaudeLiveKey  — 10 tool/service registrations MCP Version  — 1 registration + McpServerOptions
42// ── MCP Tools ─────────────────────────────────────────────42// ── MCP Client (real subprocess via JSON-RPC stdio) ────────
43builder.Services.AddSingleton<IMcpTool, InventoryLookupTool>();43builder.Services.Configure<McpServerOptions>(
44builder.Services.AddSingleton<IMcpTool, SupplierLookupTool>();44 builder.Configuration.GetSection("McpServer"));
45builder.Services.AddSingleton<IMcpTool, ShipmentTrackerTool>();45builder.Services.AddSingleton<IMcpServerService, RealMcpClientService>();
46builder.Services.AddSingleton<IMcpTool, DocumentSearchTool>();
47builder.Services.AddSingleton<IMcpTool, CalculatorTool>();
48builder.Services.AddSingleton<IMcpTool, DateTimeTool>();
49builder.Services.AddSingleton<IMcpTool, WebSearchTool>();
50builder.Services.AddSingleton<IMcpTool, CodeInterpreterTool>();
51builder.Services.AddSingleton<ErpConnectorTool>();
52builder.Services.AddSingleton<IMcpTool>(sp => sp.GetRequiredService<ErpConnectorTool>());
53builder.Services.AddSingleton<IMcpTool, AgentDelegationTool>();
54builder.Services.AddSingleton<IMcpServerService, McpServerService>();
55 46
… (remainder identical) …… (remainder identical) …

6. Web/appsettings.json — McpServer Section

src/NexaFlowDemo.Web/appsettings.json
Why it changed: A new McpServer section is added. RealMcpClientService auto-detects the server DLL by walking from the web project's binary, so the ServerDll field is optional — useful only when running from a non-standard output path.
ClaudeLiveKey MCP Version
1{1{
… (lines 2–28 identical) …… (lines 2–28 identical) …
29 "Carrier": {29 "Carrier": {
30 "SimulationMode": true,30 "SimulationMode": true,
31 "ApiKey": ""31 "ApiKey": ""
32 }32 },
33 "McpServer": {
34 "ServerDll": ""
35 }
33}36}

7. Tests — McpServer Project Reference + 27 New Tests

src/NexaFlowDemo.Tests/NexaFlowDemo.Tests.csproj
Why it changed: The test project now references NexaFlowDemo.McpServer so that tool methods can be tested directly (no subprocess needed — just call the static methods). 27 new tests cover all 9 tool classes. Total tests: 154 original + 27 = 181 passing.
ClaudeLiveKey  — 2 project references, 154 tests MCP Version  — 3 project references, 181 tests
12 <ItemGroup>12 <ItemGroup>
13 <ProjectReference Include="..\NexaFlowDemo.Core\..." />13 <ProjectReference Include="..\NexaFlowDemo.Core\..." />
14 <ProjectReference Include="..\NexaFlowDemo.McpServer\..." />
14 <ProjectReference Include="..\NexaFlowDemo.Web\..." />15 <ProjectReference Include="..\NexaFlowDemo.Web\..." />
15 </ItemGroup>16 </ItemGroup>
No McpServerToolTests.csNew: Tests/MCP/McpServerToolTests.cs — 27 tests
(file not present)1// McpServerToolTests.cs — tests call static methods directly
2// InventoryTools (5), SupplierTools (4), ShipmentTools (3)
3// CalculatorTools (4), DateTimeTools (3), WebSearchTools (3)
4// ErpTools (5) — total: 27 new tests

What Did NOT Change

Colour Legend

Removed in MCP Version (ClaudeLiveKey-only line)
Added in MCP Version
Unchanged context