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

12 KiB
Raw Blame History

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):

{
  "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 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 senttoreceivedin the same call. The response carriesheader_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.
// 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.