# 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`): - `suppliers` - `stock_items` - `purchase_orders` (with `deleted` field — see `pocketbase/pb_migrations/1786200007_updated_purchase_orders.js`) - `customers` - `invoices` (created by `1786200003_created_invoices.js`) - `invoice_items` (created by `1786200004_created_invoice_items.js`) - `customer_pos` + `customer_po_lines` (created by `1786200001_*` / `1786200002_*`) - `workflow_runs` (created by `1786200006_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 ```bash # 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= # 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: 1. Accepts a base64 image OR a file path. 2. Sends it to OpenAI Vision (or Google Document AI) with a strict schema: `{ supplier, order_number?, items: [{ sku, qty, unit_price_gbp }] }`. 3. Calls `validatePurchaseOrderDraft` to confirm the supplier / SKUs exist. 4. Calls `createPurchaseOrder` with the structured draft. 5. 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.