Code Modules in This Guide
Web
Program.cs appsettings.json
Web/Services
RealMcpClientService.cs McpServerOptions.cs
McpServer
Program.cs Tools/ (9 classes)
Core/MultiTenant
ITenantContext.cs TenantRegistry.cs TenantScopedDocumentStore.cs
Core/Audit
AuditLogger.cs AuditEntry.cs

1 The 4-Project Solution

The MCP Version adds one project compared to the ClaudeLiveKey version:

ProjectTypeRole
NexaFlowDemo.Core Class library Domain models, agents, RAG, memory, workflows, DI interfaces. Unchanged from ClaudeLiveKey.
NexaFlowDemo.Web ASP.NET Core 9 MVC Controllers, hubs, views. RealMcpClientService replaces the old McpServerService.
NexaFlowDemo.McpServer Console app (new) Hosts the real MCP server over stdio. Contains 9 [McpServerToolType] tool classes.
NexaFlowDemo.Tests xUnit test project 181 tests: 154 original Core tests + 27 new McpServer tool tests (direct method calls, no subprocess).

2 A Request from Browser to Answer

When a user types a question in the chat UI and presses Enter, here is the complete chain including the McpServer subprocess:

Browser ──▶ sends query via SignalR WebSocket
AgentHub ──▶ calls IMultiAgentOrchestrator.RunAsync()
Orchestrator ──▶ picks ReactAgent / ParallelReactAgent / PlanningAgent
Agent loop ──▶ builds prompt, calls ILlmService.CompleteAsync()
ClaudeService ──▶ HTTP POST to api.anthropic.com/v1/messages
Claude API ──▶ returns XML reply: <tool_call> or <final_answer>
Agent loop ──▶ parses XML, calls RealMcpClientService.InvokeAsync(toolName)
RealMcpClientService ──▶ McpClient.CallToolAsync() via StdioClientTransport (JSON-RPC 2.0)
NexaFlowDemo.McpServer ──▶ routes to [McpServerTool] method, returns JSON string
Agent loop ──▶ adds tool result to history, loops back to Claude
AgentHub ──▶ yields each AgentStep to browser via SignalR as it arrives
Browser ──▶ displays thinking / tool calls / final answer in real time

The yellow lines show the new MCP subprocess path. The McpServer process is started lazily by RealMcpClientService on the first tool call — you do not need to start it manually.

3 NexaFlowDemo.Web — Program.cs Walkthrough

src/NexaFlowDemo.Web/Program.cs

The key change versus ClaudeLiveKey: IMcpServerService is now registered as RealMcpClientService instead of the simulated McpServerService. Individual tool registrations are gone — the tools live in the McpServer project.

RegistrationWhat it does
AddHttpClient<ILlmService, ClaudeService> Registers ClaudeService with IHttpClientFactory. Base URL and auth headers set once at startup. See Guide 01.
AddSingleton<IMcpServerService, RealMcpClientService> New in MCP Version. The real MCP client bridge. Spawns the McpServer subprocess lazily and routes all tool calls over JSON-RPC 2.0 stdio. Replaces the old McpServerService + 9 individual tool registrations.
Configure<McpServerOptions>("McpServer") Binds the McpServer section from appsettings.json — provides the server executable path override (optional; auto-detect is used by default).
AddSingleton<IEmbeddingService>(new SimulatedDenseEmbeddingService(384)) Simulated embeddings used by RAG in the Web app (Chat/RAG pages). The McpServer registers its own instance for the document_search tool.
AddSingleton<IVectorStore, InMemoryVectorStore> In-memory similarity search store for the Web app's RAG pipeline.
AddSingleton<IDocumentStore>(...) Factory: pulls IVectorStore and IEmbeddingService to build VectorDocumentStore.
AddSingleton<ReactAgent> etc. Three agent types registered as concrete types. The orchestrator takes all three via constructor injection.
AddSingleton<IMultiAgentOrchestrator, MultiAgentOrchestrator> Routes incoming queries to the right agent. Called by the SignalR hub.
AddSingleton<RagPipeline> + AddSingleton<RagEvaluator> RAG pipeline and quality evaluator for the /Chat and /Evaluation pages.
AddSingleton<IMemoryStore, InMemoryMemoryStore> In-memory keyword-searchable memory store.
AddSingleton<IConversationStore, InMemoryConversationStore> In-memory conversation history. Switch to SqliteConversationStore for persistence.
AddSingleton<IWorkflow, ReorderWorkflow> etc. Each workflow registered under IWorkflow. WorkflowEngine receives them as IEnumerable<IWorkflow>.
AddSingleton<ITenantContext>(TenantRegistry.Acme) Hard-wires the active tenant to "Acme". In a real multi-tenant system this would be resolved per request.
AddSingleton<IAuditLogger, InMemoryAuditLogger> In-memory append-only audit log. Viewed at /Audit.
AddSingleton<ICarrierApiService, SimulatedCarrierApiService> Simulated shipping carrier (FedEx, UPS, USPS, DHL). Returns deterministic fake tracking data.
AddSingleton<IUserService, DemoUserService> 4 hardcoded users (admin, ops, manager, exec) with SHA-256 hashed passwords.
What changed vs ClaudeLiveKey One registration replaced ~10: the nine AddSingleton<IMcpTool, ...>() lines and the AddSingleton<IMcpServerService, McpServerService>() line are replaced by a single AddSingleton<IMcpServerService, RealMcpClientService>(). Tools now live in the McpServer project instead of the Core project.

