- Runbook: pre-migration settings don't persist (not "saves fail"), eligible statuses, 15 lines per quote, audit attribution, clone/template behaviour, supplier kept, doubts dot, confidence guidance (Medium is the default; raise to High if reps often change AI prices). - quotes.assistant_fill added to docs/api-mcp-layer.md, docs/audit/api-mcp-coverage.md and the Giellie skill. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
724 lines
48 KiB
Markdown
724 lines
48 KiB
Markdown
# 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` | ✅ |
|
|
| AI fill from POS | quote / Process page button | `POST /quotes/[id]/assistant/run` | `quotes.assistant_fill` | ✅ |
|
|
| 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: 28 capabilities, 26 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` | ✅ |
|
|
| Set cost / selling price / markup (batch) | `/stock` inline edit | `POST /stock/items/set-pricing` | `stock_items.set_pricing` | ✅ |
|
|
| 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/<name>.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<ToolResult> { ... }
|
|
```
|
|
|
|
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).
|