# Kin ↔ Forge Tool Matrix


> **Kin platform handoff:** [KIN_THIN_TOOLS.md](./KIN_THIN_TOOLS.md) — what to add to Kin’s thin `forge_*` list (P0 update invoice + forge_agent).

**Updated:** 2026-08-11  
**Live MCP tools:** see `/.well-known/mcp.json` toolCount (SSOT: `lib/mcp/tools.ts`)  
**Purpose:** Stop agents getting stuck on missing create/update paths.  
**SSOT for ops:** Prefer one `POST /api/v1/agent/run` (or `forge_agent`) for multi-step work.
**Agent laws:** https://forge.kin.team/api/v1/agent-laws — **v2.0.0 Product Law Balanced**; never create to update; site→property; revise invoices in place; obey `recovery.suggestedTool`.  
**On connect:** https://forge.kin.team/api/v1/agent/bootstrap (inject standingInstructions)  
**Discovery:** https://forge.kin.team/.well-known/api.json (v2.1.0) · OpenAPI v2.0.0 documents `force` / `forceReason` / `propertyAddress` on creates.

---

## Product Law Balanced (2026-08-11)

Server-enforced on commercial **creates** for every writer (UI, REST, MCP, `forge_agent`):

| Law | Agent action |
|-----|----------------|
| Client identity | Search first; on `DUPLICATE_DETECTED` / ambiguous → use `existing` + recovery — do not invent twin clients |
| Site = property | Pass `propertyId` or `propertyAddress`/`propertyLabel`; do not park site only in notes |
| Never create to update | `forge_update_*` / `forge_revise_invoice` on same J-/Q-/INV- |
| One open invoice | `OPEN_INVOICE_EXISTS` → revise that INV |
| force | `force:true` + `forceReason` only for real exceptions (audited) |

---

## Mental model (read this first)

| Layer | What it is | When to use |
|-------|------------|-------------|
| **A. Native agent** | `POST /api/v1/agent/run` · MCP `forge_agent` | Default for multi-step: quote→job→invoice, email RFQ, “update Ty’s email and send” |
| **B. Full MCP (live toolCount)** | `lib/mcp/tools.ts` via MCP / internal agent | Precise single ops when you already have IDs |
| **C. REST v1** | `/api/v1/*` Bearer `fk_live_…` | Platform integrations, Kin thin tools, scripts |
| **D. Kin thin tools** | Platform-side `forge_*` convenience tools | Convenience only — **must mirror REST**, not invent fields |

**Rule:** If a Kin agent can’t do it with thin tools, either:
1. Call **`forge_agent`** with the user’s words, or  
2. Expand Kin thin tools to match this matrix — **do not** tell the user “Forge can’t do that.”

---

## Wave B.1 (2026-08-05) — invoice price fix without clone

**Problem:** Kin thin agents often lack `forge_update_invoice` and respond to "total should be $X" by creating INV-N+1 and trashing the original.

**Fix (Forge side):**
- `forge_update_invoice` / `forge_revise_invoice` / `PATCH /api/v1/invoices/:id` accept **`targetTotal`** (tax-inclusive dollars) or `targetTotalCents`.
- Scales existing line unit prices so grand total matches — **same INV number**.
- Docs + agent laws: never create to correct a price; if thin toolset missing update, call **`forge_agent`** / `POST /api/v1/agent/run`.

| Tool | Price-fix path |
|------|----------------|
| `forge_update_invoice` | `{ id: "INV-0049", targetTotal: 13680 }` |
| `forge_revise_invoice` | same + auto-reopen if issued/emailed |
| `forge_agent` | `"Update INV-0049 so the total is $13,680"` |

---

## Wave B (2026-08-05) — catalog, tax, custom fields

| Area | Tools |
|------|-------|
| Services catalog | `forge_list_services`, `forge_get_service`, `forge_match_service`, `forge_create_service`, `forge_update_service` |
| Tax rates | `forge_list_tax_rates`, `forge_get_tax_rate`, `forge_create_tax_rate`, `forge_update_tax_rate` |
| Custom fields | `forge_list_custom_fields`, `forge_create_custom_field`, `forge_get_custom_field_values`, `forge_set_custom_field_values` |