4 NexaFlowDemo.McpServer — Program.cs

src/NexaFlowDemo.McpServer/Program.cs

The McpServer is a minimal .NET console app. Its only job is to host the MCP server over stdio:

var host = Host.CreateDefaultBuilder(args)
    .ConfigureLogging(l => l.AddFilter("Microsoft", LogLevel.Warning))
    .ConfigureServices((ctx, services) =>
    {
        // MCP server — scans assembly for [McpServerToolType] classes
        services
            .AddMcpServer()
            .WithStdioServerTransport()
            .WithToolsFromAssembly(Assembly.GetExecutingAssembly(), jsonOptions);

        // Services injected into tool constructors (e.g. for document_search)
        services.AddSingleton<IEmbeddingService>(new SimulatedDenseEmbeddingService(384));
        services.AddSingleton<IVectorStore, InMemoryVectorStore>();
        services.AddSingleton<IDocumentStore>(sp => ...);
        services.AddSingleton<RagPipeline>();
        services.AddHostedService<KnowledgeBaseSeeder>();  // seeds docs at startup
    })
    .Build();

await host.RunAsync();

Log output is suppressed except warnings — because the McpServer communicates over stdout, any log lines written to stdout would corrupt the JSON-RPC stream. The AddFilter call prevents that.

5 SignalR Hubs — Real-Time Streaming

src/NexaFlowDemo.Web/Hubs/AgentHub.cs
src/NexaFlowDemo.Web/Hubs/OrchestrationHub.cs

SignalR keeps a persistent WebSocket connection open between the browser and the server. Instead of waiting for the whole answer, the server pushes each AgentStep — one "Thinking…", one "Calling tool…", one "Got result: 250 units" — to the browser as soon as it happens.

C# analogy — it is like IAsyncEnumerable over the network: You already know that await foreach yields results one at a time from an async sequence. SignalR does the same thing across a network connection. The server calls Clients.Caller.SendAsync("AgentStep", step) for each item — exactly like yielding from an IAsyncEnumerable.
// AgentHub.cs — simplified
public async Task RunQuery(string query, string agentType)
{
    await foreach (var step in _orchestrator.RunAsync(new AgentRunRequest { Query = query, AgentType = agentType }))
    {
        await Clients.Caller.SendAsync("AgentStep", step);
    }
}

6 Multi-Tenancy

src/NexaFlowDemo.Core/MultiTenant/TenantContext.cs
src/NexaFlowDemo.Core/MultiTenant/TenantScopedDocumentStore.cs

This project supports three tenants: Acme, Globex, and Initech. The active tenant is provided via ITenantContext. TenantScopedDocumentStore wraps IDocumentStore and automatically stamps every document with metadata["tenantId"] = tenantId, then filters search results to the current tenant.

public interface ITenantContext
{
    string   TenantId   { get; }   // e.g. "tenant-acme"
    string   TenantName { get; }   // e.g. "Acme Corp"
    string[] AllowedRoles { get; }
}

public static class TenantRegistry
{
    public static ITenantContext Acme    = new TenantContext("tenant-acme",    "Acme Corp",    ...);
    public static ITenantContext Globex  = new TenantContext("tenant-globex",  "Globex Ltd",   ...);
    public static ITenantContext Initech = new TenantContext("tenant-initech", "Initech Inc",  ...);
}

