@oneshot-agent/mcp-server 0.39.0 → 0.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,105 @@
1
+ /**
2
+ * The two Physical Mail steps that a generated route tool cannot carry (#1883): the send,
3
+ * which needs an idempotency key header beside the quote id and the approval id, and the
4
+ * artwork upload, whose body is a raw PDF, PNG or JPEG. Both go through the SDK's
5
+ * `physicalMail` (libs/agent-sdk/src/physical-mail.ts). The other seven routes (validate,
6
+ * preview, quote, approve, recover, order, cancel) are generated from the declaration.
7
+ */
8
+ /** The route's own limit (`uploadArtwork` in services/physical-mail/service.ts): 20 MB. */
9
+ export const PHYSICAL_MAIL_ASSET_LIMIT_BYTES = 20 * 1024 * 1024;
10
+ export const PHYSICAL_MAIL_ASSET_TYPES = ["application/pdf", "image/png", "image/jpeg"];
11
+ export const physicalMailSendTool = {
12
+ name: "oneshot_physical_mail_send",
13
+ description: "Print and post an approved mailpiece. Paid: the quote's total_usdc is charged (see Pricing), and the receipt is on the order. " +
14
+ "Needs the quote_id from oneshot_physical_mail_preview and the approval_id from oneshot_physical_mail_approve. " +
15
+ "Give an idempotency_key you keep: a retry with the same key returns the same order instead of sending twice, and " +
16
+ "oneshot_physical_mail_recover finds the order by it after a timeout. Returns the order (order_id, order_status, " +
17
+ "payment_status, receipt_id). Delivery is a postal observation, never evidence the mail was read.",
18
+ inputSchema: {
19
+ type: "object",
20
+ properties: {
21
+ quote_id: { type: "string", description: "Quote id from oneshot_physical_mail_preview" },
22
+ approval_id: { type: "string", description: "Approval id from oneshot_physical_mail_approve (the quote's own approval)" },
23
+ idempotency_key: {
24
+ type: "string",
25
+ description: "A key you persist for this send (1-255 characters: letters, digits, _ . : -). Reuse it to retry safely.",
26
+ },
27
+ memo: { type: "string", description: "Why this mail is being sent (audit trail, max 1000 characters; optional)" },
28
+ max_cost: { type: "number", description: "Refuse to pay more than this many USDC (optional)" },
29
+ policy_approval_id: {
30
+ type: "string",
31
+ description: "Only when retrying a send the action policy held: the approval_id from the approval_required error, once a human approved it. Not the quote's approval_id.",
32
+ },
33
+ },
34
+ required: ["quote_id", "approval_id", "idempotency_key"],
35
+ },
36
+ };
37
+ export async function handlePhysicalMailSend(agent, args) {
38
+ return agent.physicalMail.send({
39
+ quote_id: args.quote_id,
40
+ approval_id: args.approval_id,
41
+ idempotencyKey: args.idempotency_key,
42
+ memo: args.memo,
43
+ maxCost: args.max_cost,
44
+ approvalId: args.policy_approval_id,
45
+ });
46
+ }
47
+ export const physicalMailUploadTool = {
48
+ name: "oneshot_physical_mail_upload",
49
+ description: "Upload print-ready artwork for a letter or postcard and get the asset id that oneshot_physical_mail_preview takes in " +
50
+ "artwork.file (a letter needs a PDF) or artwork.front and artwork.back (a postcard). Free. The file is sent as base64; " +
51
+ "it must decode to at most 20 MB. An asset is immutable: upload a new one to change the artwork.",
52
+ inputSchema: {
53
+ type: "object",
54
+ properties: {
55
+ content: { type: "string", description: "The file, base64-encoded (standard or URL-safe alphabet, padding optional)" },
56
+ content_type: { type: "string", enum: [...PHYSICAL_MAIL_ASSET_TYPES], description: "The file's media type" },
57
+ },
58
+ required: ["content", "content_type"],
59
+ },
60
+ };
61
+ /** Decodes the base64 `content`, refusing anything that is not base64 or that would exceed the route's limit before any bytes are sent. */
62
+ export function decodeArtwork(args) {
63
+ const mime = args.content_type;
64
+ if (!PHYSICAL_MAIL_ASSET_TYPES.includes(mime)) {
65
+ throw new Error(`oneshot_physical_mail_upload: content_type must be one of ${PHYSICAL_MAIL_ASSET_TYPES.join(", ")}`);
66
+ }
67
+ if (typeof args.content !== "string" || args.content.length === 0)
68
+ throw new Error("oneshot_physical_mail_upload: content is required");
69
+ const text = args.content.replace(/\s+/g, "");
70
+ const size = base64Size(text);
71
+ if (size === null)
72
+ throw new Error("oneshot_physical_mail_upload: content is not base64");
73
+ if (size > PHYSICAL_MAIL_ASSET_LIMIT_BYTES) {
74
+ throw new Error(`oneshot_physical_mail_upload: the file is ${size} bytes; the limit is ${PHYSICAL_MAIL_ASSET_LIMIT_BYTES} bytes (20 MB)`);
75
+ }
76
+ const data = Buffer.from(text, "base64");
77
+ if (data.length === 0)
78
+ throw new Error("oneshot_physical_mail_upload: content decodes to nothing");
79
+ // The decoder is lenient; the structural check above is what bounds the size, and this guards the limit regardless.
80
+ if (data.length > PHYSICAL_MAIL_ASSET_LIMIT_BYTES) {
81
+ throw new Error(`oneshot_physical_mail_upload: the file is ${data.length} bytes; the limit is ${PHYSICAL_MAIL_ASSET_LIMIT_BYTES} bytes (20 MB)`);
82
+ }
83
+ return { data: new Uint8Array(data), mime: mime };
84
+ }
85
+ /**
86
+ * The decoded size of structurally valid base64 (standard or URL-safe alphabet, padding optional), or null.
87
+ * `Buffer.from(text, "base64")` tolerates a length one past a quartet, missing, short and excess padding
88
+ * (`AAAAA`, `AAA==`, `AAAA==`), so counting characters and subtracting the `=`s would undercount such
89
+ * input; the bound has to come from a shape the decoder cannot read differently.
90
+ */
91
+ export function base64Size(text) {
92
+ if (!/^[A-Za-z0-9+/_-]*={0,2}$/.test(text))
93
+ return null;
94
+ const body = text.replace(/=+$/, "");
95
+ const padding = text.length - body.length;
96
+ const rem = body.length % 4;
97
+ // A body is never one character past a quartet, and padding, when given, is exactly what completes the last one.
98
+ if (body.length === 0 || rem === 1 || (padding > 0 && padding !== (4 - rem) % 4))
99
+ return null;
100
+ return Math.floor((body.length * 3) / 4);
101
+ }
102
+ export async function handlePhysicalMailUpload(agent, args) {
103
+ const { data, mime } = decodeArtwork(args);
104
+ return agent.physicalMail.uploadArtwork(data, mime);
105
+ }