| Server Actions (Next.js `"use server"`) | 1 file | AI settings form only |
| Service-layer exports (`src/lib/server/*-service.ts`) | ~350 across 40 files | The actual business logic |
The current coverage is **broad but uneven**. The bulk-write lifecycle (quotes → orders → PO → collection → delivery → invoice → stock) is well-covered on every layer. **Reads are skewed toward agent queries** (every collection has a `.list` / `.get` MCP tool), but **write coverage has a long tail of gaps** in domains that don't get daily agent traffic: repairs, the delivery board, the boss dashboard, the collection map, the stock quick-draft line editor, and the `price_list` / `raw_price_list` catalogue UI.
The biggest structural gap is the **MCP route file itself** — a single 7,077-line `route.ts` containing 142 tool definitions, 142 handlers, the JSON-RPC envelope, the CORS helper, the upstream-fetch wrapper, and the `TOOL_HINTS` map. This is blocking maintainability (already on the security-perf-prod audit as a major), and it's blocking discoverability for new tool authors.
## Methodology
1. Inventoried every page (`src/app/**/page.tsx`) — 53 pages.
2. Inventoried every `/api/v1/**/route.ts` — 165 route files.
3. Inventoried every tool in `src/app/api/mcp/route.ts` — 142 tools.
4. Inventoried every export from `src/lib/server/*-service.ts` (~40 services).
5. Inventoried every export from the legacy `src/lib/pb-helpers.ts` (~60 functions).
6. Inventoried Server Actions (`src/app/**/actions.ts`) — 1 file (settings/ai only).
7. Cross-referenced against `src/app/api/mcp/FEATURE-REQUESTS.md` to avoid duplicating already-known gaps.
8. Cross-referenced the MCP refactor recommendation at `docs/audit/backend-data.md:355-383` (Major finding 2.6).
The output is a capability→API→MCP matrix and a prioritized gap list with recommended slices for follow-up tasks.
| Add reply to thread | reply modal | `POST /quotes/append-reply` | `quotes.append_reply` | ✅ (route is `POST /quotes` with action, not `/append-reply` — see gap §1.4) |
| Upload attachment | sprint panel | `POST /sprint/items/[id]/attachments/upload` | `sprint.item_attachments` (POST variant) | 🟡 | MCP exposes the GET path; the POST upload is a different input shape — see §3.5 |
| Get single PO | inline | `GET /purchase-orders/[id]` | `purchase_orders.get` | ✅ |
| Create PO | server action | `POST /purchase-orders` | `stock_purchases.send` (different) | 🟡 | The agent uses `stock_purchases` for stock POs; **customer POs** go via server action with no MCP wrapper — see §1.11 |
- **Agent impact:** the approval queue (`/quotes/approvals`) and archive view are completely agent-invisible. Hermes can list but cannot act on approval decisions.
- **Recommended slice:** add two MCP wrappers, no schema changes.
- **Agent impact:** splitting an order line is a daily buyer workflow ("customer only wants 5 of the 10 we ordered"). The agent can do everything around the split but not the split itself.
- **Recommended slice:** thin wrapper, no schema changes.
#### 1.4 `quotes.append_reply` routed via the wrong endpoint
- **Severity:** now (deceptive)
- **Effort:** small
- **Issue:** MCP tool `quotes.append_reply` works, but the underlying REST is `POST /quotes` with an `action: "append_reply"` discriminator — not the natural `POST /quotes/append-reply`. The MCP wraps it, so Hermes doesn't see this, but anyone hand-calling REST hits a 404.
- **Recommended slice:** add `POST /api/v1/quotes/append-reply` as the canonical route, keep the legacy action as a back-compat alias.
- **Routes:** `/api/v1/deliveries`, `/load`, `/load-dn`, `/check-in`, `/mark-signed-copy`, plus the lookup helper
- **Agent impact:** the operator's `/deliveries` board (to_load → back_from_driver → checked_in) is the daily delivery flow; the agent can see orders but cannot act on the delivery lifecycle.
- **Recommended slice:** add `deliveries.list({tab})`, `deliveries.load({item_ids[]})`, `deliveries.load_dn({delivery_note_id})`, `deliveries.check_in({item_ids[], delivery_reference})`, `deliveries.mark_signed_copy({item_ids[]})`. These wrap the existing REST routes.
- **Agent impact:** the agent can't read or refresh AI intel on a specific item, forcing it to scan the full PB collection via the admin SDK (slow + PII risk).
- **Routes:** `/repairs` GET+POST, `/repairs/[id]` GET+PATCH+DELETE, `/repairs/[id]/files/[field]`, `/repairs/[id]/generate-delivery-note`, plus the form-data helper
- **Agent impact:** repair jobs are completely agent-invisible. The owner asked "anything in the app" → this is one of the seven operator-managed domains the agent cannot touch.
- **Effort:** small (route exists; missing wrapper)
- **Issue:** the `orders.create` tool creates orders, but **creating a customer purchase order** (the supplier-facing doc, distinct from the stock `stock_purchases` flow) is done via a server action on `/orders/[id]` and is not exposed via MCP.
- **REST surface:** no dedicated `/api/v1/purchase-orders` POST; the server action lives in the order detail component.
- **Recommended slice:** extract the PO creation into `POST /api/v1/purchase-orders` + `purchase_orders.create` MCP tool. Migrate the server action to call it.
#### 1.12 Three PO PDFs un-wrapped (customer + stock + stocktake)
- **Severity:** now (catalogue is the price-discovery backbone)
- **Effort:** medium
- **Routes needed:** `/pricelist/raw` GET+DELETE, `/pricelist/search`, `/pricelist/[sku]`, plus the upload/resync server actions need REST mirrors
- **Agent impact:** the agent can add items to the price list (`price_list.add_items`) but cannot search or read entries. Sprint automation depends on the price list search.
- **Agent impact:** the sprint auto-bucketing rule sets (`match_brands`, `match_supplier_tags`, `match_keywords`) live on categories. The agent can read items but cannot inspect or tune the routing rules.
- **Issue:** `files.fetch` returns base64; large files (10 MB+ pricelist PDFs, signed delivery copies) bloat the MCP payload. Hermes wants byte-stream download.
- **Recommended slice:** add a `files.stream({collection, recordId, filename})` that returns `{url, mime, expiresAt}` so the agent can `curl` directly.
#### 1.16 `invoicing.stock_book` MCP missing
- **Severity:** nice-to-have
- **Effort:** small
- **Route:** `POST /api/v1/invoicing/stock-book`
- **Agent impact:** accountant's "Book into stock" button is REST-only. The buyer agent can't run the workflow end-to-end.
#### 1.17 QuickBooks invoice payload has no MCP entry point
- **Severity:** now (invoicing is a daily operator workflow)
- **Effort:** large (architectural — the QB flow runs via n8n webhook, not /api/v1)
- **Issue:** the QB invoice is built and POSTed to n8n from the invoicing modal — there's no `/api/v1` route and no MCP tool. The agent can't drive the invoicing end-to-end.
- **Recommended slice:** add `/api/v1/invoicing/qb-invoice` (POST) + `invoicing.qb_invoice` MCP. Backend service: `qb-invoice-service.ts` mirroring the structure of `stock-purchase-invoice-matching-service.ts`.
### 2. MCP gaps per route (full list)
A few useful one-liner additions (severity: nice-to-have unless flagged otherwise):
### 3. MCP refactor recommendations (the 7,077-line `route.ts`)
The MCP route handler is the single biggest maintainability bottleneck in the agent layer. The approved `docs/audit/backend-data.md:355-383` calls for splitting each tool into `src/lib/server/mcp-tools/<name>.ts`. Building on that recommendation:
The `tools` array in `route.ts` reduces to `Object.entries(MCP_TOOLS).flatMap(([name, mod]) => [mod.tool])`; dispatch is `MCP_TOOLS[toolName]?.handle(...)`.
#### 3.2 Zod schema sharing with REST routes
Today the MCP layer hand-writes `inputSchema` while REST routes use `zod`. They drift (the `quote_items.create` whitelist drift was caught in Phase F; same risk exists for `quote_items.update` vs `bulk_update_quote_items` per FEATURE-REQUESTS.md).
Recommend: each MCP tool exports both `inputSchema` (JSON Schema for the MCP wire) AND a `zodSchema` for runtime validation. Use `zod-to-json-schema` to derive the JSON Schema from the zod schema. One source of truth per write operation.
#### 3.3 Service-layer dispatch table
Cross-reference: the FEATURE-REQUESTS "Standard error envelope with `hint`/`next_actions`/`allowed_tools`" entry. The 13 write tools migrated in Phase I carry a `TOOL_HINTS` map at `route.ts:266`; the remaining 33 `fetch + if (!res.ok)` blocks need the same migration. Per-tool file split makes this mechanical.
#### 3.4 Per-field widget route proliferation
`/api/v1/order-items/[id]/{qty,sku,cost,status,context}` and `/api/v1/stock-purchase-items/[id]/{qty,sku,cost}` exist as 4+3 separate PATCH routes because the operator popover posts one field at a time. This works but is awkward for an agent (which would batch). Recommend either:
- Document the existing `PATCH /order-items/[id]` (whitelist already exists) as the canonical "batch fields" endpoint for the agent; keep the per-field routes for the popover; OR
- Generate the per-field routes from a shared schema (e.g. `const PER_FIELD_ROUTES = { qty: { method: "PATCH", path: "/order-items/[id]/qty" }, ... }`).
#### 3.5 MCP tool consolidation for sprint attachments
`sprint.item_attachments` is one tool that takes either a GET or POST shape depending on context. Split into `sprint.get_attachments({id})` + `sprint.upload_attachment({id, file})` for clarity.
### 4. Architectural observations
#### 4.1 Customer PO vs stock PO model drift
The MCP separates `purchase_orders.*` (customer-side) from `stock_purchases.*` (warehouse-side). The duplicate-rest surface is fine for queries but the agent often wants a "PO" abstraction (the source PDF, the supplier reply, the lines, the email status). Consider a `purchase_orders.unified` MCP tool that returns both sides under a discriminated union — or just clearer cross-tool recipes in `docs/agents/workflows.md`.
#### 4.2 Server Actions ↔ REST surface split
Only one page (`/settings/ai`) currently uses Next.js Server Actions directly. The AGENTS.md `data-layer-discipline` rule says new endpoints belong in `/api/v1/*`. The pattern is healthy; the existing inventory confirms it's not being abused. No action needed.
#### 4.3 Auth model split
Three auth surfaces are in play:
-`/api/v1/*` — `assertAgentAuth` (bearer `AGENT_API_KEY`) OR `assertBrowserSession` (PB cookie)
-`/api/mcp` — JSON-RPC, bearer `AGENT_API_KEY`, CORS `*`
- Server Actions — PB cookie session via `(app)` layout
The audit (`docs/audit/security-perf-prod.md` Major #2) flags the bare `AGENT_API_KEY` as too weak + `/api/mcp` CORS `*`. When those are remediated, the MCP auth boundary should mirror the new contract.
#### 4.4 Streaming response types
`files.fetch` returns base64. PDF routes stream bytes. Delivery note PDF, stocktake PDF, picking slip, and PO PDF stream bytes via `NextResponse`. If the MCP layer is to drive document previews end-to-end, it needs a `files.stream` (see §1.15) that wraps the streaming routes.
#### 4.5 Invoicing flow architecture
The QuickBooks invoice pipeline runs through n8n, not `/api/v1`. Adding MCP coverage requires both a REST endpoint (`/api/v1/invoicing/qb-invoice`) and a service layer (`qb-invoice-service.ts`) that wraps the n8n webhook contract. This is the only domain where the agent has zero programmatic access to a daily operator workflow. Highest architectural priority.
## Recommended implementation slices
Slice these as separate tasks for the team-lead to assign. Each slice is sized to a single PR and ships behind a feature flag if desired.
| # | Slice | Severity | Effort | Tools added |
|---|---|---|---|---|
| S1 | Quote approval + archive MCP | now | small | 2 |
| S2 | Order lookup MCP + item split MCP | now | small | 2 |
| S3 | Collection map + bulk assign MCP | now | small | 2 |
| S4 | `/deliveries` MCP domain | now | medium | 5 |
| S5 | `order_items.intel` + `quote_items.intel` MCP | now | small | 3 |
| S6 | `repairs.*` MCP domain | now | medium | 7 |
| S7 | `delivery_notes.{get, pdf, signed_copy}` MCP | now | small | 3 |
| S8 | Customer PO creation MCP + 5 PDF MCP wrappers | now | medium | 6 |
| S9 | `pricelist.*` + `raw_price_list.*` MCP | now | medium | 7 |
| S10 | QuickBooks invoice MCP + service layer | now | large | 1 |
| S11 | MCP `route.ts` per-tool file split | major | large | refactor |
| S12 | Zod-schema sharing (MCP ↔ REST) | major | medium | refactor |
| S13 | Migrate remaining 33 `fetch + if (!res.ok)` blocks to `fetchUpstreamJson` | major | medium | refactor |
Total new tools if every slice ships: ~85 (S11/S12/S13/S18 are refactors, not additions). Brings the catalogue to ~225 tools.
## Cross-references to existing FEATURE-REQUESTS.md
This analysis **does not duplicate** the entries already tracked in `src/app/api/mcp/FEATURE-REQUESTS.md`. Open entries that this doc references but doesn't re-list:
- "Order items: clarify received" (Phase H partial, still open)
- "Order items: detect items missing selling_price" (Phase H partial)
1.**Scope of slice S10 (QuickBooks invoice MCP):** is the goal to mirror the n8n webhook in /api/v1 and call n8n from there, or to call the QB API directly? The current architecture routes through n8n for retries + audit; a direct QB integration would lose those.
2.**Scope of slice S11 (MCP route split):** is per-tool the target, or per-domain-folder? Per-tool gives 142 files; per-domain gives ~30 files each with 1-many tools. The approved backend-data.md audit calls for per-tool; per-domain is the practical mid-point.
3.**Auth: does the new MCP auth model (currently being designed for the security-perf-prod remediation) require an explicit `allowed_tools` ACL per agent?** If yes, that changes the per-tool file shape (each tool needs an `allowedRoles` manifest).
4.**Pricing model for the MCP "anything in the app" promise:** today the MCP is bearer-key-only. If the user wants the agent to drive every operator workflow, should the MCP accept the same PB cookie session as the UI? That would let `/api/mcp` proxy operator context for audit, instead of `AGENT_API_KEY` (which has no operator identity).