- Updated the search input handling to improve responsiveness by integrating a debounced search mechanism, reducing latency from 550 ms to approximately 300 ms. - Added keyboard shortcut support for focusing the search box, enhancing accessibility and usability for frequent users. - Improved the search placeholder to reflect all searchable fields, providing clearer guidance to operators. - Introduced a clear-search button to streamline user interactions, allowing quick resets of the search input. - Enhanced the display of search results with a live result count badge, providing immediate feedback on search outcomes. These changes collectively enhance the search experience for operators, making it more intuitive and efficient.
722 lines
48 KiB
Markdown
722 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` | ✅ |
|
|
| 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/<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).
|