**Rules:** Before inventing line prices, call `forge_match_service` against the price book. Prefer catalog `defaultUnitPrice` + description templates on quotes. Custom fields: list definitions → get/set values by `entityType` + `entityId` (client|job|property).

**UI (same ship):** Quote ↔ Job ↔ Invoice **Related documents** chain on detail pages — orange Invoiced badge opens the invoice.

---

## Wave A.2 (2026-08-05) — time, payroll, team

| Area | Tools |
|------|-------|
| Time entries | `forge_create_time_entry`, `forge_get_time_entry`, `forge_update_time_entry`, `forge_delete_time_entry` |
| Corrections | `forge_list_time_corrections`, `forge_approve_time_correction` |
| Time off | `forge_list_time_off`, `forge_create_time_off`, `forge_decide_time_off` |
| Payroll | `forge_list_payroll_periods`, `forge_get_payroll_period` (plus existing `forge_list_timesheets`, `forge_approve_timesheets`, `forge_lock_pay_period`) |
| Team | `forge_list_team`, `forge_add_team_member`, `forge_invite_team_member`, `forge_update_team_member`, `forge_list_team_invitations`, `forge_revoke_team_invitation` |
| Team REST | `GET/POST /api/v1/team/members`, `PATCH /api/v1/team/members/{id}`, `GET/POST /api/v1/team/invitations`, `DELETE /api/v1/team/invitations/{id}` |

**Rules:** Never create a new time entry to fix times — use `forge_update_time_entry`. Locked pay periods block create/update/delete — unlock with `forge_lock_pay_period`. Approve crew corrections with `forge_approve_time_correction` (not raw entry edit unless owner override). To onboard crew for time entry: `forge_add_team_member` (immediate) or `forge_invite_team_member` (email login). Do not tell the user "team add is invite-only" — add is supported.

---

## Wave A.1 (2026-08-05) — ops complete

New MCP tools for dispatch + collections + quote lifecycle:

| Area | Tools |
|------|-------|
| Schedule / crew | `forge_get_schedule`, `forge_list_crew`, `forge_get_availability`, `forge_unassign_crew`, `forge_reschedule_visit`, `forge_list_unscheduled_jobs` |
| Reports / payroll | `forge_get_profitability_report`, `forge_get_labor_report`, `forge_get_revenue_report`, `forge_get_materials_report`, `forge_export_payroll` |
| AR / payments | `forge_list_payments`, `forge_get_payment`, `forge_void_payment`, `forge_list_overdue_invoices`, `forge_get_ar_summary`, `forge_send_payment_reminder`, `forge_batch_send_invoices`, `forge_get_invoice_public_link` |
| Quote lifecycle | `forge_approve_quote`, `forge_decline_quote`, `forge_mark_quote_sent`, `forge_convert_quote_to_job`, `forge_duplicate_quote` |
| Invoice lifecycle | `forge_send_invoice`, `forge_mark_invoice_sent`, `forge_revise_invoice`, `forge_void_invoice` |

**Quote → job path:** `forge_approve_quote` → `forge_convert_quote_to_job` (idempotent). Do not create a standalone job to bypass convert.

---

## Field aliases (agent footguns)

| Canonical | Aliases agents send (all accepted on create/update) |
|-----------|------------------------------------------------------|
| `billingEmail` | `email`, `billing_email`, `contactEmail`, `primaryEmail` |
| `billingPhone` | `phone`, `mobile`, `contactPhone`, `billing_phone` |
| `billingAddress` | `address`, `street`, `streetAddress` |
| `billingCity` | `city` |
| `billingProvince` | `province`, `state` |
| `billingPostal` | `postal`, `zip`, `postalCode` |
| `billingContactName` | `contactName`, `primaryContactName` |
| `name` | `company`, `companyName`, `displayName` |

