# MCP + API Layer Coverage Gap Analysis **Task:** #e169d78f **Date:** 2026-08-28 **Author:** @bob **Status:** analysis only — implementation NOT included (per task brief) ## Executive summary The WFDASH app already has a substantial programmatic surface: | Surface | Count | Scope | |---|---|---| | Pages (Server Components) | 53 | The full operator UI | | REST route handlers under `/api/v1/*` | 165 | Programmatic + agent + n8n integration | | MCP tools (registered in `src/app/api/mcp/route.ts`) | 142 | Agent-facing (Hermes, etc.) | | 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. ## Coverage matrix Legend: ✅ shipped · 🟡 partial · ❌ missing ### Quotes pipeline | Capability | Page | `/api/v1` route | MCP tool | Notes | |---|---|---|---|---| | List quotes (tabs) | `/quotes` | `GET /quotes/list` + `/quotes/search` | `quotes.list` | ✅ | | Get single quote | `/quotes/[id]` | `GET /quotes/[id]` | `quotes.get` | ✅ | | Create from email (n8n ingest) | webhook | `POST /quotes/from-email` | `quotes.create_from_email`, `quotes.ingest_from_email` | ✅ | | Create walk-in | `/quotes/[id]` "new" | `POST /quotes/create` | `quotes.create` | ✅ (Phase G) | | Update quote header | inline form | `PATCH /quotes/[id]` | `quotes.update` | ✅ | | Find by Message-ID | n8n callback | `GET /quotes/lookup?messageId=` | `quotes.find_by_message_id` | ✅ | | List templates | `/quotes` Templates tab | `GET /quotes/archive` | ❌ | MCP gap | | Save as template | detail page | `POST /quotes/[id]/save-as-template` | `quotes.save_as_template` | ✅ | | Create from template | template row | `POST /quotes/[id]/create-from-template` | `quotes.create_from_template` | ✅ | | Clone quote | detail page | `POST /quotes/[id]/clone` | `quotes.clone` | ✅ | | 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) | | Re-run AI | detail page button | `POST /quotes/[id]/re-run-ai` | `quotes.re_run_ai` | ✅ | | Process pending (batch) | cron | `POST /quotes/process-pending` | `quotes.process_pending` | ✅ | | Submit for approval | detail page | `POST /quotes/[id]/submit` | `quotes.submit` | ✅ (Phase G) | | Partial approve | detail page | `POST /quotes/[id]/partial-approve` | `quotes.partial_approve` | ✅ | | Review-approve | `/quotes/approvals` | `POST /quotes/[id]/review-approve` | ❌ | MCP gap (see §1.1) | | Re-price items | inline | `POST /quotes/[id]/re-price` | `quotes.re_price` | ✅ | | Split quote | inline | `POST /quotes/[id]/split` | `quotes.split` | ✅ | | Send email | email modal | `POST /quotes/[id]/send-email` | `quotes.send_email` | ✅ | | Retry email | email modal | `POST /quotes/[id]/retry-email` | `quotes.retry_email` | ✅ | | Render PDF | preview | `GET /quotes/[id]/pdf` + `/pdf/base64` | `quotes.get_pdf` | ✅ | | Populate insights | AI panel | `POST /quotes/[id]/populate-insights` + `/quote_items/[id]/populate-insights` | `quote_items.populate_insights` | ✅ | | List attachments | attachments card | `GET /quotes/[id]/attachments` | `quotes.list_attachments` | ✅ | | Upload attachment | attachments card | `POST /quotes/[id]/attachments/upload` + `/quotes/items/[id]/attachments/upload` | `attachments.upload_for_quote` | ✅ | | Set notes / set is_no_quote | form | `PATCH /quotes/[id]` | `quotes.update` | ✅ | | Lookup (number → id) | form | `GET /quotes/lookup` | `quotes.lookup` | ✅ | | Generate next quote # | form | `GET /quotes/next-number` | `quotes.next_number` | ✅ | | Archive quotes | `/quotes/archive` | `POST /quotes/archive` | ❌ | MCP gap (see §1.1) | Result: 27 capabilities, 25 fully covered. **2 MCP gaps** (review-approve, archive). ### Quote items | Capability | Page | `/api/v1` route | MCP tool | Notes | |---|---|---|---|---| | List per quote | `/quotes/[id]` | `GET /quotes/[id]/items` | `quote_items.list` | ✅ | | Get single item | inline | `GET /quote_items/[id]` | `quote_items.get` | ✅ | | Create item | inline "add row" | `POST /quotes/[id]/items` | `quote_items.create` | ✅ (Phase F) | | Update item | inline popover | `PATCH /quote_items/[id]` | `quote_items.update` | ✅ | | Bulk update items | sprint bulk | `POST /quote_items/bulk-update` | `bulk_update_quote_items` | ✅ | | Boolean popover fields (qty/cost/sell) | inline | `PATCH /order-items/[id]/...` (per-field) | 🟡 | Split across many small routes — see §3.4 | | Populate AI insights | insights panel | `POST /quote_items/[id]/populate-insights` | `quote_items.populate_insights` | ✅ | | Get AI intel | insights panel | `GET /quote_items/[id]/intel` | 🟡 | REST yes, MCP gap (see §1.7) | | Item history | inline panel | `GET /quote_items/[id]/item-history` | ❌ | MCP gap | | SKU lookup / match | sprint match picker | `GET /quotes/[id]/items/[itemId]/lookup` + `/match` | 🟡 | REST yes, MCP gap | | Rewrite (sprint rewrite) | sprint | `POST /quotes/items/rewrite` | ❌ | MCP gap (internal AI flow) | Result: 11 capabilities, 6 fully covered, **5 partial/missing**. ### Orders pipeline | Capability | Page | `/api/v1` route | MCP tool | Notes | |---|---|---|---|---| | List orders | `/orders` | `GET /orders/list` + `/orders` | `orders.list` | ✅ | | Get single order | `/orders/[id]` | `GET /orders/[id]` | `orders.get` | ✅ | | Create from quote | "Convert" button | `POST /orders/from-quote` | `orders.create_from_quote` | ✅ | | Create walk-in | `/orders/new` | `POST /orders/create` | `orders.create` | ✅ | | Update header | detail page | `PATCH /orders/[id]` | `orders.update` | ✅ (Phase H) | | Set quote link | detail page | `POST /orders/[id]/quote` | `orders.set_quote` | ✅ | | Archive | `/orders/archive` | `POST /orders/archive` | `orders.archive` | ✅ (Phase H) | | Cancel | detail page | `POST /orders/[id]/cancel` | `orders.cancel` | ✅ (Phase H) | | Review-save | detail page | `POST /orders/[id]/review-save` | `orders.review_save` | ✅ (Phase H) | | Mark invoiced (header) | invoicing tab | `POST /orders/[id]/mark_invoiced` | `orders.mark_invoiced` | ✅ (Phase H) | | Mark delivered (header) | invoicing tab | `POST /orders/[id]/mark_delivered` | `orders.mark_delivered` | ✅ (Phase H) | | Add note | detail page | `PATCH /orders/[id]` | `orders.add_note` | ✅ (Phase H, wrapper of update) | | Add attachment | attachments card | `POST /orders/[id]/attachments/upload` | `orders.add_attachment` | ✅ | | List attachments | attachments card | `GET /orders/[id]/matched-attachments` | `orders.list_attachments`, `orders.matched_attachments` | ✅ | | Split order | detail page | `POST /orders/[id]/split` | `orders.split` | ✅ | | Add item (existing order) | inline | `POST /orders/[id]/items` | `order_items.create` | ✅ (Phase H) | | Review data (read) | review screen | `GET /orders/[id]/review` | `orders.review` | ✅ (Phase E) | | Stats (aggregate) | reports | `GET /orders/stats` | `orders.stats` | ✅ (Phase H) | | Daily summary | reports | `GET /orders/daily_summary` | `orders.daily_summary` | ✅ (Phase H) | | Sprint counts | reports | `GET /orders/sprint/counts` | `orders.sprint_counts` | ✅ (Phase E) | | Find by order # | search | `GET /orders/lookup` | `orders.find_by_order_number` | ✅ (Phase E) | | Find by Message-ID | n8n callback | `GET /orders/lookup` | `orders.find_by_email_message_id` | ✅ (Phase E) | | Find duplicates | QA | `GET /orders/duplicates` | `orders.find_duplicates` | ✅ (Phase E) | | Lookup (number → id) | form | `GET /orders/lookup` | ❌ | MCP gap — see §1.2 | Result: 24 capabilities, 23 fully covered. **1 MCP gap** (generic lookup). ### Order items | Capability | Page | `/api/v1` route | MCP tool | Notes | |---|---|---|---|---| | List per order | `/orders/[id]` | `GET /order-items?orderId=` | `order_items.list` | ✅ | | Get single item | inline | `GET /order-items/[id]` | `order_items.get` | ✅ | | Update popover fields | inline popover | `PATCH /order-items/[id]` | `order_items.update` | ✅ (Phase A whitelist) | | Per-field shortcuts | inline | `PATCH /order-items/[id]/{qty,sku,cost,status}` | 🟡 | 4 routes for the popover per-field widgets; not wrapped in MCP — see §3.4 | | Update status | workflow button | `PATCH /order-items/[id]/status` | `order_items.update_status` | ✅ | | Confirm collected (driver) | driver flow | `POST /order-items/confirm` | `order_items.confirm_collected` | ✅ | | Split item | inline | `POST /order-items/[id]/split` | ❌ | MCP gap — see §1.3 | | Request margin approval | inline | `POST /order-items/[id]/request-margin-approval` | ❌ | MCP gap | | AI intel (read) | insights panel | `GET /order-items/[id]/intel` | 🟡 | REST yes, MCP gap | | Refresh AI intel | insights panel | `POST /items/[collection]/[id]/intel/refresh` | 🟡 | REST yes, MCP gap | | Backfill intel | cron | `POST /items/backfill-intel` | ❌ | MCP gap (cron) | | Context (price-history snapshot) | context panel | `GET /order-items/[id]/context` | ❌ | MCP gap | Result: 12 capabilities, 5 fully covered, **7 partial/missing**. ### Sprint / item-level queue | Capability | Page | `/api/v1` route | MCP tool | Notes | |---|---|---|---|---| | List sprint items | `/sprint` | `GET /sprint` | `sprint.list` | ✅ | | Claim item (30-min TTL) | inline | `POST /sprint/items/[id]/claim` | `sprint.claim` | ✅ | | Item attachments | sprint panel | `GET /sprint/items/[id]/attachments` | `sprint.item_attachments` | ✅ | | Attachment counts | sprint row badge | `GET /sprint/attachment-counts` | `sprint.attachment_counts` | ✅ | | 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 | Result: 5 capabilities, 4 fully covered, **1 partial**. ### Collection pool & map | Capability | Page | `/api/v1` route | MCP tool | Notes | |---|---|---|---|---| | List collection pool | `/collection-pool` | `GET /collection-pool` (`GET /orders/...`) | `collection_pool.list` | ✅ (older route) | | Map items | `/collection-map` | `GET /collection-map/items` | ❌ | MCP gap — see §1.5 | | Assign driver (map) | map click | `POST /collection-map/items/[id]/assign` | ❌ | MCP gap | | Bulk assign per supplier | pool | server action `bulkAssign` | ❌ | MCP gap | Result: 4 capabilities, 1 covered, **3 MCP gaps**. ### Driver (mobile) | Capability | Page | `/api/v1` route | MCP tool | Notes | |---|---|---|---|---| | List drivers (users) | settings | `GET /driver` | ❌ | MCP gap — only `drivers.list_stops` exists | | List stops | mobile / driver | `GET /driver/stops` | `drivers.list_stops` | ✅ | | Update stop status | mobile / driver | `PATCH /driver/stops/[id]/status` | `drivers.update_stop_status` | ✅ | | Collection-confirm (driver UI) | `/collection-confirm` | `GET /collection-confirm` | `collection_confirm.list` | ✅ | | Confirm collection | mobile / driver | `POST /collection-confirm/confirm-order-items` | `collection_confirm.confirm_order_items` | ✅ | | Upload invoice (driver) | mobile / driver | `POST /collection-confirm/invoice-upload` | `collection_confirm.upload_invoice` | ✅ | | Confirm stock (warehouse) | warehouse | `POST /collection-confirm/confirm-stock` | ❌ | MCP gap | Result: 7 capabilities, 5 covered, **2 MCP gaps** (driver list, confirm-stock). ### Delivery board (`/deliveries`) | Capability | Page | `/api/v1` route | MCP tool | Notes | |---|---|---|---|---| | List per tab (to_load / back_from_driver / checked_in) | `/deliveries` | `GET /deliveries?tab=` | ❌ | MCP gap (see §1.6) | | Load (set status → with_driver) | `/deliveries` | `POST /deliveries/load` | ❌ | MCP gap | | Load DN (delivery-note bound load) | `/deliveries` | `POST /deliveries/load-dn` | ❌ | MCP gap | | Check-in (driver returns) | `/deliveries` | `POST /deliveries/check-in` | ❌ | MCP gap | | Mark signed copy | `/deliveries` | `POST /deliveries/mark-signed-copy` | ❌ | MCP gap | | Find item to load by DN | `/deliveries` | server action | ❌ | MCP gap | Result: 6 capabilities, 0 MCP-covered, 6 REST-only. **Whole `/deliveries` MCP domain missing.** ### Delivery notes | Capability | Page | `/api/v1` route | MCP tool | Notes | |---|---|---|---|---| | List delivery notes | `/delivery-notes` | `GET /delivery-notes` | `delivery_notes.list` | ✅ (Phase E) | | Get single | `/delivery-notes/[id]` | `GET /delivery-notes/[id]` | ❌ | MCP gap — see §1.9 | | Create from order | invoicing modal | `POST /delivery-notes` | `delivery_notes.create` | ✅ (Phase E) | | Cancel | detail page | `POST /delivery-notes/[id]/cancel` | `delivery_notes.cancel` | ✅ (Phase E) | | Mark invoiced | invoicing tab | `POST /delivery-notes/[id]/mark-invoiced` | `delivery_notes.mark_invoiced` | ✅ (Phase E) | | Mark complete | detail page | `POST /delivery-notes/[id]/mark-complete` | `delivery_notes.mark_complete` | ✅ (Phase E) | | Regenerate PDF | detail page | `POST /delivery-notes/[id]/regenerate-pdf` | `delivery_notes.regenerate_pdf` | ✅ (Phase E) | | Create quote from DN | detail page | `POST /delivery-notes/[id]/create-quote` | `delivery_notes.create_quote` | ✅ (Phase E) | | Set customer PO | detail page | `POST /delivery-notes/[id]/customer-po` | `delivery_notes.set_customer_po` | ✅ (Phase E) | | Set item quantity | inline | `POST /delivery-notes/items/[id]/quantity` | `delivery_notes.set_item_quantity` | ✅ (Phase E) | | Set order | detail page | `POST /delivery-notes/[id]/order` | `delivery_notes.set_order` | ✅ (Phase E) | | Combine (merge DNs) | admin | `POST /delivery-notes/combine` | `delivery_notes.combine` | ✅ (Phase E) | | Mark signed copy | `/delivery-notes/[id]` | `POST /delivery-notes/[id]/signed-copy` | ❌ | MCP gap | | Get PDF (stream) | preview | ❌ (no stream route) | ❌ | See §1.10 | Result: 14 capabilities, 11 covered. **3 MCP gaps** (get-single, signed-copy, PDF stream). ### Repairs | Capability | Page | `/api/v1` route | MCP tool | Notes | |---|---|---|---|---| | List repairs | `/repairs` | `GET /repairs` | ❌ | MCP gap — see §1.8 | | Get single | `/repairs/[id]` | `GET /repairs/[id]` | ❌ | MCP gap | | Create repair | `/repairs` form | `POST /repairs` | ❌ | MCP gap | | Update repair | `/repairs/[id]` | `PATCH /repairs/[id]` | ❌ | MCP gap | | Delete repair | `/repairs/[id]` | `DELETE /repairs/[id]` | ❌ | MCP gap | | List form-data (create modal) | form | `GET /repairs/form-data` | ❌ | Internal helper, no MCP gap | | Upload file | repair detail | `POST /repairs/[id]/files/[field]` | ❌ | MCP gap | | Generate delivery note | repair detail | `POST /repairs/[id]/generate-delivery-note` | ❌ | MCP gap | | Generate repair number | form | ❌ (handled inline) | ❌ | MCP gap | Result: 9 capabilities, 0 MCP-covered. **Whole `/repairs` MCP domain missing.** ### Purchase orders (PO) | Capability | Page | `/api/v1` route | MCP tool | Notes | |---|---|---|---|---| | List POs | (sprint/PO view) | `GET /purchase-orders` | `purchase_orders.list` | ✅ | | 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 | | Send PO email | email modal | `POST /purchase-orders/[id]/send-email` | `purchase_orders.send_email` | ✅ | | Retry email | email modal | `POST /purchase-orders/[id]/send-email` | `purchase_orders.retry_email` | ✅ | | Send outcome (callback) | n8n callback | `POST /purchase-orders/[id]/send-email/outcome` | `purchase_orders.record_send_outcome` | ✅ | | PDF render | preview | `GET /purchase-orders/[id]/pdf` | ❌ | MCP gap (see §1.12) | | Dispatch (operator action) | PO row | `POST /purchase-orders/[id]/dispatch` | 🟡 | REST yes, MCP gap | | Add attachment | detail page | `POST /purchase-orders/[id]/attachments/upload` | `purchase_orders.add_attachment` | ✅ | | Get attachment | preview | `GET /purchase-orders/[id]/attachments/[filename]` | 🟡 | REST yes, MCP gap | | List stale POs | reports | `GET /purchase-orders/stale` | `purchase_orders.stale` | ✅ | | Scan (PB PDF → lines) | AI flow | `POST /purchase-orders/scan` | `purchase_orders.scan` | ✅ (internal) | | Diff (PO vs lines) | AI flow | `POST /purchase-orders/diff` | `purchase_orders.diff` | ✅ (internal) | | Record diff report | AI flow | `POST /purchase-orders/record-diff-report` | `purchase_orders.record_diff_report` | ✅ (internal) | Result: 14 capabilities, 9 fully covered, **5 partial/missing**. ### Stock items + stock purchases | Capability | Page | `/api/v1` route | MCP tool | Notes | |---|---|---|---|---| | List stock items | `/stock` | `GET /stock/items` | `stock_items.list` | ✅ | | Get single | `/stock/[id]` | `GET /stock/items/[id]` | `stock_items.get` | ✅ | | Create stock item | `/stock` form | `POST /stock/items/create` | `stock_items.create` | ✅ | | Update stock item | `/stock/[id]` | `PATCH /stock/items/[id]` | `stock_items.update` | ✅ | | Manual adjust | `/stock/[id]` | `POST /stock/items/[id]/adjust` | `stock_items.adjust` | ✅ | | Fulfill from stock for order | fulfilment flow | `POST /stock/fulfill` | `stock.fulfill` | ✅ | | Surplus book | fulfilment flow | `POST /stock/surplus-book` | `stock.surplus_book` | ✅ | | List stock purchases | `/stock-orders` | `GET /stock-purchases/list` | `stock_purchases.list` | ✅ | | Get single SP | `/stock-orders/[id]` | `GET /stock-purchases` (one-shot read) | `stock_purchases.get` | ✅ | | Create SP | `/stock-orders/new` | `POST /stock-purchases` | `stock_purchases.create` | ✅ | | Send SP email | email modal | `POST /stock-purchases/[id]/send-email` | `stock_purchases.send_email` | ✅ | | Retry email | email modal | `POST /stock-purchases/[id]/send-email` | `stock_purchases.retry_email` | ✅ | | Receive SP (book into stock) | warehouse | `POST /stock-purchases/[id]/receive` | `stock_purchases.receive` | ✅ | | Match & receive from invoice | scan workflow | n8n webhook → `/stock-purchases/match-from-invoice` | `stock_purchases.match_and_receive_from_invoice` | ✅ (Phase H internal) | | Add attachment | detail page | `POST /stock-purchases/[id]/attachments/upload` | `stock_purchases.add_attachment` | ✅ | | Get attachment | preview | `GET /stock-purchases/[id]/attachments/[filename]` | ❌ | MCP gap | | Stock-purchase list PDF | reports | `GET /stock-purchases/list-pdf` | ❌ | MCP gap | | Picking slip PDF | warehouse | `GET /stock-purchases/picking-slip` | ❌ | MCP gap | | SP PDF | detail page | `GET /stock-purchases/[id]/pdf` | ❌ | MCP gap (3 PDF routes un-wrapped) | | Stock-purchase items: list | `/stock-orders/[id]` | `GET /stock-purchase-items` | `stock_purchase_items.list` | ✅ | | Stock-purchase items: update | inline | `PATCH /stock-purchase-items/[id]` | `stock_purchase_items.update` | ✅ | | Stock-purchase items: bulk update | inline | `PATCH /stock-purchase-items/bulk` | `stock_purchase_items.bulk_update` | ✅ | | Stock-purchase items: per-field (qty/sku/cost) | inline | `PATCH /stock-purchase-items/[id]/{qty,sku,cost}` | 🟡 | 3 routes for popover per-field widgets, no MCP — see §3.4 | | Stock-purchase items: assign driver | inline | `POST /order-items/assign-driver` (uses same path) | `stock_purchase_items.assign_driver` | ✅ | | Stock-purchase items: mark collected | warehouse | n/a (via confirm) | `stock_purchase_items.mark_collected` | ✅ | | Next SP number | form | `GET /stock-purchases/next-number` | `stock_purchases.next_number` | ✅ | | Send SP (shorthand) | `POST /stock-purchases/send` | 🟡 | REST yes, MCP gap | | Stocktake PDF | reports | `GET /stock-items/stocktake-pdf` | ❌ | MCP gap | | Reorder list | `/stock-orders/reorder` | (server action) | ❌ | MCP gap | Result: 30 capabilities, 21 fully covered, **9 partial/missing**. ### Stock quick-draft | Capability | Page | `/api/v1` route | MCP tool | Notes | |---|---|---|---|---| | Search catalogue | inline | `GET /stock-quick-draft/search` | `stock_quick_draft.search` | ✅ (Phase E) | | Add line to draft | inline | `POST /stock-quick-draft/add-line` | `stock_quick_draft.add_line` | ✅ (Phase E) | | List suppliers | inline | `GET /stock-quick-draft/suppliers` | `stock_quick_draft.suppliers` | ✅ (Phase E) | | List drafts | inline | `GET /stock-quick-draft/drafts` | ❌ | MCP gap | | Edit/delete line | inline | `PATCH /stock-quick-draft/line/[lineId]` | ❌ | MCP gap | | Supplier history | inline | `GET /stock-quick-draft/supplier-history` | ❌ | MCP gap | Result: 6 capabilities, 3 covered. **3 MCP gaps.** ### Pricelist / catalogue | Capability | Page | `/api/v1` route | MCP tool | Notes | |---|---|---|---|---| | List raw uploads | `/pricelist` Raw tab | `GET /pricelist/raw` (via `pb-helpers`) | ❌ | MCP gap — see §1.13 | | Delete raw upload | row | `DELETE /pricelist/raw/[id]` | ❌ | MCP gap | | Search catalogue | Catalogue tab | `GET /price-list` (via `pb-helpers`) | ❌ | MCP gap | | Get price-list entry | inline | `GET /price-list/[sku]` | ❌ | MCP gap | | Add items (bulk ingest) | AI flow | `POST /price-list/add-items` | `price_list.add_items` | ✅ | | Create raw upload | upload widget | (server action) | ❌ | MCP gap | | Resync from raw upload | row | (server action) | ❌ | MCP gap | Result: 7 capabilities, 1 covered. **6 MCP gaps** — almost the entire catalogue is agent-blind. ### Companies / Customers / Suppliers (master data) | Capability | Page | `/api/v1` route | MCP tool | Notes | |---|---|---|---|---| | List companies | `/companies` | `GET /companies` | `companies.list` | ✅ | | Get single | `/companies/[id]` | `GET /companies/[id]` | `companies.get` | ✅ | | Create | form | `POST /companies` | `companies.create` | ✅ | | Update | form | `PATCH /companies/[id]` | `companies.update` | ✅ | | Delete | confirm | `DELETE /companies/[id]` | `companies.delete` | ✅ | | Find duplicates | QA | `GET /companies/duplicates` (via pb-helpers) | ❌ | MCP gap | | List customers | `/customers` | `GET /customers/list` + `/customers` | `customers.list` | ✅ (Phase G added `rep` filter) | | Get single | `/customers/[id]` | `GET /customers/[id]` | `customers.get` | ✅ | | Create | form | `POST /customers` | `customers.create` | ✅ | | Update | form | `PATCH /customers/[id]` | `customers.update` | ✅ | | Delete | confirm | `DELETE /customers/[id]` | `customers.delete` | ✅ | | Find duplicates | QA | (via pb-helpers) | ❌ | MCP gap | | List suppliers | `/suppliers` | `GET /suppliers` | `suppliers.list` | ✅ | | Get single | `/suppliers/[id]` | `GET /suppliers/[id]` | `suppliers.get` | ✅ | | Create | form | `POST /suppliers` | `suppliers.create` | ✅ | | Update | form | `PATCH /suppliers/[id]` | `suppliers.update` | ✅ | | Delete | confirm | `DELETE /suppliers/[id]` | `suppliers.delete` | ✅ | | Find duplicates | QA | (via pb-helpers) | ❌ | MCP gap | | Geocode (read + write) | supplier row | (none) | ❌ | FEATURE-REQUESTS.md (now) | | Mappable suppliers (for map) | `/supplier-map` | `GET /suppliers/mappable` (via pb-helpers) | ❌ | MCP gap | Result: 21 capabilities, 15 covered. **6 MCP gaps** (duplicates for all 3; mappable; geocoding). ### Categories | Capability | Page | `/api/v1` route | MCP tool | Notes | |---|---|---|---|---| | List categories | (admin) | ❌ (server-component read) | ❌ | Both gaps | | CRUD | (admin) | ❌ | ❌ | Whole admin domain missing — see §1.14 | Result: 2 capabilities, 0 covered. **Whole categories domain missing.** ### Dashboard & KPIs | Capability | Page | `/api/v1` route | MCP tool | Notes | |---|---|---|---|---| | Quotes dashboard data | `/dashboard` | `GET /dashboard` | ❌ | MCP gap | | Orders dashboard data | `/dashboard` | (combined) | ❌ | MCP gap | | Boss dashboard (today/week/month) | `/m` | `GET /boss-dashboard` | ❌ | MCP gap | | Boss dashboard rep activity | `/m` | `GET /boss-dashboard/rep-activity` | ❌ | MCP gap | | Daily summary (single call) | reports | `GET /orders/daily_summary` | `orders.daily_summary` | ✅ | | Dashboard daily summary | reports | ❌ (no separate route — `dashboard.daily_summary` MCP reads PB directly) | `dashboard.daily_summary` | ✅ (Phase E) | | Health check | n8n | `GET /health` | ❌ | MCP gap | Result: 7 capabilities, 2 covered. **5 MCP gaps.** ### Settings | Capability | Page | `/api/v1` route | MCP tool | Notes | |---|---|---|---|---| | Get AI settings | `/settings/ai` | `GET /ai/settings` | `ai_settings.get` | ✅ | | Update AI settings | `/settings/ai` form | `POST /ai/settings` (via Server Action) | `ai_settings.update` | ✅ | | Rerun AI insights (per collection) | `/settings/ai` | `POST /ai-insights/rerun` | `ai_insights.rerun` | ✅ | | List AI insights | `/settings/ai` | `GET /ai-insights` | ❌ | MCP gap | | Theme picker | `/settings` | client-side | n/a | N/A | | Dev identity switcher | `/settings` | client-side | n/a | N/A | | TV session | `/tv/pin`, `/tv/board` | `/api/tv/login`, `/api/tv/logout` | ❌ | MCP gap (separate auth surface) | | User lookup | `/settings` dev-mode | `GET /users/lookup` | `users.lookup` | ✅ | | List users | (admin) | `GET /users` | ❌ | MCP gap | Result: 9 capabilities, 4 covered. **5 gaps** (3 MCP, 2 non-MCP surfaces). ### Buyer tasks | Capability | Page | `/api/v1` route | MCP tool | Notes | |---|---|---|---|---| | List buyer tasks | `/orders/tasks` | `GET /buyer-tasks` | `buyer_tasks.list` | ✅ | | Generate PO | inline | `POST /buyer-tasks/generate-po` | `buyer_tasks.generate_po` | ✅ | | Mark ready | inline | `POST /buyer-tasks/mark-ready` | `buyer_tasks.mark_ready` | ✅ | Result: 3 capabilities, 3 covered. **0 gaps.** ### Files & attachments proxy | Capability | Page | `/api/v1` route | MCP tool | Notes | |---|---|---|---|---| | Stream PB file | inline | `GET /files/[collection]/[recordId]/[filename]` | ❌ | MCP gap (see §1.15) | | Fetch as base64 | inline | ❌ (no base64 proxy) | `files.fetch` | ✅ (Phase E) — but only base64 | Result: 2 capabilities, 1 covered. **1 MCP gap** (byte-stream equivalent). ### Invoicing (header) | Capability | Page | `/api/v1` route | MCP tool | Notes | |---|---|---|---|---| | Stock book from invoice | accountant button | `POST /invoicing/stock-book` | ❌ | MCP gap (see §1.16) | | QB invoice (build + send) | invoicing modal | n8n webhook (not in /api/v1) | ❌ | Architectural gap — see §1.17 | Result: 2 capabilities, 0 covered via MCP. **2 MCP gaps.** --- ## Gap analysis (prioritized) Gaps are scored by: - **Severity** (now = blocks daily agent/operator work; nice-to-have = polish) - **Effort** (small = ≤1 PR with no schema change; medium = 1-3 PRs; large = schema + migration + docs) - **Block count** (how many downstream agent workflows depend on it) ### 1. Critical MCP gaps (now, blocks daily agent workflows) #### 1.1 `quotes.review_approve` + `quotes.archive` MCP missing - **Severity:** now - **Effort:** small (route handlers exist, just thin wrappers) - **REST surface:** `POST /api/v1/quotes/[id]/review-approve`, `POST /api/v1/quotes/archive` - **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. #### 1.2 `orders.lookup` MCP missing - **Severity:** now - **Effort:** small - **REST surface:** `GET /api/v1/orders/lookup?number=...` - **Agent impact:** the agent has `find_by_order_number` (Phase E) but no generic lookup that takes either id or number. Duplication by design. - **Recommended slice:** either alias `find_by_order_number` to `lookup` or add a thin `orders.lookup({id|number})`. #### 1.3 `order_items.split` MCP missing - **Severity:** now - **Effort:** small - **REST surface:** `POST /api/v1/order-items/[id]/split` - **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. #### 1.5 `collection_map.assign` + bulk assign MCP missing - **Severity:** now - **Effort:** small - **REST surface:** `POST /api/v1/collection-map/items/[id]/assign`; bulk-assign is a server action on the pool page - **Agent impact:** driver dispatch from the map view is invisible to the agent. Daily workflow for the office team. - **Recommended slice:** add `collection_map.assign({order_item_id, driver_id})` and `collection_map.bulk_assign({supplier_id, driver_id, order_item_ids[]})`. #### 1.6 `/deliveries` MCP domain entirely missing - **Severity:** now (whole domain) - **Effort:** medium (5 routes + 5 tools) - **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. #### 1.7 `order_items.intel` + `quote_items.intel` MCP missing - **Severity:** now - **Effort:** small (pure read wrappers) - **Routes:** `GET /order-items/[id]/intel`, `GET /quote_items/[id]/intel`, `POST /items/[collection]/[id]/intel/refresh` - **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). - **Recommended slice:** add `order_items.intel({id})`, `quote_items.intel({id})`, `items.intel_refresh({collection, id})`. #### 1.8 `repairs.*` MCP domain entirely missing - **Severity:** now (whole domain) - **Effort:** medium - **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. - **Recommended slice:** add `repairs.list`, `repairs.get`, `repairs.create`, `repairs.update`, `repairs.delete`, `repairs.upload_file`, `repairs.generate_delivery_note`. #### 1.9 `delivery_notes.get` MCP missing - **Severity:** nice-to-have (most reads go via list) - **Effort:** small - **REST surface:** no `/delivery-notes/[id]` GET route exists; the agent reads the row directly from PB. - **Recommended slice:** add `GET /api/v1/delivery-notes/[id]` + `delivery_notes.get` MCP wrapper. Consistent with `quotes.get` / `orders.get`. #### 1.10 `delivery_notes.pdf` MCP + REST missing - **Severity:** now (preview breaks for agent-rendered DNs) - **Effort:** small (mirror the quote PDF route pattern) - **Recommended slice:** add `GET /api/v1/delivery-notes/[id]/pdf` + `delivery_notes.get_pdf` MCP tool. #### 1.11 Customer PO creation has no MCP wrapper - **Severity:** now - **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 (agent can't preview PO PDFs) - **Effort:** small (3 routes already exist) - **Routes:** `GET /purchase-orders/[id]/pdf`, `GET /stock-purchases/[id]/pdf`, `GET /stock-items/stocktake-pdf`, `GET /stock-purchases/list-pdf`, `GET /stock-purchases/picking-slip` - **Recommended slice:** add `purchase_orders.get_pdf`, `stock_purchases.get_pdf`, `stock_items.stocktake_pdf`, `stock_purchases.list_pdf`, `stock_purchases.picking_slip` MCP tools. #### 1.13 `pricelist` / `raw_price_list` MCP domain entirely missing - **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. - **Recommended slice:** add `pricelist.search({q, sku, brand, category, supplier})`, `pricelist.get({sku})`, `pricelist.raw_list()`, `pricelist.raw_delete({id})`, `pricelist.raw_create(...)`, `pricelist.raw_resync({id})`. #### 1.14 `categories` MCP domain entirely missing - **Severity:** nice-to-have - **Effort:** small - **Routes needed:** `/categories` GET+POST, `/categories/[id]` GET+PATCH+DELETE - **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. - **Recommended slice:** add the 5 MCP wrappers. #### 1.15 `files.stream` MCP missing (byte-stream equivalent) - **Severity:** nice-to-have - **Effort:** small - **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. - **Recommended slice:** add `invoicing.stock_book({id, date?})` MCP wrapper. #### 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): - `purchase_orders.dispatch` — REST-only; MCP wrapper. - `purchase_orders.get_attachment` — REST-only; MCP wrapper (use `files.fetch` as the underlying primitive). - `stock_purchase_items.{qty,sku,cost}` per-field — REST-only; MCP wrappers. - `order_items.{qty,sku,cost,status,context}` per-field — REST-only; MCP wrappers. - `order_items.request_margin_approval` — REST-only; MCP wrapper. - `order_items.split` — see §1.3. - `quote_items.item_history` — REST-only; MCP wrapper. - `quote_items.lookup` / `quote_items.match` — REST-only; MCP wrappers (sprint match picker). - `quotes.items/rewrite` — REST-only; MCP wrapper (internal AI flow, low priority). - `driver.list` — REST-only; MCP wrapper. - `collection_confirm.confirm_stock` — REST-only; MCP wrapper. - `delivery_notes.signed_copy` — REST-only; MCP wrapper. - `delivery_notes.combine` — REST-only; MCP wrapper. - `stock_purchases.{send, get_attachment, list_pdf, picking_slip}` — REST-only; MCP wrappers. - `stock_purchases.drafts.{list, edit_line, supplier_history}` — REST-only; MCP wrappers. - `companies.{find_duplicates, mappable}` — REST-only; MCP wrappers. - `customers.find_duplicates` — REST-only; MCP wrapper. - `suppliers.{find_duplicates, geocode, mappable}` — REST-only; MCP wrappers (geocode already in FEATURE-REQUESTS.md). - `users.list` — REST-only; MCP wrapper. - `ai_insights.list` — REST-only; MCP wrapper. - `health.check` — REST-only; MCP wrapper (cheap operational health probe). - `repair.*` — see §1.8. - `category.*` — see §1.14. - `pricelist.*` / `raw_price_list.*` — see §1.13. - `boss_dashboard.{get, rep_activity}` — REST-only; MCP wrappers. ### 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/.ts`. Building on that recommendation: #### 3.1 Per-tool file split ``` src/lib/server/mcp-tools/ orders/ list.ts # inputSchema + handler get.ts create.ts ... quotes/ ... ``` Each file exports: ```ts export const tool: McpTool = { name: "orders.list", description: "...", inputSchema: { ... }, }; export async function handle(req: Request, input: unknown, ctx: McpContext): Promise { ... } ``` 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 | | S14 | `categories.*` MCP | nice | small | 5 | | S15 | `boss_dashboard.*` MCP + `dashboard.quotes`/`dashboard.orders` | nice | small | 4 | | S16 | `files.stream` MCP | nice | small | 1 | | S17 | Long-tail single-route MCP wrappers (§2 list) | nice | medium | ~30 | | S18 | Per-field widget route cleanup (§3.4) | nice | small | refactor | | S19 | `stock_quick_draft.{drafts, edit_line, supplier_history}` MCP | nice | small | 3 | | S20 | `health.check` MCP + `ai_insights.list` MCP + `users.list` MCP | nice | small | 3 | 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) - "Stock purchases: paperclip badge" (UI-only, now) - "Drivers: no MCP visibility or assignment" (Phase E partial — list_stops + update_stop_status shipped; `drivers.list` still missing, see §1.16) - "Suppliers: read/update works, geocoding does not" (still open) - "Stock purchases: PDF → stock order ingestion" (still open) - "Stock purchases: paperclip badge on list" (UI-only) - "Standard error envelope with hint / next_actions / allowed_tools" (Phase I partial — 13 of 46 tools migrated; see §3.3) - "Quotes: trim/normalise customer + company names on quotes.create" (still open) - "Quotes: pre-send validation errors vs SMTP failures don't share an envelope" (Phase F partial — `no_pdf` diagnostics shipped) - "Orders: incoming email recipient resolution" (still open) - "Customers: case-insensitive email match" (still open) - "quotes.send_email.attach_auth_letters per-letterhead semantics undocumented" (still open) - "quote_items.create vs quote_items.update whitelist drift" (Phase F.2 partial — still see §3.2) - "Hermes MCP origin config" (doc-only, still open) - "Bulk_update_quote_items schema" (related to §3.2) New entries to append (these came out of this audit): - **§1.4: Quotes append-reply REST is on the wrong route** — add `POST /api/v1/quotes/append-reply`, keep back-compat alias. - **§1.8: Repairs MCP domain entirely missing** — 7 tools + 1 service layer extension. - **§1.13: Pricelist / raw_price_list MCP domain entirely missing** — 7 tools + REST routes. - **§1.14: Categories MCP domain entirely missing** — 5 tools + REST routes. - **§1.17: QuickBooks invoice MCP + service layer missing** — architectural; needs `qb-invoice-service.ts`. ## Open questions for the team-lead 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).