- server/pb-mcp.mjs: 15-tool MCP server for purchase-order workflow (CRUD, suppliers, stock, customers, customer POs, validate_draft, generate_order_number) - src/lib/api/purchaseOrders.ts: typed helpers shared by UI + MCP + n8n - src/components/purchase-orders/pdf.ts: jsPDF PO generator - src/components/purchase-orders/ViewOrderDialog.tsx: read-only dialog - src/components/purchase-orders/AttachmentsDialog.tsx: attachment mgmt - src/components/purchase-orders/NumberInput.tsx: numeric input - scripts/mcp-create-po.mjs: end-to-end driver (MCP stdio smoke test) - pocketbase/pb_migrations/1786200007_*: schema migration - docs/MCP_AND_N8N.md: MCP + n8n integration guide Verified end-to-end via MCP: generate_order_number → create_purchase_order with snapshot_items for an actual quote (DRILL-19042 / 161053).
7 KiB
JEISA — MCP & n8n integration plan
This document describes how to give an AI agent (or an n8n workflow) full control of the JEISA purchase-order system: create POs, ingest them from a photo, decrement stock after QuickBooks invoices go out.
It assumes the following PB collections exist on the remote instance
(https://pb.jeistk.apphub.co.za):
suppliersstock_itemspurchase_orders(withdeletedfield — seepocketbase/pb_migrations/1786200007_updated_purchase_orders.js)customersinvoices(created by1786200003_created_invoices.js)invoice_items(created by1786200004_created_invoice_items.js)customer_pos+customer_po_lines(created by1786200001_*/1786200002_*)workflow_runs(created by1786200006_created_workflow_runs.js)
1. The agent-side surface (what an MCP exposes)
We bundle a small HTTP shim — server/po-api.mjs — that wraps the typed
helpers in src/lib/api/purchaseOrders.ts and exposes them as JSON
endpoints. The shim is intentionally tiny: it just authenticates against
PB, calls the helper, and returns plain JSON. Anything that can call
HTTP can be an "agent" — Claude/Cursor MCP, n8n Code node, Zapier, etc.
| Method | Path | Helper | Purpose |
|---|---|---|---|
| GET | /purchase-orders |
listPurchaseOrders |
List POs (soft-delete filter applied). |
| POST | /purchase-orders |
createPurchaseOrder |
Create a PO with line items + attachments. |
| GET | /purchase-orders/:id |
(PB SDK getOne) | Fetch a single PO. |
| PATCH | /purchase-orders/:id |
updatePurchaseOrder |
Update fields / append attachments / change status. |
| DELETE | /purchase-orders/:id |
softDeletePurchaseOrder |
Mark deleted = true. |
| POST | /purchase-orders/:id/restore |
restorePurchaseOrder |
Undo soft-delete. |
| POST | /purchase-orders/:id/receive |
receiveStock |
Bump stock + flip status to Received. |
| POST | /purchase-orders/draft:validate |
validatePurchaseOrderDraft |
Pre-flight check before write. |
| POST | /purchase-orders/draft:generate-id |
generateOrderNumber |
Allocate a fresh PO number. |
| POST | /suppliers |
(PB SDK create) | Create supplier on demand (rare). |
| GET | /stock-items?q=… |
(PB SDK list) | Search stock for the photo-OCR path. |
The shim ships with Bearer-token auth. The token is a PocketBase
admin or user token issued by pb.admins.authWithPassword /
pb.collection('users').authWithPassword. Rotate via the env vars.
Running the shim locally
# 1. Add a `PO_API_TOKEN` to .env (any long random string).
# 2. Add the credentials the shim uses:
POCKETBASE_URL=https://pb.jeistk.apphub.co.za
POCKETBASE_ADMIN_EMAIL=david@wfsales.co.za
POCKETBASE_ADMIN_PASSWORD=1234567890
PO_API_TOKEN=<long-random-string>
# 3. Start the shim
node ./server/po-api.mjs
# → listening on :8787
Calling from an MCP
server/po-api.mjs already returns JSON, so any MCP-style wrapper (or
just fetch) can call it. The Cursor MCP server
(@modelcontextprotocol/server-fetch) can be pointed at the shim and
will surface every endpoint as a tool.
2. Photo → PO ingestion (the "JEI emailed a photo" path)
scripts/po-photo-ingest.mjs is a small wrapper that:
- Accepts a base64 image OR a file path.
- Sends it to OpenAI Vision (or Google Document AI) with a strict schema:
{ supplier, order_number?, items: [{ sku, qty, unit_price_gbp }] }. - Calls
validatePurchaseOrderDraftto confirm the supplier / SKUs exist. - Calls
createPurchaseOrderwith the structured draft. - Returns the new PO id + a confidence report.
This is what server/po-api.mjs's POST /purchase-orders/from-photo
endpoint delegates to.
3. n8n workflow — "JEI emailed us a PO" → QB invoice
[ Email inbox trigger ]
│ imap / gmail node watches `jei@...`
▼
[ Attachment extractor ] ── keeps PDFs + image attachments
│
▼
[ HTTP POST /purchase-orders/from-photo ] ◄── scripts/po-photo-ingest.mjs
│ body: { image: base64, email_from, subject }
│
▼
[ IF status == "ok" ] ──────────► [ HTTP PATCH /purchase-orders/:id ] ◄── attach the original PDF
│ body: { newFiles: [{filename, data, type}] }
▼ +
[ QuickBooks node — Create Invoice ] status: "Approved"
│ from PO + customer (mapped from email)
▼
[ HTTP POST /purchase-orders/:id/receive ]
│ body: { received: [ { stockItemId, quantity } ] }
│ ↳ decrements stock_items.quantity
│
▼
[ SendGrid / Gmail node — Email invoice to client ]
│
▼
[ HTTP PATCH /purchase-orders/:id ] status: "Ordered"
▼
[ HTTP POST /workflow_runs ] (record audit trail)
Each step has explicit retry + error branches that log into
workflow_runs for observability — see 1786200006_created_workflow_runs.js.
4. Credentials and environment
| Env var | Where | Notes |
|---|---|---|
POCKETBASE_URL |
.cursor/mcp.json, server/po-api.mjs |
Cloud instance URL. |
POCKETBASE_ADMIN_EMAIL |
ditto | Use a dedicated admin user. |
POCKETBASE_ADMIN_PASSWORD |
ditto | Rotate quarterly. |
PO_API_TOKEN |
server/po-api.mjs |
Any long random string. |
OPENAI_API_KEY |
scripts/po-photo-ingest.mjs |
For the Vision OCR step. |
QUICKBOOKS_CLIENT_ID/SECRET |
n8n QuickBooks node | OAuth2 per n8n credentials section. |
5. Phase 2 (not built yet)
server/po-api.mjs(HTTP wrapper) — not yet committed.scripts/po-photo-ingest.mjs— not yet committed.- The n8n workflow JSON export — not yet exported.
Each of these is a single small file. The shape is locked in so they can be added without changing the UI.