There is **no** DB column `email` on `clients`. `{ email }` maps to `billingEmail`. Primary contact stays in sync.

### Quote line items (2026-08-05)
- Description-only `forge_update_quote` / `PATCH /quotes/:id` **merges** existing `unitPrice` / qty by line `id` or index — does not wipe pricing.
- Dirty money/qty accepted: `$2.80/sq ft`, `2,583`.
- Prefer multi-line `•` bullet scope with real newlines (not dash-run paragraphs).

---

## Full MCP tool inventory (120 tools)

### Always prefer for multi-step
| Tool | Write? | Notes |
|------|--------|-------|
| `forge_agent` | ✅ | PREFERRED entry point for multi-step Forge work. |

### Clients & contacts
| Tool | Write? | Notes |
|------|--------|-------|
| `forge_list_clients` |  | List/search clients by name or phone. ILIKE search can return WEAK hits — never use clients[0] just because search matched a shared word. |
| `forge_get_client` |  | Get client details including properties and contacts. |
| `forge_create_client` | ✅ | Create only when search returns nothing. Product Law blocks email/phone/name twins (409 + recovery). Pass `force`/`forceReason` only for intentional exceptions. |
| `forge_update_client` | ✅ | Update an existing client. Accepts client UUID (or pass id). |
| `forge_list_contacts` |  | List contacts for a client (people cards: name, email, phone, role, isPrimary). |
| `forge_create_contact` | ✅ | Add a person/contact under a client (name, email, phone, role). |
| `forge_update_contact` | ✅ | Update a client contact (email, phone, name, role, isPrimary). |
| `forge_delete_client` | ✅ | Move a client to trash (soft-delete). Recoverable via forge_restore or the Trash page. |

### Properties
| Tool | Write? | Notes |
|------|--------|-------|
| `forge_create_property` | ✅ | Create a property/site under an existing client. Use after forge_create_client (or when client exists but site is missing). |
| `forge_get_property` |  | Get full details for a single property by ID. Returns all address fields, contact fields |
| `forge_update_property` | ✅ | Update a property's details. Use this to update contact info (e.g. Robin Lee's phone/email on a specific site), |
| `forge_list_property_contacts` |  | List contacts assigned to a specific property. Returns each contact's name, phone, email, role, |
| `forge_assign_property_contact` | ✅ | Assign an existing client contact to a property. The contact must already exist on the client |
| `forge_delete_property` | ✅ | Move a property to trash (soft-delete). Use for junk test properties. Recoverable via forge_restore. |

### Quotes & deposits
| Tool | Write? | Notes |
|------|--------|-------|
| `forge_list_quotes` |  | List quotes. Filter with clientId (UUID). client_id alias also accepted. |
| `forge_create_quote` | ✅ | Create a professional house-style quote draft with line items and pricing. Returns the quote and a publicUrl. |
| `forge_update_quote` | ✅ | PRIMARY tool to edit an existing quote — line items, title, notes, discount, PO/refs, or status. |
| `forge_get_quote` |  | Get a specific quote by UUID or quote number (e.g. Q-0014), including public approval URL and line items. |
| `forge_send_quote` | ✅ | Email a quote to the client (Resend) with PDF + approval link. |
| `forge_delete_quote` | ✅ | Move a quote to trash (soft-delete). Accepts quote UUID or number (Q-0013). Recoverable via forge_restore. |
| `forge_request_deposit` | ✅ | Request a deposit on an approved or sent quote. ALWAYS creates a draft deposit invoice if none is open |
| `forge_approve_quote` | ✅ | Approve a quote programmatically (draft/sent/declined → approved). Idempotent if already approved. |
| `forge_decline_quote` | ✅ | Decline a quote (draft/sent/approved → declined). Idempotent if already declined. |
| `forge_mark_quote_sent` | ✅ | Mark a draft quote as sent WITHOUT emailing through Forge (external delivery: Outlook, print, portal). |
| `forge_convert_quote_to_job` | ✅ | Convert an APPROVED quote into a job (copies line items, client, property, sticky site fields). |
| `forge_duplicate_quote` | ✅ | Clone a quote into a new draft (new Q-number, fresh access token). Copies line items and notes. REST: `POST /api/v1/quotes/{id}/duplicate`. |

