WFDASH-WEBAPP/docs/api-mcp-layer.md
2026-08-06 23:29:55 +02:00

14 KiB

WFDASH API + MCP Layer

This document describes the public HTTP surface intended for external automation (n8n workflows, custom AI agents, the local "Hermes" agent, etc.). Everything below is implemented in this repo and exercised by the Vitest suite under src/app/api/.

The README still lists an older subset of this surface. This file is the source of truth. If you add or rename an endpoint or MCP tool, update both files in the same commit.


1. Environment

Add to .env.local:

NEXT_PUBLIC_POCKETBASE_URL=https://pocketbase-gnqc88suy8fqk6wyryjjdpvl.169.239.180.130.sslip.io
PB_ADMIN_EMAIL=david.els1@outlook.com
PB_ADMIN_PASSWORD=1234567890
AGENT_API_KEY=123456
ORDER_ITEMS_COLLECTION=order_items   # or `order_items_v2` after migration

The MCP endpoint and every /api/v1/* route (except /api/v1/health) require the agent bearer token:

Authorization: Bearer <AGENT_API_KEY>

Rotate AGENT_API_KEY in this file plus every consumer when a key leaks.


2. REST endpoints (/api/v1/*)

All routes return JSON. All PATCH / POST bodies are JSON unless noted. Pagination defaults to page=1, perPage=50 where relevant.

2.1 Health / discovery

Method Path Auth Notes
GET /api/v1/health none { ok, service, time, version, env }

2.2 Orders

Method Path Body / params Notes
GET /api/v1/orders ?page&perPage&status&search Status filter is informational; items drive workflow. Expands company,customer,quote.
POST /api/v1/orders/from-quote { "quote_id": "..." } Creates order + items; idempotent on existing order_for_quote.
GET /api/v1/orders/{id} — Order detail with item rows.
GET /api/v1/orders/{id}/matched-attachments — Returns attachment matches against quote items.

2.3 Order items

Method Path Body Notes
PATCH /api/v1/order-items/{id}/status { item_status?, drv_status?, notes?, problem_notes? } Accepts legacy driver_status alias.

2.4 Quotes

Method Path Body / params Notes
GET /api/v1/quotes/lookup ?q=... Fuzzy lookup by quote number, ref, customer.
GET /api/v1/quotes/next-number — Atomic Q-YYYY-MM-NNN from quote_counters.
POST /api/v1/quotes/{id}/clone — Clone a quote + items.
POST /api/v1/quotes/{id}/save-as-template { name } Save a quote as a template.
POST /api/v1/quotes/{id}/create-from-template { template_id } New quote from a template.
POST /api/v1/quotes/{id}/retry-email — Resend the outbound quote email.
POST /api/v1/quotes/{id}/send-email { template? } Send the outbound quote email.
GET /api/v1/quotes/{id}/pdf — Render the quote PDF.
POST /api/v1/quotes/{id}/re-price { items: [...] } Bulk re-price quote items.
GET /api/v1/quotes/{id}/attachments ?direction=inbound|outbound|generated|all List quote-level attachments.
POST /api/v1/quotes/{id}/attachments/upload multipart file Upload a quote-level attachment (admin).
GET /api/v1/quotes/{id}/items/{itemId}/match — Match a quote item to a price-list row.
GET /api/v1/quotes/{id}/items/{itemId}/lookup — Suggest matches for an item.
POST /api/v1/quotes/items/rewrite { items: [...] } AI rewrite item descriptions.

2.5 Quote item attachments

Method Path Body Notes
POST /api/v1/quotes/items/{id}/attachments/upload multipart file New manual-upload endpoint (source = "manual").

2.6 Sprint

Method Path Body Notes
POST /api/v1/sprint/items/{id}/claim { ttl_minutes? } Claim a sprint row (30-min default lock).
GET /api/v1/sprint/items/{id}/attachments — List attachments for a quote item.
POST /api/v1/sprint/items/{id}/attachments/upload multipart file Upload a pricing proof (operator or AI).
GET /api/v1/sprint/attachment-counts ?quote_item_ids=... Bulk counts for sprint badges.

2.7 Companies

Method Path Body Notes
GET /api/v1/companies ?page&perPage&search&sort Filter on company / vendor substring.
POST /api/v1/companies { company, address?, vendor?, coordinates?: {lon,lat} } company required.
GET /api/v1/companies/{id} — Single row.
PATCH /api/v1/companies/{id} partial of create body
DELETE /api/v1/companies/{id} — PB cascade rules apply.

2.8 Customers

Method Path Body Notes
GET /api/v1/customers ?page&perPage&search&sort&expand expand=company,rep to inline.
POST /api/v1/customers { customer, email?, alternative_email?, buyer_email?, number?, company?, rep?, coordinates?, description? } customer required. company / rep accept id or name.
GET /api/v1/customers/{id} ?expand
PATCH /api/v1/customers/{id} partial Empty string unsets company / rep.
DELETE /api/v1/customers/{id} — Cascade rules apply.

2.9 Suppliers

Method Path Body Notes
GET /api/v1/suppliers ?page&perPage&search&sort coordinates is the legacy "lat, lng" text.
POST /api/v1/suppliers { suppliername, email?, suppliernumber?, supplieraddress?, supply_tags?, coordinates?, closingtime? }
GET /api/v1/suppliers/{id} —
PATCH /api/v1/suppliers/{id} partial
DELETE /api/v1/suppliers/{id} — Cascade rules apply.

2.10 Collection pool / map

Method Path Body Notes
GET /api/v1/collection-pool ?page&perPage&status Pool status filter.
POST /api/v1/collection-map/items/{id}/assign { driver_id } Assign a pool row to a driver.

2.11 Purchase orders

Method Path Body Notes
POST /api/v1/purchase-orders/{id}/dispatch { channel: "email"|"print" } Dispatch PO.
POST /api/v1/purchase-orders/{id}/send-email { to?, cc?, body? } Send PO email.
GET /api/v1/purchase-orders/{id}/send-email/outcome — Outcome of last send.
GET /api/v1/purchase-orders/{id}/pdf — Render PO PDF.
GET /api/v1/purchase-orders/stale ?days=... List POs untouched for N days.
POST /api/v1/purchase-orders/{id}/attachments/upload multipart/form-data field file Append a free-form attachment to additional_attachments. Max 25 MB.
DELETE /api/v1/purchase-orders/{id}/attachments/{filename} — Remove a single additional-attachment token.

2.12 Stock purchases

Method Path Body Notes
POST /api/v1/stock-purchases/{id}/attachments/upload multipart/form-data field file Append a free-form attachment (supplier invoice, packing slip) to stock_purchases.additional_attachments. Max 25 MB.
DELETE /api/v1/stock-purchases/{id}/attachments/{filename} — Remove a single additional-attachment token.

2.13 Users / file proxy

Method Path Body Notes
GET /api/v1/users/lookup ?q=... User search.
GET /api/v1/files/{collection}/{recordId}/{filename} — Auth-aware file proxy for collections whose rules reject the client.

3. MCP endpoint (/api/mcp)

JSON-RPC over HTTP. Same Authorization bearer as /api/v1/*.

3.1 Methods

  • tools/list — returns the full tool list (23 tools as of writing).
  • tools/call — { params: { name, arguments } }.

3.2 Tools

Orders / pool / sprint

Tool Purpose
orders.list List orders by order_number substring.
orders.create_from_quote Create order + items from a quote.
collection_pool.list List pool rows by status.
order_items.update_status Update item_status / drv_status / notes.

Quotes (email pipeline)

Tool Purpose
quotes.create_from_email Create quote + customer + items from parsed email. Idempotent on message_id. Use attachments.upload_for_quote per file after this returns.
quotes.append_reply Append a structured reply. Matches In-Reply-To then Q-YYYY-MM-NNN in subject.
quotes.find_by_message_id Look up by RFC 5322 Message-ID.
attachments.upload_for_quote Upload one attachment to a quote (base64, ≤25 MB).
bulk_update_quote_items Patch up to 100 quote items in one call (whitelisted fields only).

Companies / customers / suppliers (full CRUD)

Tool Purpose
companies.list / .get / .create / .update / .delete Full CRUD.
customers.list / .get / .create / .update / .delete Full CRUD. company / rep accept id or name.
suppliers.list / .get / .create / .update / .delete Full CRUD.

Stock purchases / POs (attachments)

Tool Purpose
purchase_orders.add_attachment Upload one free-form attachment to a PO (base64, ≤25 MB).
stock_purchases.add_attachment Upload one free-form attachment (supplier invoice, packing slip, photo of damaged stock) to a stock purchase (base64, ≤25 MB). Appends to additional_attachments; does NOT touch the versioned-PDF documents ledger.

3.3 Tool input shapes (condensed)

quotes.create_from_email (required: subject):

{
  "from_email": "buyer@acme.co.za",
  "from_name": "Acme Procurement",
  "subject": "RFQ — magnetic drill",
  "body_plain": "...",
  "body_html": "...",
  "message_id": "<unique-id@mail>",
  "in_reply_to": "<reply@mail>",
  "received_at": "2026-07-09T10:00:00Z",
  "company_hint": "Acme",
  "extracted_items": [
    { "description": "Magnetic drill", "quantity": 1, "unit": "ea", "notes": "" }
  ],
  "attachments": []
}

attachments.upload_for_quote (required: quote_id, filename, base64):

{
  "quote_id": "abc123",
  "filename": "spec.pdf",
  "content_type": "application/pdf",
  "size": 482113,
  "base64": "JVBERi0xLjQK..."
}

bulk_update_quote_items (required: items):

{
  "items": [
    {
      "id": "15-char-quote-item-id",
      "patch": {
        "cost_price": 1200.00,
        "selling_price": 1800.00,
        "supplier": "15-char-supplier-id",
        "pricing_source": "ai",
        "pricing_notes": "Auto-priced from Acme catalogue 2026-07-09"
      },
      "force_overwrite": false
    }
  ],
  "dry_run": false
}

Whitelist: cost_price, selling_price, description, sku, unit, supplier, markup, pricing_source, pricing_notes, matched_price_list_id, user_edited. Anything else returns reason: "forbidden_field" per row. Pre-priced rows (cost_price > 0 AND selling_price > 0) are skipped with reason: "already_priced" unless force_overwrite: true. Cap is 100 rows per call.

companies.create:

{ "company": "Acme", "address": "1 Road", "vendor": "V-1", "coordinates": { "lon": 28.04, "lat": -26.20 } }

customers.create:

{
  "customer": "Acme Procurement",
  "email": "buyer@acme.co.za",
  "alternative_email": "ops@acme.co.za",
  "buyer_email": "ap@acme.co.za",
  "number": "+27 11 555 0100",
  "company": "Acme",
  "rep": "David",
  "coordinates": { "lon": 28.04, "lat": -26.20 },
  "description": "Industrial buyer"
}

suppliers.create:

{
  "suppliername": "Acme Supplies",
  "email": "sales@acme.co.za",
  "suppliernumber": "+27 11 555 0200",
  "supplieraddress": "2 Road",
  "supply_tags": "tools,fasteners",
  "coordinates": "-26.20, 28.04",
  "closingtime": "17:00"
}

3.4 Response envelopes

Success:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "content": [{ "type": "text", "text": "{\"...\":\"...\"}" }]
  }
}

Error:

{
  "jsonrpc": "2.0",
  "id": 1,
  "error": { "code": -32601, "message": "Method not found" }
}

Auth failures return HTTP 401 for /api/v1/* but land in the JSON-RPC error envelope for /api/mcp (per the in-tree test suite).


4. Known quirks

  • orders.list has no status filter. The orders.status column was removed; status lives on items now. The MCP orders.list tool only supports search / order_number substring.
  • Quote-level attachments via the REST route (/api/v1/quotes/{id}/attachments/upload) and via the MCP tool (attachments.upload_for_quote) are independent code paths. New agents should prefer the MCP tool.
  • Quote-item attachments have two upload routes — the new one at /api/v1/quotes/items/{id}/attachments/upload (source "manual"), and the sprint one at /api/v1/sprint/items/{id}/attachments/upload (source "operator_proof" / "agent_proof"). Use the sprint route for pricing proofs; use the quotes route for raw attachments coming in with the original RFQ.
  • customers.create / customers.update accept company and rep as either ids or case-insensitive names. An unresolved name leaves the relation empty (no error) so the operator can wire it up later.
  • suppliers.coordinates is the legacy "lat, lng" text field. Don't migrate it to {lon,lat} — supplier-map.ts still parses the text form.

5. Testing the surface

Unit/API tests:

npm run test:run -- src/app/api

End-to-end (boots Next dev on :3100):

export E2E_BASE_URL=http://localhost:3100
export E2E_AGENT_API_KEY=$AGENT_API_KEY
npm run e2e

The Playwright suite intentionally skips env-driven specs when the keys aren't set, so a clean checkout reports passed, N skipped.