WFDASH-WEBAPP/docs/AGENT_INSTRUCTIONS.md
Dave 73127f10cc docs(mcp): document stock_items.set_pricing and create pricing fields
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 01:02:44 +02:00

321 lines
No EOL
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 `:3100` if 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 schema
- `tools/call` — invoke a tool by name with `arguments`
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):
```json
{
"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
```json
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "orders.list",
"arguments": { "page": 1, "perPage": 25, "search": "SO-3" }
}
}
```
Success returns:
```json
{
"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 Stock purchase + PO attachments
| Tool | Use it for |
| --- | --- |
| `purchase_orders.add_attachment` | Attach a supplier quote PDF or proof-of-delivery to a PO (base64, ≤25 MB). |
| `stock_purchases.add_attachment` | Attach a supplier invoice, packing slip, or damaged-stock photo to a stock purchase (base64, ≤25 MB). Appends to `additional_attachments`; does NOT touch the versioned-PDF `documents` ledger. |
| `stock_purchase_items.update` | Patch whitelisted pricing + catalogue fields on a single stock_purchase_item: `cost_price`, `sku`, `description`, `notes`, `unit`, `supplier_sku`, `quantity`. Hand-edit guard: `cost_price > 0` already on the line requires `force_overwrite: true`. |
| `stock_purchase_items.bulk_update` | Batch patch stock_purchase_items (≤100 rows). Same whitelist + hand-edit guard as `.update`. Per-row independence — one row's failure never affects another. `dry_run: true` returns the same shape with `status: "dry_run"` per row. |
| `stock_purchase_items.assign_driver` | Bind a stock_purchase_item to a driver. Transitions line status `null → assigned_to_driver` or `collection_pool → assigned_to_driver`. Wrong_status (409) when the line is already past the assignment step. |
| `stock_purchase_items.mark_collected` | Mark a line as physically delivered. Status → `collected`, bumps `quantity_received`. Does NOT book into stock — that's `stock_purchases.receive`. Header auto-flips to `received` when every line on the PO has `quantity_received >= quantity`. |
| `stock_items.update` | Patch catalogue fields on a master stock_item: `sku`, `description`, `notes`, `unit`, `selling_price`, `markup`. For inventory qty use `stock_items.adjust`. Returns `{ok: true, item, previous}` on success or `{ok: false, reason, message, field?}` on rejection. |
| `stock_items.set_pricing` | Set `cost_price` / `selling_price` / `markup` on existing stock_items in one batch (max 100 per call). The POS till reads `selling_price` (not `markup`), so set both consistently: selling = cost × (1 + markup/100). Cost changes are audited in `stock_ledger` with the `actor`; quantities are NOT changed (use `stock_items.adjust`). Requires `actor.id`. Returns per-row `{ok, item, previous}` / `{ok: false, reason, message}`. |
### 4.5 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.
1. **Respect field whitelists.** `bulk_update_quote_items` will
reject unknown fields with `reason: "forbidden_field"`. Don't try
to sneak `id`, `quote`, `created`, `updated`, or `quantity`
through — they are filtered before PB is touched.
2. **Don't overwrite hand-priced rows silently.** Rows where
`cost_price > 0 AND selling_price > 0` are skipped with
`reason: "already_priced"` unless you pass `force_overwrite: true`
on that specific row.
3. **Stamp your work.** When you change pricing fields, include
`pricing_notes` in the patch. The tool does **not** auto-inject
an audit string.
4. **Use the right upload route.**
- `attachments.upload_for_quote` (MCP) → quote-level attachment
- `/api/v1/quotes/items/{id}/attachments/upload` (REST) →
quote-item attachment with `source: "manual"`
- `/api/v1/sprint/items/{id}/attachments/upload` (REST) →
pricing proof with `source: "operator_proof"` or `"agent_proof"`
5. **Don't migrate `suppliers.coordinates`.** It is the legacy
`"lat, lng"` text field. `supplier-map.ts` parses that format.
6. **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.
7. **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.
8. **Respect the per-batch cap.** `bulk_update_quote_items` is
capped at 100 rows. Page larger batches.
9. **Use `dry_run: true` on unfamiliar batches.** The shape of the
response is identical; only `status` changes 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_items` are
**not** a batch failure. Other rows still apply. Read the per-row
`reason` and decide whether to retry.
- **Per-row failures** in `stock_purchase_items.bulk_update` behave the
same way — one row's failure never affects another. Same `reason`
pattern: `forbidden_field`, `already_priced`, `pb_error`.
- **`mark_collected` auto-flip** — the service inspects every line on
the parent PO after the write. If every line has `quantity_received
>= quantity`, the PO header flips from `sent` to `received` in the
same call. The response carries `header_flipped: true` only when this
call was the one that completed the PO.
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_email` is 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_id` is optional. A stable `synth-…@wfdash.local` id is
derived from the email identity when missing. Still pass it when
you have it — that's what makes retries idempotent.
- `in_reply_to` is supported on both tools. Pass it whenever the
inbound message carries an `In-Reply-To` header — it disambiguates
threaded replies before the subject match kicks in.
- `company_hint` is supported on `create_from_email`. Free-text,
case-insensitive. Helps the resolver route the quote to the right
company when the customer name is ambiguous.
- `body_html` is supported on both tools, alongside `body_plain`.
Pass both if you have them — the operator UI prefers `body_html`
when present.
- `attachments` on `create_from_email` is **deprecated**. It is
accepted for back-compat but **ignored**. Use
`attachments.upload_for_quote` per file after the create call
returns the new quote id.
- `attachments.upload_for_quote` is capped at **25 MB per file**
(raw base64-decoded bytes). Larger files must be split or rejected
at the source.
- `extracted_items[].notes` is supported. Use it for line-specific
context (e.g. `"see attached sketch page 2"`) that doesn't belong
in the description.
```jsonc
// 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.