feat(mcp): expose orders.quote_match_sweep

Hermes (n8n) drops the AGENT_API_KEY on direct REST calls, so anything
n8n needs to run must exist as an MCP tool. This forwards the agent's
JSON-RPC call to POST /api/v1/orders/quote-match/sweep, mirroring the
existing orders.set_quote forwarding pattern.

Updated the tools/list expected-list test to include the new tool so
the catalog stays self-documenting.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Dave 2026-08-30 08:46:28 +02:00
parent 4e54fd6218
commit e374513fba
2 changed files with 66 additions and 0 deletions

View file

@ -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",

View file

@ -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.");