Experiments with DDD, CQRS+ES, MCP and Agents
Overview
BrewUpErp is an end‑to‑end sample ERP for a brewery, built to demonstrate: - Domain‑Driven Design (DDD) and a modular / bounded‑context architecture - CQRS and event sourcing with Muflone - Functional‑style domain modelling and clean, testable application layers - : one MCP server per module, an A2A agent, and a coordinator that orchestrates them - for local orchestration and observability of every service The solution is intentionally realistic but still small enough to study. It consists of: - – backend HTTP API, domain, MCP servers and the Knowledge agent (ASP.NET Core, .NET 10) - – Blazor web front‑end - – .NET MAUI mobile application Note: this repository is a work in progress. Expect breaking changes and incomplete features, especially in less‑used flows. The backend is a (BrewUp.Rest) surrounded by — one per module — plus a for the Knowledge module.
README
BrewUpErp
BrewUpErp is an end‑to‑end sample ERP for a brewery, built to demonstrate:
- Domain‑Driven Design (DDD) and a modular / bounded‑context architecture
- CQRS and event sourcing with Muflone
- Functional‑style domain modelling and clean, testable application layers
- An agentic AI layer: one MCP server per module, an A2A agent, and a coordinator that orchestrates them
- .NET Aspire for local orchestration and observability of every service
The solution is intentionally realistic but still small enough to study. It consists of:
- BrewApi – backend HTTP API, domain, MCP servers and the Knowledge agent (ASP.NET Core, .NET 10)
- BrewSpa – Blazor web front‑end
- BrewApp – .NET MAUI mobile application
Note: this repository is a work in progress. Expect breaking changes and incomplete features, especially in less‑used flows.
Architecture at a glance
The backend is a modular monolith (BrewUp.Rest) surrounded by independently deployable MCP
servers — one per module — plus a standalone A2A agent for the Knowledge module. The MCP servers
never talk to the monolith: each one reads its own module’s read model directly, so the AI surface of
a module ships and scales on its own.
Two consumers use that surface:
- the Mother coordinator inside
BrewUp.Rest, which drives Azure OpenAI function calling over the pooled tool catalog of all four MCP servers, and delegates knowledge questions to the Knowledge agent over A2A; - any external MCP client (VS Code, Claude Code, …), since every server speaks plain MCP over HTTP
at
/mcp.
The whole set is started by the Aspire AppHost, which injects configuration and points every service at the Aspire dashboard over OTLP.
Solution Structure
src/
├── BrewApi/ # Backend API, domain, MCP servers, Knowledge agent, Aspire AppHost
├── BrewSpa/ # Blazor web front-end
└── BrewApp/ # .NET MAUI mobile app
BrewDocs/ # Bruno API contracts & example payloads
docker/ # Backing services (KurrentDB, MongoDB, RabbitMQ)
infra/
├── Bicep/ # Azure Container Apps deployment
└── Docker/ # MCP + agent compose files, .env template
docs/images/ # Architecture diagrams
BrewApi
The backend solution is src/BrewApi/BrewUp.slnx.
Orchestration & cross‑cutting projects
| Project | Purpose |
|---|---|
BrewOrchestrator.Host |
.NET Aspire AppHost — starts the MCP containers, the Knowledge agent and the REST API |
BrewOrchestrator.ServiceDefaults |
Aspire service defaults: OpenTelemetry, health checks, service discovery, HTTP resilience |
BrewUp.Rest |
ASP.NET Core Web API host — composition root with explicit module registration |
BrewUp.Shared |
Shared abstractions: value objects, domain IDs, messages, read‑model contracts, validators, A2A agent contracts (AgentCard, A2ATaskRequest/Response, IAgent) |
BrewUp.Infrastructure |
Cross‑cutting infrastructure: MongoDB helper, Muflone wiring (KurrentDB, RabbitMQ, Azure Service Bus) |
Bounded contexts — each normally contains Domain, Facade, Infrastructure, ReadModel,
SharedKernel and Tests:
| Context | Description | MCP server |
|---|---|---|
MasterData/ |
Beer catalog, customers, suppliers, warehouses (also has .Entities) |
✅ BrewUp.MasterData.McpServer |
Sales/ |
Sales orders and related workflows | ✅ BrewUp.Sales.McpServer |
Warehouse/ |
Warehouse and stock management (also has .Entities) |
✅ BrewUp.Warehouse.McpServer |
Knowledge/ |
Document ingestion, RAG search, and an evidence-backed synthesized Wiki (has .Core in place of .Domain) |
✅ BrewUp.Knowledge.McpServer + BrewUp.Knowledge.Agent |
Purchases/ |
Purchase orders | — |
Dashboards/ |
Cross‑context dashboard aggregations (also has .Entities) |
— |
Sagas/ |
Long‑running process managers / sagas | — |
Chat/ |
BrewUp.Mother.Facade + BrewUp.Mother.SharedKernel — the Mother coordinator, chat service and MCP client pool |
— |
Module composition in BrewUp.Rest
Program.cs uses an explicit composition‑root pattern — each feature registers itself as an IModule:
SharedModule · CorsModule · LoggingModule · TelemetryModule · InfrastructureModule · OpenApiModule
MasterDataModule · PurchasesModule · SalesModule · WarehouseModule
DashboardsModule · SagasModule · ChatModule · KnowledgeModule
The same pattern is reused, at a smaller scale, inside every MCP server and inside the Knowledge agent.
BrewSpa
The Blazor front‑end solution (src/BrewSpa/BrewSpa.slnx) mirrors the API’s bounded‑context decomposition:
| Project | Purpose |
|---|---|
BrewSpa/BrewSpa |
Main Blazor app – shell, layouts, pages, wwwroot |
BrewSpa.Shared |
Shared client models, helpers, tests |
BrewSpa.Shared.Components |
Reusable Razor components, custom types, JS interop, messages |
Chat/ |
BrewSpa.Chat.Application, BrewSpa.Chat.Facade — chat UI against the Mother endpoint |
Dashboards/ |
BrewSpa.Dashboards.ApplicationServices, .Facade, .Tests |
MasterData/ |
BrewSpa.MasterData.Application, .Facade |
Sales/ |
BrewSpa.Sales.Application, .Facade |
BrewApp
The .NET MAUI mobile solution lives under src/BrewApp/mobile/:
| Folder | Purpose |
|---|---|
mobile/src/ |
BrewApp.Mobile – AppShell, features, components, services, models, platform targets |
mobile/tests/ |
Mobile unit / integration tests |
specs/ |
Specification & acceptance artifacts (.specify) |
Bounded Context Layer Layout
| Layer | Responsibility |
|---|---|
*.Domain |
Aggregates, value objects, domain events, domain services |
*.SharedKernel |
Types shared only within the bounded context |
*.Infrastructure |
Persistence and integration (MongoDB, event store, messaging, embeddings) |
*.ReadModel |
Denormalized read models, queries and projections |
*.Facade |
Application services / use‑case orchestration, HTTP endpoints, in‑process agents |
*.McpServer |
Standalone MCP server exposing the module’s read side as AI tools |
The AI layer
MCP servers — one per module
Every MCP server is a small ASP.NET Core app built on the official MCP C# SDK 2.x and aligned with
the MCP 2026-07-28 protocol. BrewUp deliberately uses discovery-first negotiation and stateless
Streamable HTTP: each request is self-describing and normal operation does not depend on an
Mcp-Session-Id or transport-level session state.
Each server:
- registers a stateless HTTP MCP transport and maps it at
/mcp(MapMcp("/mcp")); - exposes one
[McpServerToolType]class whose[McpServerTool]methods delegate to a thin façade over the module’s read model; - calls
AddServiceDefaults()so it takes part in Aspire telemetry, health checks and resilience; - exposes
/health(and/alivein Development); - ships as its own container image.
Statelessness is an architectural choice rather than an incidental SDK default. BrewUp tools query bounded-context read models and do not need cross-request transport state. Protocol negotiation, per-request MCP metadata and fallback to older MCP peers are handled by the SDK; BrewUp does not implement custom compatibility protocol code.
MasterData MCP — brewup.masterdata.mcpserver
| Tool | Description |
|---|---|
get_catalog_beers |
The current beer catalog |
get_beer_details |
Beer details by (partial) beer id |
resolve-beer-catalog |
Resolve a beer by (partial) name — used by Mother to turn free text into ids |
get_active_customers |
Active customers |
get_customer_info |
Customer details by (partial) id |
get_active_suppliers |
Active suppliers |
get_supplier_info |
Supplier details by (partial) id |
get_active_warehouses |
Active warehouses |
Sales MCP — brewup.sales.mcpserver
| Tool | Description |
|---|---|
get_open_sales_orders |
Currently open sales orders |
get_sales_order_details |
Order details by (partial) order id |
get_orders_by_customer |
Orders for a customer name |
get_orders_by_beer |
Orders for a beer name |
get_late_sales_orders |
Late orders at a given business date (yyyy-MM-dd) |
get_customer_total_purchased |
Total purchased per customer |
get_sales_order_total_quantities |
Total quantities for an order |
Warehouse MCP — brewup.warehouse.mcpserver
| Tool | Description |
|---|---|
get_beer_availability |
Current availability for a beer |
get_reorder_thresholds |
Reorder threshold for a beer |
Knowledge MCP — brewup.knowledge.mcpserver
| Tool | Description |
|---|---|
search_knowledge_base |
RAG search over ingested documents. Parameters: query, optional scope (General, Sales, Warehouse, MasterData, Production), topK (default 5, capped at 20) |
query_wiki |
Semantic search over synthesized Wiki pages. Returns derived knowledge rather than operational ERP state |
get_wiki_page |
Current page content, claims, links, and unresolved issues by stable key or title |
get_wiki_page_evidence |
Claim-level source chunks for a page, including evidence availability |
Knowledge module — ingestion, RAG, and synthesized Wiki
The Knowledge context is the read side that the Knowledge MCP server queries. It is written from
BrewUp.Rest through /v1/knowledge:
| Endpoint | Purpose |
|---|---|
POST /v1/knowledge/ingest |
Ingest a document from text |
POST /v1/knowledge/ingest-file |
Ingest an uploaded file (plain text, Markdown, PDF via PdfPig) |
POST /v1/knowledge/search |
Search (the same retrieval the MCP tool uses) |
GET /v1/knowledge/documents |
List documents |
GET /v1/knowledge/documents/{id} |
Document metadata |
GET /v1/knowledge/documents/{id}/chunks |
Inspect the chunks produced for a document |
POST /v1/knowledge/documents/{id}/reindex |
Re‑chunk and re‑embed |
DELETE /v1/knowledge/documents/{id} |
Delete a document and its chunks |
POST /v1/knowledge/wiki/query |
Search current synthesized Wiki pages |
GET /v1/knowledge/wiki/pages/{key} |
Get a page with its claims, links, and unresolved issues |
GET /v1/knowledge/wiki/pages/{pageId}/evidence |
Get claim-level provenance for the current revision |
GET /v1/knowledge/documents/{id}/wiki-job |
Inspect asynchronous Wiki synthesis status |
The durable RAG path remains:
extract text → semantic chunking → chunk embeddings → vector store
When Knowledge:Wiki:Enabled is true, successful ingestion additionally enqueues a durable Wiki job.
A hosted worker asks Azure OpenAI for strict structured page, claim, link, and issue proposals, validates
every source chunk and page reference, generates page embeddings, and commits the complete analysis in
one transaction. A synthesis failure is retried and reported by the job endpoint, but never rolls back a
successful RAG ingestion.
Wiki pages are derived, comparatively stable interpretations of documents. They are not authoritative operational state: live inventory, orders, and other ERP facts must still come from their owning module. Every current claim points to the exact document chunks that support it. Deleting or reindexing a source marks its old evidence unavailable while retaining page revisions and provenance identities; successful reindexing queues a fresh synthesis.
| Concern | Options |
|---|---|
Vector store (Knowledge:VectorStore) |
SqlServer (default) or AzureAiSearch; an in‑memory store is used when no configuration is supplied (tests) |
| Embeddings | Azure OpenAI (BrewUp:Embeddings) when endpoint + deployment are set, otherwise a deterministic FakeEmbeddingGenerator so the pipeline still runs offline |
Wiki synthesis (Knowledge:Wiki) |
Disabled by default. Configure Enabled, PollIntervalSeconds, LeaseDurationSeconds, CandidateLimit, MaximumAttempts, MaximumPagesPerAnalysis, MaximumClaimsPerPage, and MaximumContentLength |
| Wiki model | Uses BrewUp:AzureOpenAI (or the existing top-level AzureOpenAI fallback): Endpoint, DeploymentName, and either ApiKey or managed identity settings |
For manual SQL Server provisioning, run infra/SqlServer/CreateKnowledgeWikiSchema.sql
against the Knowledge database after matching @WikiEmbeddingDimensions to the configured embedding dimensions.
Knowledge Agent — A2A
BrewUp.Knowledge.Agent is a standalone agent (built on Microsoft.Agents.AI) that fronts the
Knowledge MCP server with an Agent‑to‑Agent contract instead of an MCP one:
| Endpoint | Purpose |
|---|---|
GET /.well-known/agent-card.json |
Standard A2A discovery document |
GET /a2a/agent-card |
The same card, on a convenience route |
POST /a2a/tasks |
Submit an A2ATaskRequest (taskId, message, correlationId) and get an A2ATaskResponse back |
Its agent card advertises the search_knowledge skill plus capabilities for knowledge retrieval,
documentation lookup, policy lookup and procedure lookup, with sample questions such as “What is the
reorder policy for IPA?”.
On each task the executor discovers the tools of the Knowledge MCP server, checks that
search_knowledge_base is present, calls it, and maps the hits into KnowledgeFinding records —
so a missing or unhealthy MCP server degrades into a clear failure summary rather than an exception.
The MCP endpoint it targets comes from KnowledgeAgent:Mcp:Endpoint (Aspire and Compose override it
with the in‑network address).
Mother — the coordinator
Chat/BrewUp.Mother.Facade exposes POST /chat and picks one of three paths:
- Knowledge question (
policy,procedure,documentation,quality standard, …) andBrewUp:Mother:A2A:Enabledistrue→ delegate to the Knowledge agent over A2A. - “What if” / impact question with parseable quantities (“what if we sell 500 bottles of IPA?”) →
run the in‑process agent chain
MasterDataAgent→SalesAgent→WarehouseAgent→KnowledgeAgent, then compose a stock/reorder recommendation. - Anything else → Azure OpenAI chat with function calling over the pooled MCP tool catalog.
Supporting details worth knowing:
McpToolsProviderkeeps one long‑livedMcpClientper server, caches the merged tool catalog and refreshes it every 5 minutes; a server that is down is logged and skipped instead of failing the request. Clients use MCP 2.x discovery first and leave protocol downgrade negotiation to the official SDK.- The standalone Knowledge Agent uses the same SDK-managed discovery and invocation path while preserving
its A2A boundary and the
execute_tool search_knowledge_basesemantic span. FoundryGuardedChatClientapplies theBrewUp:FoundryLimitsbudget (concurrency, requests/minute, queue length, max output tokens, max function‑calling iterations) so a small Foundry deployment cannot be overrun.- Every AI request creates a semantic AgentRun on the existing OpenTelemetry trace. Mother then adds workflow and agent spans that explain why the underlying HTTP, MCP, model and database calls happened.
- The run has its own
brewup.agent_run.id, separate from the technicalTraceIdand the userConversationId. The same run identifier is reused as the A2A correlation ID instead of creating unrelated identifiers at each layer.
This migration intentionally does not enable MCP Tasks, MCP Apps, subscriptions/listen, sampling,
elicitation or an authentication/OAuth redesign. Those capabilities remain separate future increments.
Aspire orchestration & observability
BrewOrchestrator.Host is the Aspire AppHost. It:
- declares 16 parameters (Mongo, SQL Server, Service Bus, RabbitMQ, KurrentDB, Azure OpenAI and
embeddings), the sensitive ones marked
secret: trueand read from AppHost user secrets underParameters:; - starts the four MCP servers and the Knowledge agent as containers, and
BrewUp.Restas a project; - injects connection strings into every container, plus
WithOtlpExporter()so each one exports traces, metrics and logs to the Aspire dashboard; - wires dependencies: the agent gets
WaitFor(knowledge-mcp)and itsKnowledgeAgent__Mcp__Endpointresolved by Aspire (neverlocalhost), andbrewup-restwaits for and references all four MCP endpoints.
The AppHost references images by name — it does not build them.
AddContainer("knowledge-mcp", "brewup.knowledge.mcpserver", "latest")resolves an image that must already exist locally or in your registry. See Building the container images.
Telemetry, end to end:
| Source | What it emits |
|---|---|
BrewOrchestrator.ServiceDefaults |
ASP.NET Core, HttpClient and runtime instrumentation; OTLP export when OTEL_EXPORTER_OTLP_ENDPOINT is set |
TelemetryModule (BrewUp.Rest) |
Registers the BrewUp.Mother.Coordinator and BrewUp.Chat activity sources, the BrewUp.Agent meter and SqlClient instrumentation; console exporter in Development |
MotherTelemetry |
AgentRun, invoke_workflow, invoke_agent and evaluation spans, plus agent run, duration and handoff metrics |
KnowledgeAgentTelemetry |
Internal Knowledge Agent execution and execute_tool search_knowledge_base spans, plus tool-call metrics |
BrewUp.Knowledge.Wiki |
knowledge.wiki.analysis spans with job, attempt, page/link/issue counts, outcome, and error type |
BrewUp.Chat |
Existing Microsoft.Extensions.AI model and function-calling instrumentation |
The semantic spans complement rather than replace the existing distributed trace. A what-if request is represented conceptually as:
HTTP POST /chat
└── AgentRun INTERNAL
route = what-if
brewup.agent_run.id =
gen_ai.conversation.id =
└── invoke_workflow brewup.what-if INTERNAL
├── invoke_agent MasterDataAgent INTERNAL
│ └── existing MCP / HTTP / database spans
├── invoke_agent SalesAgent INTERNAL
│ └── existing MCP / HTTP / database spans
├── invoke_agent WarehouseAgent INTERNAL
│ └── existing MCP / HTTP / database spans
├── invoke_agent KnowledgeAgent INTERNAL
│ └── existing MCP / HTTP / retrieval spans
└── evaluation INTERNAL
Mother is intentionally represented as a deterministic workflow:
gen_ai.operation.name = invoke_workflow
gen_ai.workflow.name = brewup.what-if
Real agent calls use gen_ai.operation.name = invoke_agent, gen_ai.agent.name and a stable
gen_ai.agent.id. In-process agent calls are INTERNAL; only the remote A2A call made by Mother is a
CLIENT operation. On the Knowledge Agent service the server-side semantic execution remains internal,
while the underlying MCP/HTTP transport retains its existing client span:
invoke_agent KnowledgeAgent CLIENT
└── HttpClient / ASP.NET A2A transport CLIENT / SERVER
└── invoke_agent BrewUp Knowledge Agent INTERNAL
└── execute_tool search_knowledge_base INTERNAL
gen_ai.tool.type = datastore
└── MCP / HTTP / vector search existing transport/data spans
Correlation and outcomes
The three identifiers deliberately remain distinct:
| Identifier | Meaning |
|---|---|
OpenTelemetry TraceId |
Technical distributed trace across processes and transports |
gen_ai.conversation.id |
User conversation/session, propagated when available |
brewup.agent_run.id |
One semantic execution of a user request; also carried through the A2A correlation mechanism |
brewup.route has one of three low-cardinality values: direct-ai, what-if or knowledge-a2a.
brewup.outcome describes operational execution (completed, partial or failed) and is independent
from deterministic evidence evaluation. Consequently, a valid trace can show:
HTTP success = true
brewup.outcome = completed
brewup.evaluation.passed = false
This means the workflow completed, but the gathered bounded-context evidence was insufficient. A valid
Knowledge lookup that returns zero findings is not marked as an OpenTelemetry error. Error status is
reserved for operational failures such as exceptions, timeouts, protocol failures or unavailable tools;
failed semantic spans record the exception and a stable error.type.
Agent metrics
The existing OpenTelemetry metrics pipeline exports these instruments from the BrewUp.Agent meter:
| Metric | Purpose |
|---|---|
brewup.agent.runs |
Number of semantic agent runs |
brewup.agent.run.duration |
End-to-end run duration in seconds |
brewup.agent.handoffs |
Delegations from Mother to specialized agents |
brewup.agent.tool.calls |
Knowledge Agent tool executions |
Metric dimensions are intentionally low-cardinality (route, outcome, agent, tool). Run IDs,
trace IDs, conversation IDs, customer/product data, prompts and responses are never metric dimensions.
Telemetry privacy defaults
Custom telemetry does not record prompts, responses, tool arguments, tool results or customer/business
content by default. It records only structured operational facts such as route, outcome, agent/tool name,
success, evidence availability and result counts. Sensitive Microsoft.Extensions.AI telemetry remains
opt-in; OpenTelemetryChatClient.EnableSensitiveData is not enabled globally.
Technology Stack
- Runtime: .NET 10 (
net10.0) - API: ASP.NET Core Web API
- OpenAPI via Scalar
- Serilog (console + rolling file)
- OpenTelemetry tracing & metrics (OTLP + console; Azure Monitor exporter available)
- Explicit module composition‑root pattern
- Architecture
- DDD with explicit domain and application layers
- Modular monolith, grouped by bounded context, with MCP servers split out per module
- CQRS and event sourcing powered by Muflone 10.x
- .NET Aspire (
Aspire.AppHost.Sdk13.x) for orchestration
- Persistence & Messaging
- MongoDB – read‑model document store
- KurrentDB (EventStoreDB) – event store for domain events and sagas
- RabbitMQ 4.x with Stream plugin, or Azure Service Bus – message bus
- SQL Server – Knowledge chunks and vectors
- AI
ModelContextProtocol.AspNetCorefor the MCP servers,ModelContextProtocol.Clientfor the poolMicrosoft.Extensions.AIfunction calling over Azure OpenAIMicrosoft.Agents.AIfor the Knowledge agent; A2A agent card + task contract- Azure OpenAI embeddings; SQL Server or Azure AI Search as vector store
- Web UI: Blazor (
net10.0) - Mobile: .NET MAUI (Android / iOS / Windows)
Check individual
*.csprojfiles and the compose files for exact package versions.
Getting Started
Prerequisites
- .NET 10 SDK
- Docker Desktop — for backing services and for the MCP / agent images
- Aspire CLI — optional, only if you prefer
aspire runoverdotnet runon the AppHost (see the Aspire docs for the current install command) - A recent IDE: Visual Studio 2022+, JetBrains Rider, or VS Code with C# Dev Kit
Optional: Bruno for the contracts in BrewDocs/, and the Azure CLI for the Bicep deployment.
1. Start the backing services
cd docker
.\run-docker-compose.bat
| Service | Container | Host port(s) |
|---|---|---|
| KurrentDB (EventStoreDB) | sales-eventstore |
4113 |
| MongoDB (sagas) | sagas-mongodb |
27017 |
| MongoDB (sales read model) | sales-mongodb |
37017 |
| RabbitMQ (+ stream plugin) | rabbitmq |
5672 AMQP · 5552 stream · 15672 management UI |
2. Building the container images
The four MCP servers and the Knowledge agent each have a Dockerfile next to their project. All of
them build from the src/BrewApi directory as context, because they restore sibling projects.
Create the environment file the containers read, then build every image in one go:
Copy-Item infra\Docker\env.example src\BrewApi\.env # then fill in the values
docker compose -f infra\Docker\mcp-docker-compose.yml --project-directory src\BrewApi build
--project-directory src\BrewApi is what makes the relative context: . and env_file: .env inside the
compose file resolve to src/BrewApi.
This produces exactly the image names the Aspire AppHost and the Bicep deployment expect:
| Image | Project | Dockerfile |
|---|---|---|
brewup.masterdata.mcpserver:latest |
BrewUp.MasterData.McpServer |
MasterData/BrewUp.MasterData.McpServer/Dockerfile |
brewup.sales.mcpserver:latest |
BrewUp.Sales.McpServer |
Sales/BrewUp.Sales.McpServer/Dockerfile |
brewup.warehouse.mcpserver:latest |
BrewUp.Warehouse.McpServer |
Warehouse/BrewUp.Warehouse.McpServer/Dockerfile |
brewup.knowledge.mcpserver:latest |
BrewUp.Knowledge.McpServer |
Knowledge/BrewUp.Knowledge.McpServer/Dockerfile |
brewup.knowledge.agent:latest |
BrewUp.Knowledge.Agent |
Knowledge/BrewUp.Knowledge.Agent/Dockerfile |
To build a single image instead:
docker build -t brewup.sales.mcpserver:latest `
-f Sales\BrewUp.Sales.McpServer\Dockerfile src\BrewApi
3. Run everything with Aspire
Set the AppHost secrets once (values live in user secrets, never in source):
cd src\BrewApi\BrewOrchestrator.Host
dotnet user-secrets set "Parameters:mongo-connection-string" ""
dotnet user-secrets set "Parameters:sqlserver-connection-string" ""
dotnet user-secrets set "Parameters:servicebus-connection-string" ""
dotnet user-secrets set "Parameters:azure-openai-api-key" ""
# …and the remaining Parameters:* keys declared in Parameters/BrewUpParameters.cs
SetBrewUpEnvironment.ps1 in the same folder is a starting point for scripting this.
Then start the AppHost, either from src/BrewApi:
aspire run
or directly:
dotnet run --project src\BrewApi\BrewOrchestrator.Host\BrewOrchestrator.Host.csproj
The dashboard link is printed in the console; from there you get every resource’s state, logs, traces and metrics, including the MCP containers and the A2A hop.
4. Or run the MCP servers and agent with Compose
docker compose -f infra\Docker\mcp-docker-compose.yml --project-directory src\BrewApi up -d
There is also a scaled‑out variant that puts Traefik in front of N Knowledge MCP replicas — useful for showing that a stateless MCP server load‑balances:
docker compose -f infra\Docker\mcp-docker-compose-scalable.yml --project-directory src\BrewApi `
up -d --build --scale knowledge-mcp=3
The MCP endpoint then moves to the gateway (http://localhost:8081/mcp), with the Traefik dashboard on
http://localhost:8090. See infra/Docker/RunMcpKnowledgeScalable.ps1 for the full command set.
5. Or run projects individually
cd src\BrewApi
dotnet run --project BrewUp.Rest\BrewUp.Rest.csproj
The repository root also has convenience scripts: RunSalesMcpServer.bat,
RunWarehouseMcpServer.bat, RunMasterDataMcpServer.bat, RunKnowledgeMcpServer.bat,
RunKnowledgeAgent.bat.
Note that dotnet run uses each project’s launchSettings.json port, not the container port:
| Service | Aspire / Compose port | dotnet run (HTTP) |
|---|---|---|
| Knowledge Agent (A2A) | 8080 |
5005 |
| Knowledge MCP | 8081 |
5236 |
| MasterData MCP | 8082 |
5007 |
| Warehouse MCP | 8083 |
5279 |
| Sales MCP | 8084 |
5229 |
BrewUp.Rest |
assigned by Aspire | 5094 / 6094, HTTPS 7289 |
BrewSpa |
— | 5156, HTTPS 7131 |
When you run the servers with dotnet run, point BrewUp:McpServers:* and
BrewUp:Mother:A2A:KnowledgeAgentUrl at the launchSettings ports instead of the container ones.
6. Web front‑end and mobile app
cd src\BrewSpa\BrewSpa
dotnet run
For the MAUI app, open src/BrewApp/mobile/src in Visual Studio 2022+ or Rider, pick a target
(Android / iOS / Windows) and run from the IDE.
Configuration
BrewUp.Rest/appsettings.json (plus appsettings.Development.json) holds the API configuration; the
containers read the same keys as double‑underscore environment variables (see infra/Docker/env.example).
"BrewUp": {
"MongoDbSettings": { "ConnectionString": "...", "DatabaseName": "BrewUp" },
"EventStore": { "ConnectionString": "esdb://localhost:4113?tls=false" },
"RabbitMQ": { "Host": "localhost", "Username": "guest", "Password": "guest", "UseRMQ": true },
"AzureServiceBus": { "ConnectionString": "...", "TopicName": "brewup", "UseAzureServiceBus": false },
"SqlServer": { "ConnectionString": "...", "Dimensions": 1536 },
"McpServers": {
"MasterDataUrl": "http://localhost:8082/mcp",
"WarehouseUrl": "http://localhost:8083/mcp",
"SalesUrl": "http://localhost:8084/mcp",
"KnowledgeUrl": "http://localhost:8081/mcp"
},
"AzureOpenAI": { "Endpoint": "...", "DeploymentName": "...", "ApiKey": "...", "UseManagedIdentity": false },
"Embeddings": { "Endpoint": "...", "DeploymentName": "text-embedding-3-small", "Dimensions": 1536 },
"FoundryLimits": { "MaxConcurrentRequests": 1, "RequestsPerMinute": 6, "MaxOutputTokens": 600 },
"Mother": { "A2A": { "Enabled": true, "KnowledgeAgentUrl": "http://localhost:8080" } }
},
"Knowledge": { "VectorStore": "SqlServer" }
The Knowledge agent has its own small section:
"KnowledgeAgent": { "Mcp": { "ServerName": "knowledge", "Endpoint": "http://localhost:8081/mcp", "DefaultTopK": 5 } }
Use user secrets or environment variables for connection strings and API keys — do not commit them.
Using the MCP servers from an MCP client
Every server is a normal MCP endpoint, so you can attach VS Code, Claude Code or any other client
directly. Each MCP project already ships a .vscode/mcp.json, for example:
{
"servers": {
"brewup": { "type": "http", "url": "http://localhost:5236/mcp" }
}
}
Swap the URL for http://localhost:8081/mcp (or the relevant 808x port) when the servers run in
containers.
Azure deployment
infra/Bicep deploys the AI layer to Azure Container Apps: a container registry, a Log Analytics
workspace, a Container Apps environment, and one public Container App per MCP server plus the Knowledge
agent. It deliberately leaves SQL Server, Azure OpenAI, Azure AI Search and the REST API out of scope.
The flow is: build the images (step 2) → az acr login → tag and push → deploy the Container Apps with
the connection strings passed as parameters. See infra/Bicep/README.md for the
exact commands and the .env‑key → Bicep‑parameter mapping.
Tests
| Project | Scope |
|---|---|
BrewUp.Rest.Tests |
API integration & architecture tests, Mother coordinator tests |
BrewUp.Shared.Tests |
Shared utilities and A2A contracts |
MasterData/BrewUp.MasterData.Tests |
MasterData domain & architecture |
Sales/BrewUp.Sales.Tests |
Sales domain |
Purchases/BrewUp.Purchases.Tests |
Purchases domain |
Warehouse/BrewUp.Warehouse.Tests |
Warehouse domain |
Knowledge/BrewUp.Knowledge.Tests |
Ingestion, chunking, PDF extraction, search, SQL Server & Azure AI Search vector stores, governance |
Dashboards/BrewUp.Dashboards.Tests |
Dashboards |
Sagas/BrewUp.Sagas.Tests |
Sagas |
BrewSpa/Dashboards/BrewSpa.Dashboards.Tests |
Blazor dashboards |
BrewApp/mobile/tests/ |
Mobile unit / integration |
cd src\BrewApi
dotnet test
Architectural Notes
DDD & layers
- Domain layer — business rules, invariants and ubiquitous language; aggregates, value objects and domain events; pure C# with no infrastructure dependencies.
- Application / Facade layer — use cases, orchestration between domain, read models and infrastructure; also where in‑process agents live.
- Infrastructure layer — persistence, messaging, embeddings and external services.
Modular monolith, modular AI
- Each bounded context lives in its own folder tree and assemblies.
- Cross‑context communication goes through explicit contracts and messages
(
BrewUp.Shared/Messages/) rather than shared tables or implicit coupling. - Shared code goes into
BrewUp.Shared(truly cross‑cutting) or*.SharedKernel(single context). - The AI surface follows the same seam: one MCP server per module, reading only that module’s read model. A module’s tools can be deployed, versioned and scaled without touching the monolith — and the Knowledge module shows the next step up, wrapping its MCP server in an A2A agent with a published agent card.
Functional‑style modelling
While written in C#, many domain and application components are built with a functional mindset: immutable value objects, clear input/output contracts for use cases, and IO pushed to the edges.
Roadmap & Status
- Complete end‑to‑end flows for all bounded contexts
- MCP servers for the remaining modules (Purchases, Dashboards)
- Expand test coverage (unit, integration and contract tests)
- Broaden the A2A surface beyond the Knowledge module
- Harden observability (metrics dashboards, trace‑based assertions)
Contributions in the form of issue reports, ideas and pull requests are welcome, but please treat this as a learning/reference project rather than production‑ready software.
License
See the LICENSE file at the root of the repository for the full license text.
Install
This server does not publish a one-line install command.
Open the repository installation guide New to MCP? Start with the mcpskill.org guide →Configuration
"BrewUp": {
"MongoDbSettings": { "ConnectionString": "...", "DatabaseName": "BrewUp" },
"EventStore": { "ConnectionString": "esdb://localhost:4113?tls=false" },
"RabbitMQ": { "Host": "localhost", "Username": "guest", "Password": "guest", "UseRMQ": true },
"AzureServiceBus": { "ConnectionString": "...", "TopicName": "brewup", "UseAzureServiceBus": false },
"SqlServer": { "ConnectionString": "...", "Dimensions": 1536 },
"McpServers": {
"MasterDataUrl": "http://localhost:8082/mcp",
"WarehouseUrl": "http://localhost:8083/mcp",
"SalesUrl": "http://localhost:8084/mcp",
"KnowledgeUrl": "http://localhost:8081/mcp"
},
"AzureOpenAI": { "Endpoint": "...", "DeploymentName": "...", "ApiKey": "...", "UseManagedIdentity": false },
"Embeddings": { "Endpoint": "...", "DeploymentName": "text-embedding-3-small", "Dimensions": 1536 },
"FoundryLimits": { "MaxConcurrentRequests": 1, "RequestsPerMinute": 6, "MaxOutputTokens": 600 },
"Mother": { "A2A": { "Enabled": true, "KnowledgeAgentUrl": "http://localhost:8080" } }
},
"Knowledge": { "VectorStore": "SqlServer" }