300 lines
9.7 KiB
Markdown
300 lines
9.7 KiB
Markdown
|
|
# 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 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.
|
||
|
|
|
||
|
|
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.
|