- Add Supplier Map component to suppliers page and reorganize sidebar navigation - Implement picking slip PDF generation with supplier per line item and picked-by signature footer - Add invoice bulk print tracking API and view models to track printed invoice state - Extend company search and form to support email/telephone fields and improve searchability - Fix data table debounce race condition by using useRef for onSearch handler - Fix supplier page fetch race condition with debounced timeout - Reduce list page item counts to 10 per page for customers, companies, and suppliers - Update company search filter to include email and telephone in search queries - Add comprehensive test suite for new and existing components and APIs - Add pdf-lib dependency for PDF generation and update related tooling - Clean up unused code and fix minor linting issues
9.7 KiB
WFDASH Agent Instructions
You are an external automation agent connecting to a running instance of the WFDASH business CRM. This document tells you how to connect, what you can do, and what you must not do.
The full reference for every endpoint and tool is in
docs/api-mcp-layer.md. Read it before doing anything non-trivial.
1. Connecting
WFDASH exposes a JSON-RPC MCP endpoint:
POST <BASE>/api/mcp
<BASE> is the URL the operator runs WFDASH at:
- Local dev:
http://localhost:4000(or:3100if the operator pointed Playwright there) - Staging / production: the public host they tell you
Every request must include the agent bearer token:
Authorization: Bearer 123456
The token is shared with you out-of-band. Do not log it. Do not write it to disk. Treat it like any other long-lived secret. If you suspect it leaked, stop and ask the operator to rotate it.
The MCP endpoint accepts only two methods:
tools/list— discover every tool and its input schematools/call— invoke a tool by name witharguments
There is also a REST surface at /api/v1/* for direct calls. Prefer
the MCP tools when one matches — they encode the same business rules
and give you safer error envelopes.
2. Discovering tools
Always start by listing tools on first contact (and again when the operator says tools have changed):
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list"
}
The response includes every tool's name, description, and
inputSchema. Do not hard-code schemas from this document alone
— they drift. Trust tools/list.
3. Calling a tool
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "orders.list",
"arguments": { "page": 1, "perPage": 25, "search": "SO-3" }
}
}
Success returns:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"content": [{ "type": "text", "text": "{...JSON string...}" }]
}
}
Failures (auth, validation, upstream PB error) come back as either
HTTP non-2xx or a JSON-RPC error object — see "Failure modes"
below.
4. What you can do
You have access to five families of tools. Always pick the most specific tool that fits the task.
4.1 Quote email pipeline (read + write)
| Tool | Use it for |
|---|---|
quotes.create_from_email |
Ingest a parsed inbound RFQ email into a new quote (idempotent on message_id). |
quotes.append_reply |
Append an inbound reply to the right quote (In-Reply-To then subject fallback). |
quotes.find_by_message_id |
Look up an existing quote by RFC 5322 Message-ID. |
attachments.upload_for_quote |
Attach one file to a quote. Call per file after quotes.create_from_email returns the new quote id — never base64-embed in the create payload (blows the 1 MB request limit). |
bulk_update_quote_items |
Patch up to 100 quote items in one round-trip. Use dry_run: true first when unsure. |
4.2 Quotes (read + write from your side)
| Tool | Use it for |
|---|---|
orders.create_from_quote |
Turn an approved quote into an order + items. |
4.3 Order workflow
| Tool | Use it for |
|---|---|
orders.list |
List orders by order_number substring. |
collection_pool.list |
Inspect what drivers need to collect. |
order_items.update_status |
Move an item through the operational pipeline. |
4.4 CRM maintenance
| Tool | Use it for |
|---|---|
companies.list / .get / .create / .update / .delete |
Maintain company records. |
customers.list / .get / .create / .update / .delete |
Maintain customer records. company / rep accept id or name. |
suppliers.list / .get / .create / .update / .delete |
Maintain supplier records. |
5. Hard rules
These come from the operator and from the codebase. Treat them as non-negotiable.
- Respect field whitelists.
bulk_update_quote_itemswill reject unknown fields withreason: "forbidden_field". Don't try to sneakid,quote,created,updated, orquantitythrough — they are filtered before PB is touched. - Don't overwrite hand-priced rows silently. Rows where
cost_price > 0 AND selling_price > 0are skipped withreason: "already_priced"unless you passforce_overwrite: trueon that specific row. - Stamp your work. When you change pricing fields, include
pricing_notesin the patch. The tool does not auto-inject an audit string. - Use the right upload route.
attachments.upload_for_quote(MCP) → quote-level attachment/api/v1/quotes/items/{id}/attachments/upload(REST) → quote-item attachment withsource: "manual"/api/v1/sprint/items/{id}/attachments/upload(REST) → pricing proof withsource: "operator_proof"or"agent_proof"
- Don't migrate
suppliers.coordinates. It is the legacy"lat, lng"text field.supplier-map.tsparses that format. - Cascade-aware deletes. Deleting a company / customer / supplier fails if PocketBase relation rules block it. If your delete fails, surface the error — don't try to recurse and delete the blockers.
- One file per call. Don't batch multiple attachments into one MCP call. Each file is its own round-trip on purpose so a single bad file doesn't roll back the quote.
- Respect the per-batch cap.
bulk_update_quote_itemsis capped at 100 rows. Page larger batches. - Use
dry_run: trueon unfamiliar batches. The shape of the response is identical; onlystatuschanges per row.
6. Failure modes
- Wrong / missing bearer → HTTP 401 on
/api/v1/*, JSON-RPC error envelope on/api/mcp. - Unknown tool → JSON-RPC error:
Method not found. - PB admin unavailable → JSON-RPC error: usually a 5xx with the underlying message.
- Per-row validation failures in
bulk_update_quote_itemsare not a batch failure. Other rows still apply. Read the per-rowreasonand decide whether to retry.
If you ever get a 401, stop and check the token before retrying — the operator needs to know if the token expired.
7. Endpoints you should NOT call directly
The full REST surface exists for the in-app UI and tests. As an external agent, prefer MCP tools. Only drop to REST if:
- There is no MCP tool for what you need (rare; tell the operator so they can add one), or
- You are uploading a binary attachment (MCP upload uses base64 and is slower than the REST multipart endpoint).
Never poke PocketBase directly. Every call goes through the
AGENT_API_KEY-gated admin PB client, and only via these tools.
8. Worked example: ingest an inbound RFQ email
The quotes.create_from_email and quotes.append_reply tools are
lenient on the optional fields:
from_emailis optional. If missing, the quote is linked to a sentinel"Unknown Sender"customer (unknown-sender@no-from.wfdash.local) that the operator reassigns in the quote-editing UI.message_idis optional. A stablesynth-…@wfdash.localid is derived from the email identity when missing. Still pass it when you have it — that's what makes retries idempotent.in_reply_tois supported on both tools. Pass it whenever the inbound message carries anIn-Reply-Toheader — it disambiguates threaded replies before the subject match kicks in.company_hintis supported oncreate_from_email. Free-text, case-insensitive. Helps the resolver route the quote to the right company when the customer name is ambiguous.body_htmlis supported on both tools, alongsidebody_plain. Pass both if you have them — the operator UI prefersbody_htmlwhen present.attachmentsoncreate_from_emailis deprecated. It is accepted for back-compat but ignored. Useattachments.upload_for_quoteper file after the create call returns the new quote id.attachments.upload_for_quoteis capped at 25 MB per file (raw base64-decoded bytes). Larger files must be split or rejected at the source.extracted_items[].notesis supported. Use it for line-specific context (e.g."see attached sketch page 2") that doesn't belong in the description.
// 1) Create the quote (and its customer/company/items).
{
"method": "tools/call",
"params": {
"name": "quotes.create_from_email",
"arguments": {
"from_email": "buyer@acme.co.za",
"from_name": "Acme Procurement",
"subject": "RFQ — magnetic drill",
"body_plain": "Need pricing for one magnetic drill.",
"body_html": "<p>Need pricing for one magnetic drill.</p>",
"message_id": "<msg-abc@mail.acme.co.za>",
"in_reply_to": "<prior-msg@mail.acme.co.za>",
"received_at": "2026-07-09T10:00:00Z",
"company_hint": "Acme",
"extracted_items": [
{
"description": "Magnetic drill",
"quantity": 1,
"unit": "ea",
"notes": "See attached spec page 2"
}
]
}
}
}
// 2) Parse `result.content[0].text` → get the new `quote.id`.
// 3) For each attachment (one call per file, ≤25 MB each):
{
"method": "tools/call",
"params": {
"name": "attachments.upload_for_quote",
"arguments": {
"quote_id": "<quote.id>",
"filename": "spec.pdf",
"content_type": "application/pdf",
"size": 482113,
"base64": "JVBERi0xLjQK..."
}
}
}
// 4) Operator reviews in the UI and approves. If they ask you to
// turn it into an order:
{
"method": "tools/call",
"params": {
"name": "orders.create_from_quote",
"arguments": { "quote_id": "<quote.id>" }
}
}
9. When in doubt
Ask the operator. WFDASH is a real business CRM — silent mistakes cost people money. Prefer one extra confirmation call over one silent write.