diff --git a/src/app/api/mcp/route.test.ts b/src/app/api/mcp/route.test.ts index 79e6ef1..7a7c7bb 100644 --- a/src/app/api/mcp/route.test.ts +++ b/src/app/api/mcp/route.test.ts @@ -229,6 +229,9 @@ const toolNames = body.result.tools.map((t: { name: string }) => t.name); // Hermes unblock (PR 1, 2026-08-21): // set_quote writes the orders.quote relation for an existing order "orders.set_quote", + // Backlog sweep for the historical orders the intake matcher + // never saw (2026-08-30). + "orders.quote_match_sweep", "orders.add_note", "collection_pool.list", "order_items.update_status", diff --git a/src/app/api/mcp/route.ts b/src/app/api/mcp/route.ts index 94e6f71..c560bad 100644 --- a/src/app/api/mcp/route.ts +++ b/src/app/api/mcp/route.ts @@ -870,6 +870,31 @@ const tools = [ }, }, }, + { + name: "orders.quote_match_sweep", + description: + "Run quote auto-detection over orders that have never been attempted. Mirrors the REST " + + "POST /api/v1/orders/quote-match/sweep endpoint.\n\n" + + "Why this exists: orders created from now on are matched automatically at intake. This is the " + + "catch-up path for the historical backlog and for orders whose line items changed after " + + "creation.\n\n" + + "Writes ONLY the soft suggestion (`potential_quote` + `potential_quote_confidence`). It never " + + "writes the hard `orders.quote` link — promoting a suggestion is a deliberate human action via " + + "`orders.set_quote`.\n\n" + + "Processes one batch per call. Re-run until the returned `remaining` is 0.\n\n" + + "Returns `{ processed, suggested, noMatch, failed, remaining, ageGaps }`. `ageGaps` buckets the " + + "quote-to-order age in days for every match found (d0_30, d31_60, d61_90, d91_180, d181_365, " + + "d365_plus, total) — use it to check whether the 90-day fuzzy search window is set correctly.", + inputSchema: { + type: "object", + properties: { + limit: { + type: "number", + description: "Orders to process in this batch. 1..500, default 100.", + }, + }, + }, + }, { name: "orders.add_note", description: @@ -3922,6 +3947,44 @@ export async function POST(req: NextRequest) { }); } + if (toolName === "orders.quote_match_sweep") { + // Forward to the REST route — keeps the surface in lock-step + // with the agent-auth route and surfaces the structured 4xx/5xx + // data without re-implementing the error envelope here. + const limit = typeof args.limit === "number" ? args.limit : undefined; + const upstreamRes = await fetch( + `${upstreamOrigin(req)}/api/v1/orders/quote-match/sweep`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + authorization: `Bearer ${process.env.AGENT_API_KEY ?? ""}`, + }, + body: JSON.stringify(limit === undefined ? {} : { limit }), + }, + ); + const data = await upstreamRes.json().catch(() => ({})); + if (!upstreamRes.ok) { + return NextResponse.json( + { + jsonrpc: "2.0", + id: body.id ?? null, + error: { + code: -32010, + message: `orders.quote_match_sweep upstream ${upstreamRes.status}: ${data?.error ?? "unknown"}`, + data, + }, + }, + { status: 200 }, + ); + } + return NextResponse.json({ + jsonrpc: "2.0", + id: body.id ?? null, + result: { content: [{ type: "text", text: JSON.stringify(data) }] }, + }); + } + if (toolName === "orders.add_note") { const orderId = String(args.id || "").trim(); if (!orderId) throw new Error("Missing id for orders.add_note.");