7 Cookie Authentication & DemoUserService

src/NexaFlowDemo.Web/Services/UserService.cs

Authentication uses ASP.NET Core cookie authentication. DemoUserService holds four hardcoded users:

UsernamePasswordRoleTenant
adminadmin123AdminAcme
opsops123OpsAcme
managermanager123ManagerGlobex
execexec123ExecInitech

Passwords are stored as SHA-256 hashes. On login, the input is hashed and compared:

var hash = SHA256.HashData(Encoding.UTF8.GetBytes(password));
var hex  = Convert.ToHexString(hash).ToLowerInvariant();

8 Audit Logging

src/NexaFlowDemo.Core/Audit/InMemoryAuditLogger.cs

Every MCP tool call, workflow trigger, and important action can be recorded. InMemoryAuditLogger stores entries in a thread-safe list and returns them most-recent-first:

public class AuditEntry
{
    public string   Id        { get; set; }
    public DateTime Timestamp { get; set; }
    public string   UserId    { get; set; }
    public string   TenantId  { get; set; }
    public string   Action    { get; set; }   // "tool_call", "workflow_trigger", etc.
    public string   Resource  { get; set; }   // tool name, workflow name, etc.
    public string   Details   { get; set; }   // JSON input/output
    public bool     Success   { get; set; }
}

9 Carrier Integration

src/NexaFlowDemo.Core/Integration/SimulatedCarrierApiService.cs

SimulatedCarrierApiService returns deterministic fake rates from FedEx, UPS, USPS, and DHL. Shipments get tracking numbers in the format NF0001. To plug in a real carrier API, implement ICarrierApiService and swap the registration in Program.cs.

10 Web Controllers at a Glance

AgentController
REST API wrapper around the agent orchestrator. Returns JSON steps.
/api/agent/run
McpController
REST API for listing and invoking MCP tools via RealMcpClientService.
/api/mcp/{toolName}
RagController
Query the RAG pipeline and view retrieved documents.
/Rag
WorkflowController
View registered workflows and fire triggers manually.
/Workflow
AuditController
View and search the audit log.
/Audit
IntegrationController
UI for carrier rates, shipment creation, and ERP actions.
/Integration
StreamController
Server-Sent Events (SSE) endpoint — alternative to SignalR for streaming agent steps.
/stream/run
WebhookController
Receives inbound order events, fires workflow triggers.
/api/webhooks/orders
AuthController
Login / logout with cookie authentication.
/Auth/Login
HomeController
Dashboard and main landing page.
/

11 SSE vs SignalR — Two Streaming Options

SignalR (WebSocket)SSE (Server-Sent Events)
DirectionTwo-way: browser ↔ serverOne-way: server → browser only
SetupRequires SignalR JS library + hub endpointPlain fetch() or EventSource, no library needed
Use caseChat UI where user can cancel, send new messages mid-streamSimple monitoring page, external tool calling the API
In this projectMain chat interface (AgentHub.cs)GET /stream/run?query=... (StreamController.cs)

12 How to Extend the Project (MCP Version)

Add a new MCP tool

  1. In NexaFlowDemo.McpServer/Tools/, create a new class or add a method to an existing class.
  2. Decorate the class with [McpServerToolType] and each method with [McpServerTool(Name="...")] + [Description("...")].
  3. Rebuild NexaFlowDemo.McpServer. WithToolsFromAssembly picks up the new tool automatically.
  4. The web app's RealMcpClientService will see the new tool listed at next ListToolsAsync() call — no web app changes needed.

Add a new workflow

  1. Create a class in src/NexaFlowDemo.Core/Workflows/ implementing IWorkflow.
  2. Add builder.Services.AddSingleton<IWorkflow, YourWorkflow>(); in NexaFlowDemo.Web/Program.cs.
  3. Fire it from anywhere with workflowEngine.TriggerAsync("your_trigger_name", context);

Add a new knowledge base document

  1. Open src/NexaFlowDemo.Core/Data/KnowledgeBaseSeeder.cs (used by the Web app) and the KnowledgeBaseSeeder inside the McpServer project (used by the document_search tool).
  2. Add await store.AddAsync(new Document { ... }); to both.
  3. Restart both projects — the document is indexed at startup.

13 Summary — The Whole Picture