Skip to content

feat(ai-tools): add get_quote / list_quotes read tools + reject empty update patch (#2361) - #2373

Merged
ToddHebebrand merged 2 commits into
mainfrom
feat/2361-quote-read-tools
Jul 12, 2026
Merged

feat(ai-tools): add get_quote / list_quotes read tools + reject empty update patch (#2361)#2373
ToddHebebrand merged 2 commits into
mainfrom
feat/2361-quote-read-tools

Conversation

@ToddHebebrand

Copy link
Copy Markdown
Collaborator

Summary

Stacked on #2370 (base branch = fix/2362-manage-quotes-validation) so the diff shows only the #2361 work. Merge #2370 first, then retarget/merge this one.

manage_quotes exposed the full quote write surface (header, blocks, lines, deposits, send/decline/pay-link) with no read tools — inconsistent with siblings (invoices: list_invoices/get_invoice; contracts: list_contracts/get_contract). The model could not discover a quote or see its blocks/lines, and the only "read" workaround was {action:"update", patch:{}} — a write used as a read that bumped updatedAt.

Changes

  • list_quotes — org/status filters, newest first, copying the list_contracts pattern. Filters are validated with the shared listQuotesQuerySchema (the same schema the GET /quotes route uses), so a bad filter returns a structured VALIDATION_ERROR. Returns { quotes, showing }.
  • get_quote — full view: header (with derived dueOnAcceptanceTotal/depositDueTotal/categoryBreakdown), blocks, and lines, reusing the existing getQuote service the web UI reads. No new SQL.
  • Sibling-list registration (the parity contract test enforces all of these):
    • tool registry: registerQuoteTools (aiToolsQuotes.ts), tier 2 / deviceArgs: [] like get_contract;
    • toolInputSchemas (aiToolSchemas.ts);
    • TOOL_PERMISSIONS (aiGuardrails.ts) → quotes:read.
    • TOOL_TIERS (aiAgentSdkTools.ts) is deliberately not touched: the billing-domain siblings (list_contracts, get_contract, get_invoice, list_invoices, manage_quotes itself) are all absent from it — that map only curates the in-app SDK agent's offered toolset; the MCP server path reads the registry tier directly.
  • Empty-patch update: {action:"update", patch:{}} now returns {"error":"patch is empty — nothing to update. Use get_quote to read a quote.","code":"VALIDATION_ERROR"} before any UPDATE runs, so a no-op can no longer dirty updatedAt. I chose the reject-with-VALIDATION_ERROR option (over silent short-circuit) because the empty patch was only ever used as a read workaround — rejecting steers the model to the new get_quote, and a silent success would hide caller bugs.

Tests

  • aiToolsQuotes.test.ts: list_quotes default limit + filter forwarding + invalid-status VALIDATION_ERROR; get_quote full view + missing quoteId + QUOTE_NOT_FOUND mapping; empty-patch rejection asserting updateQuote is never called.
  • Contract sweeps green: aiToolsRegistryParity (schema + RBAC parity for every registered tool), aiTools.deviceArgsCoverage.contract, aiTools.deviceAccessSiteScope.contract, aiGuardrails (97 tests).
  • tsc --noEmit --project apps/api/tsconfig.json clean.

Closes #2361

🤖 Generated with Claude Code

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 12, 2026

Copy link
Copy Markdown

Deploying breeze with  Cloudflare Pages  Cloudflare Pages

Latest commit: ec365cb
Status: ✅  Deploy successful!
Preview URL: https://e0da2fcf.breeze-9te.pages.dev
Branch Preview URL: https://feat-2361-quote-read-tools.breeze-9te.pages.dev

View logs

@ToddHebebrand

Copy link
Copy Markdown
Collaborator Author

Review run: /code-review (high effort, inline finder angles: line-by-line diff scan, removed-behavior audit, cross-file caller trace, reuse/simplification, CLAUDE.md conventions).
Findings: 1 raised → fixed in 3a449f7; 0 outstanding. (Empty-header-patch error message was misleading when the patch contained only unknown/line-level fields, which updateQuoteSchema strips before the check — message now points at update_line as well as get_quote.)
Registration parity verified: registry + toolInputSchemas + TOOL_PERMISSIONS (the aiToolsRegistryParity contract test enforces all three); TOOL_TIERS deliberately untouched to match the billing-domain siblings (list_contracts/get_contract/get_invoice are all absent from it — it only curates the in-app SDK agent's toolset).
Tests: aiToolsQuotes (25), aiToolsRegistryParity, aiTools.deviceArgsCoverage.contract, aiTools.deviceAccessSiteScope.contract (39), aiGuardrails (97) all green; tsc --noEmit --project apps/api/tsconfig.json clean.
Status: review-clean, awaiting maintainer merge. Stacked on #2370 (base = fix/2362-manage-quotes-validation) — merge #2370 first, then retarget this PR to main.

Todd Hebebrand and others added 2 commits July 12, 2026 11:56
… update patch (#2361)

manage_quotes exposed the full quote write surface with no read tools —
unlike invoices (list_invoices/get_invoice) and contracts
(list_contracts/get_contract). The model could not discover a quote or
see its blocks/lines; the only workaround was an empty-patch update,
a write used as a read that bumped updatedAt.

- list_quotes: org/status filters, newest first, mirroring
  list_contracts; validates filters with the shared
  listQuotesQuerySchema the GET /quotes route uses.
- get_quote: full view (header with derived totals/deposit breakdown +
  blocks + lines) reusing the same getQuote service the web UI reads —
  no new SQL.
- Registered in every sibling list: aiTools registry
  (registerQuoteTools), toolInputSchemas (aiToolSchemas.ts), and
  TOOL_PERMISSIONS (aiGuardrails.ts, quotes:read). TOOL_TIERS
  (aiAgentSdkTools.ts) intentionally NOT touched — the billing-domain
  siblings are absent from it by design (it only curates the in-app SDK
  agent's offered toolset).
- manage_quotes {action:"update", patch:{}} now returns a structured
  VALIDATION_ERROR pointing at get_quote instead of running a no-field
  UPDATE that dirtied updatedAt.

Tests: list_quotes filter/limit forwarding + invalid-status
VALIDATION_ERROR, get_quote full-view + missing-quoteId +
QUOTE_NOT_FOUND mapping, empty-patch rejection (asserts updateQuote is
never called). Registry-parity, deviceArgs-coverage, site-scope and
aiGuardrails contract suites green.

Closes #2361

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nown/line-level fields

Review finding: updateQuoteSchema strips unknown keys before the
empty-patch check, so a patch of only line-level fields (e.g.
depositEligible) read as 'patch is empty'. Message now points at
update_line as well as get_quote.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ToddHebebrand
ToddHebebrand force-pushed the feat/2361-quote-read-tools branch from 3a449f7 to ec365cb Compare July 12, 2026 17:57
@ToddHebebrand
ToddHebebrand merged commit 879188e into main Jul 12, 2026
41 checks passed
@ToddHebebrand
ToddHebebrand deleted the feat/2361-quote-read-tools branch July 12, 2026 19:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[MCP] Quotes have full write tools but no read tools (get_quote / list_quotes)

1 participant