feat(mcp): add stock_purchases.add_attachment tool
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
parent
ccf04bc39f
commit
f89bbb3a35
2 changed files with 99 additions and 0 deletions
|
|
@ -178,6 +178,7 @@ describe("POST /api/mcp", () => {
|
|||
"stock_purchases.receive",
|
||||
"stock_purchases.send_email",
|
||||
"stock_purchases.retry_email",
|
||||
"stock_purchases.add_attachment",
|
||||
// Sprint + buyer tasks (added 2026-07-30)
|
||||
"sprint.list",
|
||||
"sprint.attachment_counts",
|
||||
|
|
@ -420,6 +421,43 @@ describe("POST /api/mcp", () => {
|
|||
}
|
||||
});
|
||||
|
||||
it("tools/call stock_purchases.add_attachment decodes base64 and writes via the service helper", async () => {
|
||||
// The handler delegates to the service helper, which in turn hits
|
||||
// the admin PB. The top-level pocketbase-admin mock returns the
|
||||
// shared `getOne`/`update` mocks — queue one getOne (read before)
|
||||
// and one update (FormData write).
|
||||
getOne.mockResolvedValueOnce({ id: "sp-1", additional_attachments: [] });
|
||||
update.mockResolvedValueOnce({
|
||||
additional_attachments: ["fresh.pdf"],
|
||||
});
|
||||
|
||||
const res = await POST(
|
||||
mcpRequest({
|
||||
method: "tools/call",
|
||||
id: 63,
|
||||
params: {
|
||||
name: "stock_purchases.add_attachment",
|
||||
arguments: {
|
||||
purchase_id: "sp-1",
|
||||
filename: "invoice.pdf",
|
||||
content_type: "application/pdf",
|
||||
base64: Buffer.from("fake-pdf-bytes").toString("base64"),
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
expect(getOne).toHaveBeenCalledTimes(1);
|
||||
expect(update).toHaveBeenCalledTimes(1);
|
||||
const body = await res.json();
|
||||
const data = JSON.parse(body.result.content[0].text);
|
||||
expect(data).toMatchObject({
|
||||
ok: true,
|
||||
attachments: ["fresh.pdf"],
|
||||
added: ["fresh.pdf"],
|
||||
});
|
||||
});
|
||||
|
||||
it("tools/call orders.list_attachments proxies to the matched-attachments route", async () => {
|
||||
const fetchMock = vi.fn(async () =>
|
||||
new Response(
|
||||
|
|
|
|||
|
|
@ -1157,6 +1157,29 @@ const tools = [
|
|||
properties: { id: { type: "string" } },
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "stock_purchases.add_attachment",
|
||||
description:
|
||||
"Upload a single free-form attachment (supplier invoice, packing slip, photo of damaged stock) to an existing stock purchase. The file is appended to `stock_purchases.additional_attachments`. Call once per file. Max 25 MB per file. The versioned-PDF `documents` field is intentionally NOT touched.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
required: ["purchase_id", "filename", "base64"],
|
||||
properties: {
|
||||
purchase_id: {
|
||||
type: "string",
|
||||
description: "15-char PocketBase stock_purchase id",
|
||||
},
|
||||
filename: { type: "string" },
|
||||
content_type: { type: "string" },
|
||||
size: { type: "number" },
|
||||
base64: {
|
||||
type: "string",
|
||||
description:
|
||||
"Raw file bytes encoded as base64 (no `data:` URL prefix).",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sprint.list",
|
||||
description:
|
||||
|
|
@ -2781,6 +2804,44 @@ export async function POST(req: NextRequest) {
|
|||
return jsonRpcOk(body.id, result);
|
||||
}
|
||||
|
||||
if (toolName === "stock_purchases.add_attachment") {
|
||||
if (!args.purchase_id || !args.filename || !args.base64) {
|
||||
throw new Error(
|
||||
"Missing purchase_id, filename, or base64 for stock_purchases.add_attachment",
|
||||
);
|
||||
}
|
||||
const { addStockPurchaseAttachment } = await import(
|
||||
"@/lib/server/stock-purchases-service"
|
||||
);
|
||||
const bytes = Buffer.from(String(args.base64), "base64");
|
||||
const file = new File([new Uint8Array(bytes)], String(args.filename), {
|
||||
type:
|
||||
typeof args.content_type === "string"
|
||||
? args.content_type
|
||||
: "application/octet-stream",
|
||||
});
|
||||
const result = await addStockPurchaseAttachment({
|
||||
purchaseId: String(args.purchase_id),
|
||||
file,
|
||||
});
|
||||
return NextResponse.json({
|
||||
jsonrpc: "2.0",
|
||||
id: body.id ?? null,
|
||||
result: {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
ok: true,
|
||||
attachments: result.attachments,
|
||||
added: result.added,
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (toolName === "stock_purchases.next_number") {
|
||||
const purchase_number = await generateStockPurchaseNumber(pb);
|
||||
return jsonRpcOk(body.id, { purchase_number });
|
||||
|
|
|
|||
Loading…
Reference in a new issue