§ 05 — MCP service

One process, eleven endpoints.

A separate .NET process that exposes every external integration as an MCP endpoint. Some endpoints proxy upstream MCP servers (vBox, Jira). Most are native — the service implements the tools itself, talking to Microsoft Graph, Tavily, az CLI, our own database, and the file system.

Why a separate service

Process boundary
The MCP service can crash, deadlock, or hang on a third-party API without affecting the main agent loop. The API container retries and surfaces a tool error; the user keeps chatting.
Different cadence
Adding a new MCP tool ships independently of API rebuilds.
Smaller footprint
The MCP image doesn't carry EF Core migrations, the Langfuse client, or the Azure OpenTelemetry exporters. Just the MCP SDK and the few HTTP clients we need.
Shared DB only
The MCP service reads from the same Postgres for token lookups (integration_tokens) but otherwise has its own isolated state.

On-disk layout

mcp-service/
├── backend/src/McpService/
│   ├── Program.cs                — endpoint registration, DI, MCP framework wiring
│   ├── Data/
│   │   ├── IIntegrationTokenStore.cs
│   │   ├── IntegrationTokenStore.cs   — encrypted token reads
│   │   ├── TokenDecryptor.cs          — shared key with API
│   │   └── *TokenRefresher.cs         — vBox, Jira, msgraph, sharepoint
│   ├── Health/HealthEndpoint.cs       — /health (90 s startup grace)
│   ├── Infrastructure/
│   │   ├── McpServerAttribute.cs      — [McpServer("Name")]
│   │   ├── McpServerToolFilter.cs     — server-name → tool subset
│   │   └── LenientBoolConverter.cs
│   ├── Proxy/                         — vBox + Jira upstream proxies
│   ├── Servers/
│   │   ├── Azure/                     — AzureToolFactory, az credential store
│   │   ├── PriceCalculator/           — pricing data loader, calculator
│   │   ├── WebSearch/                 — Tavily wrapper
│   │   ├── Schedule/                  — job scheduler tools
│   │   ├── TeamsMeetings/             — Graph calendar / meetings / transcripts
│   │   ├── Outlook/                   — Graph mail
│   │   ├── Documents/                 — DOCX / XLSX / PPTX / PDF read
│   │   ├── SharePointSearch/          — Graph search
│   │   └── ProjectPlan/               — project-plan tools
│   ├── Services/                      — cross-cutting
│   ├── Testing/                       — mock services for test-mode
│   └── VBox/                          — vBox-specific token refresh
└── infra/docker/Dockerfile.mcp

Endpoint registration

mcp-service/backend/src/McpService/Program.cs

Each app.MapMcp("/mcp/<name>") call mounts a new MCP endpoint on the same HTTP host (:8001). The server-name filter (McpServerToolFilter) picks which subset of tools is visible at each path.

Tool filtering by attribute

Native tool classes are decorated with [McpServer("Name")]. The classes' methods are decorated with [McpServerTool]. At startup the framework scans the assembly and builds a frozen { toolName → serverName } map.

When a request hits /mcp/<name>, the ConfigureSessionOptions callback inspects the URL path, looks up the server name in PathToServerMap, and dynamically populates McpServerOptions.ToolCollection with only the tools whose attribute matches. The result: one binary, one process, eleven distinct tool surfaces.

Health

/health returns 200 OK once the service is ready. The Docker compose health check uses a 90-second startup grace period because token-refresh probes on cold start can be slow.

Token store

The MCP service does not own user tokens; it reads them from the API's database (integration_tokens table) via IntegrationTokenStore. The shared TOKEN_ENCRYPTION_KEY env var lets it decrypt entries the API encrypted.

For RequiresUserContext calls the API forwards X-User-Id; the store looks up that user's Outlook / Jira / SharePoint token, refreshes it if expired, and uses it on the upstream HTTP call.

Where to dive