MCP API Reference
Reference for the FlowPOS MCP server in apps/backend/src/mcp/.
This page documents current behavior from source code (controllers, guards, services, and tool definitions), including:
- HTTP endpoints
- Auth and token flows
- Session lifecycle
- Runtime tool catalog and visibility rules
The current runtime catalog registers 35 tools. If an AI client shows a smaller list, start with the visibility and scope rules below before assuming a registration bug.
Architecture map (hexagonal)
domain/
mcp-principal.interface.ts
mcp-tool.interface.ts
mcp-api-key-repository.domain.ts
application/
mcp-api-key.service.ts
mcp-token.service.ts
mcp-session.service.ts
mcp-tools-initializer.service.ts
infrastructure/
mcp-api-key.repository.ts
mcp-prompts.handler.ts
mcp-resources.handler.ts
interfaces/
mcp.controller.ts
mcp-token.controller.ts
mcp-api-key.controller.ts
mcp-well-known.controller.ts
guards/
Infrastructure handles HTTP, Redis, and DB details. Domain defines the principal/tool contracts and security errors; application services implement token exchange, session state, and tool registration.
Authentication modes
FlowPOS supports two runtime credential types on the same /mcp transport. Both are sent as Authorization: Bearer ...; McpAuthGuard tries API key validation first and MCP JWT validation second.
| Mode | Best for | Bearer value | Principal source | Session state |
|---|---|---|---|---|
| V1 API key | internal automations, server-to-server scripts | fp_mcp_<token> | mcp_api_key metadata | in-memory transport only |
| V2 MCP JWT | merchant-facing assistants | JWT from POST /mcp/token | Firebase user -> active business_user memberships | in-memory transport + Redis principal mirror |
V1: API key
- Key management endpoints:
/mcp/keys/* - Runtime auth:
Authorization: Bearer fp_mcp_<token> - Key is validated by SHA-256 hash lookup in
mcp_api_key - Principal comes from DB key metadata:
rolebusinessId->activeBusinessIdscopescreatedBy->userId(nullable)
V2: Firebase -> MCP JWT
- Exchange endpoint:
POST /mcp/token - Refresh endpoint:
POST /mcp/token/refresh - Runtime auth:
Authorization: Bearer <jwt> - Role is always
merchantfor V2 tokens - Default token TTL is 24 hours (
MCP_TOKEN_TTL_SECONDSoverrides it) - Refresh accepts recently expired tokens for up to 7 days and re-resolves memberships from the database
- Backend startup fails if
MCP_TOKEN_SECRETis missing - Scopes are fixed by token service:
pos:readpos:writereports:readpsa:readpos:intents
Guard chain on /mcp
McpAuthGuard attempts:
McpApiKeyGuardMcpTokenGuard
If both fail -> 401 Unauthorized.
Multi-business behavior
V2 tokens include every active business membership for the Firebase user. The first membership becomes activeBusinessId; multi-business users can call set_active_business when that tool is visible. Refresh preserves the previous active business if it is still authorized.
API-key sessions are scoped to the key's businessId, so they do not receive set_active_business.
Discovery metadata and token exchange
McpWellKnownController publishes:
GET /.well-known/oauth-authorization-serverGET /.well-known/oauth-protected-resource
The current metadata is intentionally minimal. It identifies /mcp as the protected resource and points token-capable clients to the FlowPOS Firebase-to-MCP exchange at POST /mcp/token.
Do not assume a standard authorization-code + PKCE flow exists in the backend today:
- there is no current
/mcp/oauth/authorizeendpoint - there is no current
/mcp/oauth/tokenendpoint mcp-oauth-discovery-designis a historical/proposed design document, not the runtime contract
For AI clients that cannot supply a Firebase ID token, use the V1 API-key flow until a full OAuth controller is implemented.
Endpoint reference
POST /mcp/keys
Create a new MCP API key.
- Auth: Firebase app auth (global
AuthGuard) - Controller:
McpApiKeyController - Raw key is returned once (
rawKey) and cannot be recovered platform_operatorkey creation is rejected by service
Request body:
{
"businessId": "uuid",
"role": "merchant",
"scopes": ["pos:read", "reports:read"],
"label": "My key",
"expiresAt": "2027-01-01T00:00:00.000Z"
}
Notes:
businessIdis effectively required for this endpoint- Allowed roles in DTO include
platform_operator, but service forbids it rawKeyis returned only in the create response; store it immediately
GET /mcp/keys?businessId=<uuid>
List key metadata for a business.
- Auth: Firebase app auth
- Does not return key hash or raw key
PATCH /mcp/keys/:id/revoke
Soft revoke key (isActive = false).
- Auth: Firebase app auth
- Effect is immediate for MCP auth
DELETE /mcp/keys/:id
Hard delete key record.
- Auth: Firebase app auth
- Response:
204
POST /mcp/token
Exchange Firebase ID token for MCP JWT.
- Public endpoint (
@IsPublic) - Validates Firebase token
- Resolves internal user and active business memberships
- Returns MCP access token and business metadata
Request:
{ "firebaseIdToken": "<firebase-id-token>" }
Response includes the JWT plus the active and authorized business context:
{
"accessToken": "<mcp-jwt>",
"expiresIn": 86400,
"role": "merchant",
"activeBusinessId": "<business-uuid>",
"authorizedBusinessIds": ["<business-uuid>"],
"authorizedBusinesses": [{ "id": "<business-uuid>", "name": "Acme Store" }],
"scopes": ["pos:read", "pos:write", "reports:read", "psa:read", "pos:intents"]
}
Common errors:
401: invalid/expired Firebase token403: no active business memberships
POST /mcp/token/refresh
Refresh MCP JWT without requiring Firebase token.
- Public endpoint
- Accepts valid or expired JWT
- Expired token is refreshable for up to 7 days
- Memberships are reloaded from DB during refresh
Request:
{ "token": "<mcp-jwt>" }
Common errors:
401: invalid signature/claims401: expired beyond 7-day grace window
GET /.well-known/oauth-authorization-server
OAuth Authorization Server Metadata (RFC 8414).
GET /.well-known/oauth-protected-resource
OAuth Protected Resource Metadata (RFC 9728).
POST /mcp
Primary MCP JSON-RPC endpoint.
- Auth:
McpAuthGuard(API key or MCP JWT) - First request must be
initialize - On initialize success, response includes
mcp-session-idheader - Non-initialize requests require
mcp-session-id
Error patterns:
400: missingmcp-session-idon non-initialize request400: unknown session id401: invalid credential
Minimal initialize request:
curl -i -X POST "http://localhost:4000/mcp" \
-H "Authorization: Bearer <mcp-jwt-or-fp_mcp_key>" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-03-26",
"capabilities": {},
"clientInfo": { "name": "local-dev", "version": "0.1.0" }
}
}'
Save the mcp-session-id response header and send it on later requests:
curl -s -X POST "http://localhost:4000/mcp" \
-H "Authorization: Bearer <mcp-jwt-or-fp_mcp_key>" \
-H "mcp-session-id: <session-id-from-initialize>" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "get_active_business",
"arguments": {}
}
}'
GET /mcp
Open SSE stream for existing session.
- Requires both:
Authorizationmcp-session-id
DELETE /mcp
Close existing session.
- Requires both:
Authorizationmcp-session-id
Session lifecycle and state
Source: mcp-session.service.ts
- The first
POST /mcpmust be JSON-RPCinitializeand must not require an existing session. - The successful initialize response includes
mcp-session-id. - Tool calls,
GET /mcp, andDELETE /mcpmust include the samemcp-session-id. DELETE /mcpor transport close removes the in-memory session.
For V2 MCP JWT sessions, principal is additionally stored in Redis key:
mcp:session:{sessionId}
Important constraint:
- Redis stores principal state, not transport
- Session transport remains in-memory only
- Sessions do not survive instance restart
GET /mcpandDELETE /mcprequire the in-memory session; Redis cannot restore the streamable transport by itself- after deploys, restarts, or Cloud Run routing changes, clients should re-initialize and use the new
mcp-session-id
Context tools (set_active_business, set_active_tenant) update the session principal. For V2 MCP JWT sessions, the Redis principal mirror is updated too; memberships, API keys, and Firebase claims are not modified.
Runtime tool catalog (35 tools)
The runtime tool list is built by McpToolsInitializer.
Tool descriptions in source are written as instructions for AI clients. Keep this table aligned with the tool factories under apps/backend/src/mcp/tools/ when adding or renaming tools.
For implementation guidance and scope/visibility checklists, see MCP Tool Authoring Guide.
Maintaining tool descriptions
Tool descriptions are public interface copy for AI clients. Keep them precise, imperative, and source-aligned:
- tool name, description, input schema, and scope requirements live in each tool factory under
apps/backend/src/mcp/tools/* McpToolsInitializeris the registration list; a factory that is not registered there is not visible at runtime- visibility flags (
operatorOnly,multiBusinessOnly,skipTenantCheck) are part of the domain contract inMcpToolDefinition - prompt and resource copy is shared from
packages/global/consts/mcp-capabilities.consts.tsand registered byMcpPromptsHandler/McpResourcesHandler
When adding or renaming a tool:
- Add or update the tool factory, including Zod input descriptions.
- Register it in
McpToolsInitializer. - Assign the narrowest required scopes and visibility flags.
- Add or update registry/tool tests for visibility and handler behavior.
- Update this table and any related runbook entry.
Platform and context tools (5)
| Tool | When to use | Scopes / visibility |
|---|---|---|
list_tenants | Platform operator lists registered businesses before selecting a tenant. | operator only |
set_active_tenant | Platform operator switches tenant context for later domain calls. | operator only, no tenant check |
set_active_business | Multi-business merchant switches active business within one session. | visible only when authorizedBusinessIds.length > 1 |
get_active_business | AI client or developer orients itself with active business metadata and locations. | pos:read |
list_locations | Resolve location UUIDs returned by inventory and sales tools. | pos:read |
Commerce and reporting tools (15)
| Tool | Purpose | Scopes |
|---|---|---|
get_products | Paginated product listing with optional filters | pos:read |
search_products | Fast lookup by name/SKU/barcode | pos:read |
get_inventory | Inventory snapshot per product/location | pos:read |
get_low_stock_alerts | Flat low-stock list by reorder threshold | pos:read |
list_orders | Paginated restaurant order listing | pos:read |
get_order | Restaurant order detail by ID | pos:read |
create_order | Create restaurant order | pos:write |
void_transaction | Void retail sale transaction | pos:write |
list_sales | Paginated retail sales listing | pos:read |
get_sale | Retail sale detail by ID | pos:read |
list_purchases | Paginated purchase listing | pos:read |
get_purchase | Purchase detail by ID | pos:read |
get_sales_report | Raw date-range totals (revenue, count, AOV) | reports:read |
get_top_products | Top-selling products by revenue | reports:read |
get_sales_by_location | Revenue breakdown by location | reports:read |
FEL tools (7)
| Tool | Purpose | Scopes |
|---|---|---|
get_taxpayer_info | NIT lookup via FEL shared info | pos:read |
list_credit_notes | Paginated credit note listing | pos:read |
get_credit_note | Credit note detail by ID | pos:read |
list_debit_notes | Paginated debit note listing | pos:read |
get_debit_note | Debit note detail by ID | pos:read |
list_cancellations | Paginated cancellation listing | pos:read |
get_cancellation | Cancellation detail by ID | pos:read |
Implementation Portal (PSA) tools (4)
| Tool | Purpose | Scopes |
|---|---|---|
list_implementations | List implementation boards | psa:read |
get_implementation | Full board tree (phases and steps) | psa:read |
log_hours | Log time entry on hourly steps | psa:read |
get_implementation_status | Completion/status summary for one board | psa:read |
log_hours uses the authenticated principal's userId. Only platform_operator principals may override userId; merchant/API-key callers need a resolvable principal user ID.
Intent tools (4)
| Tool | Purpose | Scopes |
|---|---|---|
summarize_day | Daily business narrative summary | pos:intents |
summarize_period | Multi-day summary with prior-period comparison | pos:intents |
get_inventory_health | Prioritized low-stock/out-of-stock health report | pos:intents |
get_client_health | PSA board health summary with hours and blockers | pos:intents, psa:read |
Tool argument and behavior quick reference
Use this section when debugging AI-client calls or changing tool descriptions in source. Argument names are the public contract exposed through each tool's Zod input schema.
Platform and context
| Tool | Key arguments | Defaults and constraints |
|---|---|---|
list_tenants | page, size | operator only; default page=1, size=20; size max 100 |
set_active_tenant | businessId | operator only; requires a UUID; updates only the current MCP session principal |
set_active_business | businessId | visible only for multi-business principals; businessId must already be in authorizedBusinessIds; updates only the current MCP session principal |
get_active_business | none | returns business id/name/country and up to 100 locations for activeBusinessId |
list_locations | page, limit | default page=1, limit=50; limit max 100 |
Products, inventory, orders, sales, and purchases
| Tool | Key arguments | Defaults and constraints |
|---|---|---|
get_products | search, categoryId, page, limit | default page=1, limit=20; limit max 100; category filter requires UUID |
search_products | query, limit | query is required; default limit=10; limit max 50; use get_products for paginated/category browsing |
get_inventory | locationId, page, limit | default page=1, limit=100; limit max 200; use list_locations to resolve location UUIDs |
get_low_stock_alerts | page, limit | default page=1, limit=100; limit max 200; flat threshold list, not velocity-prioritized |
list_orders | locationId, page, limit | restaurant orders only; default page=1, limit=20; limit max 100 |
get_order | orderId | restaurant order detail by UUID; use get_sale for retail transactions |
create_order | locationId, employeeId, tableId, orderType | restaurant order creation; locationId and employeeId required; orderType defaults to dine_in and accepts dine_in, takeout, delivery |
void_transaction | transactionId, confirm | retail sale void only; omitted confirm returns a preview; confirm: true executes the irreversible void |
list_sales | page, limit, status, createdAtFrom, createdAtTo | retail sales only; default page=1, limit=20; date filters require ISO 8601 date-times |
get_sale | saleId | retail sale detail by UUID |
list_purchases | page, limit, status, createdAtFrom, createdAtTo | purchase orders / goods received notes; default page=1, limit=20; date filters require ISO 8601 date-times |
get_purchase | purchaseId | purchase order detail by UUID |
Reports and intent summaries
| Tool | Key arguments | Defaults and constraints |
|---|---|---|
get_sales_report | from, to, locationId | raw structured totals; default range is the last 24 hours; locationId optional |
get_top_products | from, to, limit | default range is the last 30 days; default limit=10; limit max 50 |
get_sales_by_location | from, to | default range is the last 30 days |
summarize_day | date, locationId | defaults to today; returns daily summary, yesterday comparison, top 3 products, and low-stock alert count; partial failures are returned in _partialErrors |
summarize_period | from, to | both dates are required ISO 8601 date-times; prior-period comparison uses an equal-length period immediately before from; partial failures are returned in _partialErrors |
get_inventory_health | locationId | returns up to 100 low-stock alerts prioritized with out-of-stock items first, then velocity score from 30-day top products; partial failures are returned in _partialErrors |
get_client_health | implementationId | requires pos:intents and psa:read; computes completion, current phase, hours burned/estimated, blocker count, and next milestone from the board tree |
FEL
| Tool | Key arguments | Defaults and constraints |
|---|---|---|
get_taxpayer_info | nit | required NIT; uses active business FEL certifier through FelService.getSharedInfo |
list_credit_notes | page, limit, status | default page=1, limit=20; limit max 100 |
get_credit_note | creditNoteId | credit note detail by UUID |
list_debit_notes | page, limit, status | default page=1, limit=20; limit max 100 |
get_debit_note | debitNoteId | debit note detail by UUID |
list_cancellations | page, limit, status | default page=1, limit=20; limit max 100 |
get_cancellation | cancellationId | cancellation detail by UUID |
Implementation Portal (PSA)
| Tool | Key arguments | Defaults and constraints |
|---|---|---|
list_implementations | page, limit, status | default page=1, limit=20; status accepts draft, active, completed, archived; total is currently returned as null and hasMore is inferred from page size |
get_implementation | boardId | full board tree by UUID; use only when step-level detail is needed |
get_implementation_status | boardId | lightweight status and completion count from the board tree |
log_hours | stepId, hours, description, date, userId | stepId and positive hours required; date is ISO 8601 and defaults to now; non-operators cannot override userId; current scope is psa:read despite write behavior |
Tenant scoping notes
- List and aggregate tools pass
principal.activeBusinessIdinto the owning service/repository call. - Context-switching tools only change the in-memory session principal and, for V2 sessions, the Redis principal mirror.
- Detail-by-ID tools delegate to the owning module's application service by ID. When tightening isolation or adding new detail tools, verify tenant scoping in the owning service rather than relying on MCP wrapper code alone.
- Tool schemas validate declared argument shapes before handlers run. Unknown
fields are stripped by default because
McpSessionServiceregisters each schema throughz.object(...).
Tool visibility rules
Visibility is filtered when a session is initialized through ToolRegistry.listFor.
The MCP HTTP path registers only the tools visible to that principal on the session's McpServer; calls then invoke each registered tool handler with the current session principal.
ToolRegistry.dispatch also contains secondary scope and tenant guards for direct registry callers and tests. Do not assume those guards are an extra runtime layer for the streamable HTTP MCP path unless the controller is changed to route calls through dispatch.
Key rules:
operatorOnlytools -> onlyplatform_operatormultiBusinessOnlytools -> only whenauthorizedBusinessIds.length > 1pos:intentstools:- require
pos:intentsscope - never visible to
tenant_developer
- require
- most non-operator tool logic expects an
activeBusinessId; context-switching tools useskipTenantCheck = true
Tool visibility is calculated at session initialization. If role, scopes, or business memberships change, create a new session so tools/list reflects the new principal.
Scope reference
pos:read: product/inventory/order/sale/purchase/FEL readspos:write:create_order,void_transactionreports:read: report toolspsa:read: PSA tools (list_implementations,log_hours, etc.)pos:intents: intent tools
Note on log_hours: write behavior is intentionally protected by psa:read in current implementation.
Prompt shortcuts (8)
McpPromptsHandler registers merchant-facing prompt shortcuts on every session. Prompts are instructions for the AI client; they do not bypass tool visibility or scopes.
| Prompt | Typical use | Tool path it asks the client to follow |
|---|---|---|
daily_briefing | Daily store-owner summary | summarize_day, then get_low_stock_alerts when alerts exist |
end_of_day_report | Closing report | summarize_day, get_top_products, get_inventory_health |
weekly_review | Week-over-week performance | summarize_period for target and prior week |
monthly_performance | Month-over-month performance | summarize_period for target and prior month, plus get_top_products |
low_stock_check | Current inventory risk | get_inventory_health, get_low_stock_alerts |
top_sellers | Best-selling products for a period | get_top_products with calculated date range |
restock_plan | Suggested replenishment actions | get_inventory_health, get_low_stock_alerts, get_top_products |
sales_by_location | Location comparison | get_active_business, get_sales_by_location, get_sales_report |
Prompt arguments are optional strings parsed by the prompt handler, such as date, locationId, period, limit, month, weekOffset, and daysToProject.
Resources (5)
McpResourcesHandler registers static documentation resources and dynamic resource templates. Dynamic resources return instructions telling the client which tools to call for live data.
| Resource URI | Type | Purpose |
|---|---|---|
flowpos://docs/quick-start | static markdown | Assistant capabilities and prompt shortcut overview |
flowpos://docs/prompt-guide | static markdown | Detailed reference for the 8 prompts |
flowpos://docs/currency-guide | static markdown | Guatemala business context, GTQ formatting, FEL terms, UTC-6 |
flowpos://business/profile | dynamic template | Live business and location context; hydrate with get_active_business |
flowpos://inventory/low-stock | dynamic template | Live low-stock snapshot; hydrate with get_inventory_health and get_low_stock_alerts |