MCP Server Architecture and Tool Surface
This page is superseded by
dev/mcp/api-reference, which is updated first and tracks the latest implementation details.
Source-backed reference for apps/backend/src/mcp/.
This page remains as historical context. For the canonical and continuously maintained source-backed details, use dev/mcp/api-reference.
What the MCP module does
The MCP module exposes FlowPOS use cases through Model Context Protocol with:
- HTTP entrypoint (
POST /mcp) for initialize and tool calls - optional SSE stream (
GET /mcp) for MCP clients that request streaming - session cleanup (
DELETE /mcp) - dual authentication (V1 API keys + V2 JWT access tokens)
- per-principal tool filtering and call-time guard enforcement
Key entrypoints:
mcp.module.tsinterfaces/mcp.controller.tsapplication/mcp-session.service.tsregistry/tool-registry.service.tsapplication/mcp-tools-initializer.service.ts
Hexagonal mapping
| Layer | MCP files | Responsibility |
|---|---|---|
| Domain | domain/mcp-tool.interface.ts, domain/mcp-principal.interface.ts | Tool contract, principal contract, security errors |
| Application | mcp-session.service.ts, mcp-token.service.ts, mcp-api-key.service.ts, mcp-tools-initializer.service.ts | Session lifecycle, token issuance/refresh, key lifecycle, tool wiring |
| Infrastructure | mcp-api-key.repository.ts, mcp-prompts.handler.ts, mcp-resources.handler.ts | DB-backed key storage, prompt/resource registration |
| Interfaces | mcp.controller.ts, mcp-token.controller.ts, mcp-api-key.controller.ts, .well-known controller, guards | HTTP transport and authentication adapters |
The domain layer defines access semantics; infrastructure implements storage and MCP SDK integration.
Authentication flows
McpAuthGuard is an OR-guard:
McpApiKeyGuard(Authorization: Bearer fp_mcp_...)- fallback
McpTokenGuard(Authorization: Bearer <FlowPOS MCP JWT>)
If both fail, MCP returns 401 Authentication required: provide a valid MCP API key or access token.
V1: API keys
Managed via authenticated REST endpoints:
POST /mcp/keysGET /mcp/keys?businessId=...PATCH /mcp/keys/:id/revokeDELETE /mcp/keys/:id
rawKey is returned once on create and never recoverable.
V2: token exchange and refresh
POST /mcp/token: Firebase ID token -> MCP JWTPOST /mcp/token/refresh: refresh MCP JWT (valid and recently-expired tokens supported)
Important behavior from mcp-token.service.ts:
- default TTL is
86400seconds - refresh grace window is 7 days after expiry
- memberships are reloaded from DB during refresh
- default scopes in issued JWT:
pos:readpos:writereports:readpsa:readpos:intents
Session workflow
The session flow is strict:
- First request to
POST /mcpmust be JSON-RPCinitializewithoutmcp-session-id. - Server returns
mcp-session-idresponse header. - All subsequent
POST /mcp,GET /mcp, andDELETE /mcprequests must include:Authorizationmcp-session-id
McpSessionService stores session transport + principal in memory.
For OAuth/JWT sessions, principal state is also stored in Redis under mcp:session:{sessionId} with token-aligned TTL. This lets context updates (for example set_active_business) persist across subsequent requests within the same session lifetime.
Tool visibility and enforcement model
Layer 1: session-time visibility (ToolRegistry.listFor)
Tools are hidden when:
operatorOnlyand principal is notplatform_operatormultiBusinessOnlyand principal has <=1 authorized business- tool requires
pos:intentsand principal istenant_developer - principal lacks required scopes
Layer 2: call-time enforcement (ToolRegistry.dispatch)
Before executing handler:
- tool existence is validated
- scopes are revalidated
- active business is required unless tool is operator-only or marked
skipTenantCheck
This dual check prevents accidental exposure from transport-level errors.
Tool catalog (current)
The initializer currently registers 35 tools.
Platform tools (5)
| Tool | Scopes | Notes |
|---|---|---|
list_tenants | none | operator-only |
set_active_tenant | none | operator-only, skips tenant check |
set_active_business | none | multi-business only, skips tenant check |
get_active_business | pos:read | active business context + locations |
list_locations | pos:read | location IDs for active business |
Domain tools (26)
| Area | Tools | Scopes |
|---|---|---|
| Products | get_products, search_products | pos:read |
| Inventory | get_inventory, get_low_stock_alerts | pos:read |
| Orders | list_orders, get_order | pos:read |
| Orders write | create_order, void_transaction | pos:write |
| Sales | list_sales, get_sale | pos:read |
| Purchases | list_purchases, get_purchase | pos:read |
| Reports | get_sales_report, get_top_products, get_sales_by_location | reports:read |
| FEL | get_taxpayer_info, list_credit_notes, get_credit_note, list_debit_notes, get_debit_note, list_cancellations, get_cancellation | pos:read |
| Implementation portal | list_implementations, get_implementation, log_hours, get_implementation_status | psa:read |
Intent tools (4)
| Tool | Scopes |
|---|---|
summarize_day | pos:intents |
summarize_period | pos:intents |
get_inventory_health | pos:intents |
get_client_health | pos:intents, psa:read |
Prompts and resources
Session initialization also registers:
- 8 prompt shortcuts (
McpPromptsHandler) - 5 resources (
McpResourcesHandler: 3 static + 2 templates)
These are capability-level guidance surfaces; they do not bypass tool scope rules.
Operational constraints and pitfalls
GET /mcpandDELETE /mcpare authenticated (same guard as POST).- Redis stores principal state, not transport state; unknown in-memory session IDs still require re-initialize.
- Scope changes are reflected only in new sessions because tool visibility is calculated on initialize.
- In production,
MCP_TOKEN_SECRETmust be set (module startup fails if default secret is used in production mode).
For production triage, use:
dev/runbooks/mcp-session-auth-troubleshooting