Migration from simulated IMcpTool/McpServerService to the official ModelContextProtocol v1.3.0 NuGet package with real JSON-RPC 2.0 stdio transport | Generated 2026-05-24
| 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> | |||
| ClaudeLiveKey — File does not exist | MCP Version — New file | |||
| (file not present) | 1 | var 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(); | |||
| 17 | await host.RunAsync(); | |||
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.cs | 1 | // src/NexaFlowDemo.McpServer/Tools/InventoryTools.cs | |
| 2 | public sealed class InventoryLookupTool : IMcpTool | 2 | [McpServerToolType] | |
| 3 | { | 3 | public 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) … | |||
| ClaudeLiveKey — McpServerService (in-process dictionary) | MCP Version — RealMcpClientService (subprocess + JSON-RPC) | |||
| 1 | // McpServerService.cs — in Core/MCP/ | 1 | // RealMcpClientService.cs — in Web/Services/ | |
| 2 | public class McpServerService : IMcpServerService | 2 | public 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 | } | |
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) ──────── | |
| 43 | builder.Services.AddSingleton<IMcpTool, InventoryLookupTool>(); | 43 | builder.Services.Configure<McpServerOptions>( | |
| 44 | builder.Services.AddSingleton<IMcpTool, SupplierLookupTool>(); | 44 | builder.Configuration.GetSection("McpServer")); | |
| 45 | builder.Services.AddSingleton<IMcpTool, ShipmentTrackerTool>(); | 45 | builder.Services.AddSingleton<IMcpServerService, RealMcpClientService>(); | |
| 46 | builder.Services.AddSingleton<IMcpTool, DocumentSearchTool>(); | |||
| 47 | builder.Services.AddSingleton<IMcpTool, CalculatorTool>(); | |||
| 48 | builder.Services.AddSingleton<IMcpTool, DateTimeTool>(); | |||
| 49 | builder.Services.AddSingleton<IMcpTool, WebSearchTool>(); | |||
| 50 | builder.Services.AddSingleton<IMcpTool, CodeInterpreterTool>(); | |||
| 51 | builder.Services.AddSingleton<ErpConnectorTool>(); | |||
| 52 | builder.Services.AddSingleton<IMcpTool>(sp => sp.GetRequiredService<ErpConnectorTool>()); | |||
| 53 | builder.Services.AddSingleton<IMcpTool, AgentDelegationTool>(); | |||
| 54 | builder.Services.AddSingleton<IMcpServerService, McpServerService>(); | |||
| 55 | 46 | |||
| … (remainder identical) … | … (remainder identical) … | |||
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 | } | |
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.cs | New: 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 | |||
Mock<ILlmService> or SimulatedLlmService directly; they never call the live API or subprocess.<tool_call>/<final_answer> format. Claude doesn't know MCP changed.RealMcpClientService.