### Jobs, crew & photos
| Tool | Write? | Notes |
|------|--------|-------|
| `forge_list_jobs` |  | List/search jobs. ALWAYS call this (or forge_get_job with J-####) before creating a second job for the same client/PO. |
| `forge_get_job` |  | Get full job details including costs and time entries. |
| `forge_create_job` | ✅ | Create a NEW job only. If the user wants to CHANGE an existing job (J-####), use forge_update_job — never create. |
| `forge_update_job` | ✅ | Update an EXISTING job (preferred over create when user says change/fix/edit). |
| `forge_get_transitions` |  | Get valid next actions for a job, quote, or invoice (UUID or J-/Q-/INV- number). |
| `forge_delete_job` | ✅ | Move a job to trash (soft-delete). Accepts job UUID or number (J-0021). Prefer this over renaming duplicates. |
| `forge_assign_crew` | ✅ | Assign a crew member to a job for a work date (job_crew_assignments). |
| `forge_list_job_photos` |  | List photos attached to a job (id or J-number). |
| `forge_attach_job_photo` | ✅ | Attach a photo/PDF to a job. Prefer r2Key from a prior upload. |
| `forge_list_crew` |  | List active team/crew members for the tenant (userId, name, email, role, hourly rate). |
| `forge_unassign_crew` | ✅ | Remove a crew assignment from a job/work date. Pass assignmentId OR jobId+userId/userName+workDate. |
| `forge_list_unscheduled_jobs` |  | List active jobs that need scheduling: missing startDate, or no crew assignments / visits in the lookahead window. |

### Visits / schedule
| Tool | Write? | Notes |
|------|--------|-------|
| `forge_list_visits` |  | List schedule visits. Filter by jobId (UUID or J-####), status, from/to ISO dates. |
| `forge_create_visit` | ✅ | Schedule a visit on a job. Requires jobId (UUID or J-####) and scheduledStart (ISO). |
| `forge_update_visit` | ✅ | Update a visit by id. Fields: title, notes, scheduledStart/End, assignedUserIds, status |
| `forge_complete_visit` | ✅ | Mark a visit completed (sets status=completed + completedAt). Pass visit id. |
| `forge_get_schedule` |  | Get the field schedule for a date range: jobs overlapping the window, crew assignments, and visits. |
| `forge_get_availability` |  | Show who is already assigned on work dates (and optional visits) so you can pick free crew. |
| `forge_reschedule_visit` | ✅ | First-class visit reschedule: move scheduledStart/scheduledEnd (and optional assignees). |

### Job complete & costs
| Tool | Write? | Notes |
|------|--------|-------|
| `forge_complete_job` | ✅ | Mark job completed + completedAt. Prefer over `forge_update_job` status patch. Accepts J-####. Idempotent if already complete. |
| `forge_get_job_costs` |  | Labour $ (time × crew rate), materials[], receipts, revenue, margin. |
| `forge_add_job_cost` | ✅ | Add material/other cost line on job.materials[]. Use `forge_link_receipt` for photo receipts. |
| `forge_remove_job_cost` | ✅ | Remove material line by id or index from get_job_costs. |
| REST | | `GET/POST /api/v1/jobs/{id}/costs` (add or action=remove), `POST /api/v1/jobs/{id}/complete` |

**Rules:** Prefer `forge_complete_job` then `forge_convert_job_to_invoice`. Costs labour $ needs crew hourly rates set via `forge_add_team_member` / `forge_update_team_member`.

### Invoices, payments, AR & QB
| Tool | Write? | Notes |
|------|--------|-------|
| `forge_list_invoices` |  | List invoices. Filter by status, clientId, jobId (UUID or J-####), invoiceNumber, or search/q (PO, number, title). |
| `forge_get_invoice` |  | Get full invoice details including line items and payment history. Accepts invoice UUID or number. |
| `forge_create_invoice` | ✅ | Create a NEW draft invoice from scratch. NEVER use this to fix addresses/lines on an existing invoice (e.g. INV-0038) — that creates a duplicate number. Use for |
| `forge_convert_job_to_invoice` | ✅ | Convert a completed job into a draft invoice, pulling in the job's line items, PO number, work order number, and property. Links the invoice to the job and mark |
| `forge_update_invoice` | ✅ | Update existing invoice (same INV-####). Supports **targetTotal** (tax-inclusive $) to fix price without cloning. |
| `forge_revise_invoice` |  | PREFERRED way to fix a sent/issued invoice without creating a duplicate. |
| `forge_reopen_invoice` |  | Reopen an unpaid issued/emailed/overdue invoice back to draft so it can be edited. |
| `forge_void_invoice` | ✅ | Void an unpaid invoice (status voided, number retained). Prefer voiding a mistaken duplicate |
| `forge_send_invoice` | ✅ | Send an invoice to the client by email with a branded PDF attachment. Marks invoice as issued if it was a draft. Returns emailSent status. |
| `forge_record_payment` | ✅ | Record a payment received on an invoice. Supports full and partial payments. Auto-marks invoice as paid when balance reaches zero. |
| `forge_delete_invoice` | ✅ | Move an invoice to trash (soft-delete). Prefer void for issued invoices you want cancelled. Draft/test invoices can be trashed. |
| `forge_publish_invoice_qb` | ✅ | Push an invoice to QuickBooks Online (and push existing payments). |
| `forge_sync_invoice_qb_dates` | ✅ | Repair QuickBooks invoice TxnDate/DueDate from Forge issuedAt (fallback createdAt). |
| `forge_list_payments` |  | List payments received (optionally filter by invoiceId/INV-####, clientId, date range, method). |
| `forge_get_payment` |  | Get one payment by UUID, including parent invoice balance snapshot. |
| `forge_void_payment` | ✅ | Delete/void a recorded payment and recalculate invoice paidCents/status. |
| `forge_list_overdue_invoices` |  | List unpaid invoices past due (status issued/emailed/overdue/partial, dueAt < now, balance > 0). |
| `forge_get_ar_summary` |  | Accounts receivable aging snapshot: current, 1-30, 31-60, 61-90, 90+ days past due, plus totals and top overdue clients. |
| `forge_send_payment_reminder` | ✅ | Email a payment reminder for an overdue/unpaid invoice (reuses invoice email with reminder framing). |
| `forge_batch_send_invoices` | ✅ | Send multiple draft/ready invoices by email in one call. Pass invoiceIds (UUID or INV-####). |
| `forge_get_invoice_public_link` | ✅ | Return the client-facing public invoice URL (/i/{accessToken}) plus balance snapshot. |


### Customer messaging (SMS + freeform email)
| Tool | Write? | Notes |
|------|--------|-------|
| `forge_send_customer_sms` | ✅ | Freeform SMS to client (schedule / on-my-way / note). Resolves phone from client or job/quote/invoice/CO. Not for formal PDF sends. |
| `forge_send_customer_email` | ✅ | Freeform email (no quote/invoice PDF). For PDFs use forge_send_quote / forge_send_invoice. |
| `forge_list_customer_messages
- `forge_list_sms_conversations` / `forge_get_sms_thread` / `forge_mark_sms_read` — two-way SMS inbox + thread + mark read` |  | Recent outbound SMS/email from activity log. |
| `forge_send_quote_sms` | ✅ | Text quote e-sign link (marks draft→sent). |
| `forge_send_change_order_sms` | ✅ | Text change-order e-sign link. |

**REST:** `POST /api/v1/messages/sms`, `POST /api/v1/messages/email`, `GET /api/v1/messages`, `POST /api/v1/quotes/{id}/send-sms`, `POST /api/v1/change-orders/{id}/send-sms`.

### Change orders
| Tool | Write? | Notes |
|------|--------|-------|
| `forge_list_change_orders` |  | List change orders for the tenant, optionally filtered by jobId (UUID or J-####). Newest first. |
| `forge_get_change_order` |  | Get a change order by UUID or CO-#### number, including sign URL. |
| `forge_create_change_order` | ✅ | Create a draft change order on a job. Prefer playbook_change_order_from_brief. |
| `forge_update_change_order` | ✅ | Update a draft/sent change order (title, reason, notes, lineItems). Locked when approved or voided. |
| `forge_send_change_order` | ✅ | Email a change order to the client with e-sign link. Requires at least one line description. Needs approval unless auto-send is on. |

### Time & payroll
| Tool | Write? | Notes |
|------|--------|-------|
| `forge_list_timesheets` |  | List timesheet entries with optional date range and user filters. Returns employee, job, date, hours, break time, overtime flags, and approval status. |
| `forge_approve_timesheets` | ✅ | Approve time entries (sets approvedAt). Pass entryIds[], or userId + from/to ISO range. |
| `forge_lock_pay_period` | ✅ | Lock (or unlock) a bi-weekly pay period. periodStart = any YYYY-MM-DD inside the period |

### Receipts
| Tool | Write? | Notes |
|------|--------|-------|
| `forge_list_receipts` |  | List receipts and expenses uploaded to the account. Returns vendor, amount, category, job, billable flag, and review status. |
| `forge_update_receipt` | ✅ | Update a receipt: link jobId (UUID or J-####), set vendor/category/notes/billable/totals, |
| `forge_link_receipt` | ✅ | Link a receipt to a job (alias of forge_update_receipt with jobId). |
| `forge_approve_receipt` | ✅ | Approve a receipt after OCR review (alias of forge_update_receipt approve:true). |

### Search, memory, trash
| Tool | Write? | Notes |
|------|--------|-------|
| `forge_get_overview` |  | Get dashboard overview with KPIs and summary stats |
| `forge_search` |  | Search across clients, properties, jobs, and quotes. Use this FIRST when preparing a quote. |
| `forge_remember` | ✅ | Store a key-value pair in agent memory |
| `forge_recall` |  | Retrieve a stored value from agent memory |
| `forge_list_trash` |  | List soft-deleted items in the Forge trash (clients, properties, quotes, jobs, invoices). Use after creating junk test records. Nothing is hard-deleted — restor |
| `forge_restore` | ✅ | Restore a soft-deleted item from trash. entityType: client\|property\|quote\|job\|invoice. id accepts UUID or document number where applicable. |

### Other
| Tool | Write? | Notes |
|------|--------|-------|
| `forge_list_properties` |  | List/search properties (service sites). Filter by clientId for all sites under one client. |

---

## REST highlights (quote lifecycle)

| Method | Path | Notes |
|--------|------|-------|
| POST/PATCH | `/api/v1/quotes/:id/approve` | approve or decline |
| POST/PATCH | `/api/v1/quotes/:id/mark-sent` | external delivery, no email |
| POST | `/api/v1/quotes/:id/convert` | approved → job |
| GET | `/api/v1/invoices/:id/payments` | list payments |

MCP tools above wrap the same semantics for agent hosts.

### Reports & payroll export (2026-08-07)

| Tool | REST | Notes |
|------|------|-------|
| `forge_get_profitability_report` | `GET /api/v1/reports/profitability` | Job margins: revenue, labour, materials, profit % |
| `forge_get_labor_report` | `GET /api/v1/reports/labor` | Hours/cost by employee or job |
| `forge_get_revenue_report` | `GET /api/v1/reports/revenue` | 6-mo revenue + outstanding |
| `forge_get_materials_report` | `GET /api/v1/reports/materials` | Receipt spend + reimbursements |
| `forge_export_payroll` | `GET /api/v1/reports/payroll` | JSON or `format=csv`; OT × 1.5 est. pay |

Ask agents: "How did we do last month?", "Who worked the most hours?", "Export payroll for this period."

