@oneshot-agent/mcp-server 0.39.1 → 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.
package/README.md CHANGED
@@ -102,7 +102,7 @@ Read tools (inbox, SMS inbox, notifications, balance, browser profiles) return p
102
102
  ## Remote (hosted)
103
103
 
104
104
  Clients that run in someone else's cloud (Grok Bot, hosted runners) cannot spawn this
105
- server and must not hold a wallet key. For them, OneShot hosts the same 117 tools over
105
+ server and must not hold a wallet key. For them, OneShot hosts the same tool registry over
106
106
  Streamable HTTP at **`https://win.oneshotagent.com/mcp`**, authenticated with an
107
107
  **agent access token** and billed to the agent's credit balance:
108
108
 
@@ -169,8 +169,9 @@ Soul Hunt is a separate service (`api.soulhunt.ai`) from the main OneShot API; t
169
169
  | Tool | Description |
170
170
  |------|-------------|
171
171
  | `oneshot_research` | Deep web research with sources |
172
+ | `oneshot_solve` | Plan vehicle routes with constraint checks and conflicts |
172
173
  | `oneshot_person_find` | Search for people by title, company, etc. |
173
- | `oneshot_person_profile` | Enrich person profile from LinkedIn/email |
174
+ | `oneshot_person_profile` | Enrich person profile from LinkedIn URL, email, or name + company |
174
175
  | `oneshot_person_email` | Find someone's email address |
175
176
  | `oneshot_person_email_status` | Verify email deliverability |
176
177
  | `oneshot_company_find` | Search for companies by industry, size, location, etc. |
@@ -205,6 +206,22 @@ Soul Hunt is a separate service (`api.soulhunt.ai`) from the main OneShot API; t
205
206
  | `oneshot_product_find` | Search for products |
206
207
  | `oneshot_commerce_buy` | Purchase products |
207
208
 
209
+ ### Physical Mail
210
+
211
+ Quote-to-pay with an explicit approval in between. Prices are on the [Pricing](https://docs.oneshotagent.com/pricing) page; the quote carries the price of each piece.
212
+
213
+ | Tool | Description |
214
+ |------|-------------|
215
+ | `oneshot_physical_mail_upload` | Upload print-ready artwork (PDF, PNG or JPEG as base64, 20 MB max) and get its asset id |
216
+ | `oneshot_physical_mail_validate_address` | Check that a U.S. address is deliverable |
217
+ | `oneshot_physical_mail_preview` | Quote a letter or postcard and render an uncharged proof |
218
+ | `oneshot_physical_mail_quote` | Read a quote |
219
+ | `oneshot_physical_mail_approve` | Approve the exact piece and price of a quote |
220
+ | `oneshot_physical_mail_send` | Print and post an approved piece (quote id, approval id and an idempotency key you keep) |
221
+ | `oneshot_physical_mail_recover` | Find the order a send created, by its idempotency key |
222
+ | `oneshot_physical_mail_order` | Read an order and its status |
223
+ | `oneshot_physical_mail_cancel` | Ask to cancel an order (not guaranteed once submitted) |
224
+
208
225
  ### Build
209
226
 
210
227
  | Tool | Description |
package/dist/index.js CHANGED
@@ -50,7 +50,7 @@ async function main() {
50
50
  const agent = await initAgent();
51
51
  const server = new Server({
52
52
  name: "oneshot-mcp",
53
- version: "0.39.1", // keep in sync with package.json; guarded by version.test.ts
53
+ version: "0.40.0", // keep in sync with package.json; guarded by version.test.ts
54
54
  }, {
55
55
  capabilities: {
56
56
  tools: {},
@@ -7,6 +7,7 @@ const OPEN = true;
7
7
  const OWN = false;
8
8
  const DESTRUCTIVE = true;
9
9
  export const TOOL_ANNOTATIONS = {
10
+ oneshot_solve: read("Solve", OWN),
10
11
  oneshot_hunt_list: read("List Soul Hunts", OPEN),
11
12
  oneshot_hunt_status: read("Get Soul Hunt Status", OPEN),
12
13
  oneshot_hunt_predict: write("Predict Soul Hunt", OPEN),
@@ -24,6 +25,10 @@ export const TOOL_ANNOTATIONS = {
24
25
  oneshot_voice_inbound_set: write("Set up call answering", OPEN, DESTRUCTIVE), // replaces the number's answering settings; outside callers reach it via the carrier
25
26
  oneshot_voice_inbound_disable: write("Stop answering calls", OPEN),
26
27
  oneshot_voice_inbound_calls: read("List answered calls", OWN),
28
+ // Physical Mail (#1883): the upload stores artwork in the agent's own account; the send puts a letter in the post.
29
+ // The generated route tools (validate, preview, quote, approve, recover, order, cancel) get their hints from the manifest below.
30
+ oneshot_physical_mail_upload: write("Upload mail artwork", OWN),
31
+ oneshot_physical_mail_send: write("Send approved physical mail", OPEN),
27
32
  oneshot_research: read("Deep research", OPEN),
28
33
  oneshot_person_find: read("Find people", OPEN),
29
34
  oneshot_person_profile: read("Look up a person's profile", OPEN),
@@ -21,6 +21,10 @@ export const personProfileTool = {
21
21
  type: "string",
22
22
  description: "Company domain for additional context",
23
23
  },
24
+ company_name: {
25
+ type: "string",
26
+ description: "Employer name, for a name lookup when the company domain is not known (e.g. Coinbase)",
27
+ },
24
28
  },
25
29
  },
26
30
  };
@@ -71,6 +75,7 @@ export async function handlePersonProfile(agent, args) {
71
75
  email: args.email,
72
76
  name: args.name,
73
77
  company_domain: args.company_domain,
78
+ company_name: args.company_name,
74
79
  });
75
80
  }
76
81
  export async function handlePersonEmail(agent, args) {
@@ -2,6 +2,7 @@ import { emailTool, handleEmail } from "./email.js";
2
2
  import { voiceTool, handleVoice } from "./voice.js";
3
3
  import { voiceNumberProvisionTool, voiceNumbersTool, voiceInboundSetTool, voiceInboundDisableTool, voiceInboundCallsTool, handleVoiceNumberProvision, handleVoiceNumbers, handleVoiceInboundSet, handleVoiceInboundDisable, handleVoiceInboundCalls, } from "./voice-inbound.js";
4
4
  import { smsTool, handleSms } from "./sms.js";
5
+ import { solveTool, handleSolve } from "./solve.js";
5
6
  import { researchTool, handleResearch } from "./research.js";
6
7
  import { personFindTool, handlePersonFind } from "./people.js";
7
8
  import { productFindTool, commerceBuyTool, handleProductFind, handleCommerceBuy } from "./commerce.js";
@@ -25,6 +26,7 @@ import { listDomainsTool, pauseDomainTool, resumeDomainTool, handleListDomains,
25
26
  import { huntListTool, huntStatusTool, huntPredictTool, huntCollectionTool, huntLeaderboardTool, handleHuntList, handleHuntStatus, handleHuntPredict, handleHuntCollection, handleHuntLeaderboard } from "./hunt.js";
26
27
  import { TOOL_ANNOTATIONS } from "./annotations.js";
27
28
  import { lookupToolDefinitions, lookupHandlers, routeToolDefinitions, routeHandlers } from "./lookup.js";
29
+ import { physicalMailSendTool, physicalMailUploadTool, handlePhysicalMailSend, handlePhysicalMailUpload } from "./physical-mail.js";
28
30
  import { spendBreakdownTool, rocsTool, rocsByGoalTool, receiptsListTool, tagReceiptValueTool, handleSpendBreakdown, handleRocs, handleRocsByGoal, handleReceiptsList, handleTagReceiptValue } from "./analytics.js";
29
31
  const toolDefinitions = [
30
32
  emailTool,
@@ -39,6 +41,9 @@ const toolDefinitions = [
39
41
  voiceInboundSetTool,
40
42
  voiceInboundDisableTool,
41
43
  voiceInboundCallsTool,
44
+ solveTool,
45
+ physicalMailUploadTool,
46
+ physicalMailSendTool,
42
47
  researchTool,
43
48
  personFindTool,
44
49
  personProfileTool,
@@ -129,6 +134,9 @@ const handlers = {
129
134
  "oneshot_voice_inbound_set": handleVoiceInboundSet,
130
135
  "oneshot_voice_inbound_disable": handleVoiceInboundDisable,
131
136
  "oneshot_voice_inbound_calls": handleVoiceInboundCalls,
137
+ "oneshot_solve": handleSolve,
138
+ "oneshot_physical_mail_upload": handlePhysicalMailUpload,
139
+ "oneshot_physical_mail_send": handlePhysicalMailSend,
132
140
  "oneshot_research": handleResearch,
133
141
  "oneshot_person_find": handlePersonFind,
134
142
  "oneshot_person_profile": handlePersonProfile,
@@ -111,6 +111,29 @@ export const LOOKUP_MANIFEST = [
111
111
  "limit": 1
112
112
  }
113
113
  },
114
+ {
115
+ "name": "compound_properties",
116
+ "path": "lookup/compound-properties",
117
+ "title": "Compound Properties",
118
+ "description": "Give the computed molecular descriptors of a chemical compound by its compound identifier (CID) or its InChIKey: the IUPAC name, molecular formula, InChIKey, molecular weight in g/mol, exact mass in Da, XLogP, topological polar surface area in square angstroms, the counts of hydrogen bond donors and acceptors, rotatable bonds and heavy atoms, the formal charge and the complexity score, with a link to the compound page. These are values the source computes from the structure, not measurements. Use it to characterise a compound in a literature review, a screening library or a regulatory file. Give exactly one of `cid` and `inchikey`. `compound` is null when the source knows no compound with that identifier. Synonyms, depositor records, and any synthesis or preparation data are not returned.",
119
+ "inputSchema": {
120
+ "type": "object",
121
+ "properties": {
122
+ "cid": {
123
+ "type": "integer",
124
+ "description": "The compound identifier (CID), for example 2244. From 1 to 999999999."
125
+ },
126
+ "inchikey": {
127
+ "type": "string",
128
+ "description": "The standard InChIKey of the compound, for example BSYNRYMUTXBXSQ-UHFFFAOYSA-N. Exactly 27 characters. An InChIKey is 14 capitals, a hyphen, 10 capitals, a hyphen and one capital."
129
+ }
130
+ },
131
+ "required": []
132
+ },
133
+ "example": {
134
+ "cid": 2244
135
+ }
136
+ },
114
137
  {
115
138
  "name": "compound_resolve",
116
139
  "path": "resolve/compound",
@@ -269,6 +292,42 @@ export const LOOKUP_MANIFEST = [
269
292
  "variant": "FWD"
270
293
  }
271
294
  },
295
+ {
296
+ "name": "product_device_clearances",
297
+ "path": "lookup/product-device-clearances",
298
+ "title": "Device Clearances",
299
+ "description": "Look up medical devices cleared by the US Food and Drug Administration through premarket notification (510(k)). Supply at least one clearance number, applicant, device name or product code. Names are matched as word phrases; multiple filters must all match. Returns the total matches and clearance records, newest decision first, with device classifications and decision and received dates. Contact names and street addresses are excluded. No match returns an empty list. The source refreshes weekly.",
300
+ "inputSchema": {
301
+ "type": "object",
302
+ "properties": {
303
+ "k_number": {
304
+ "type": "string",
305
+ "description": "The clearance number, for example K253582"
306
+ },
307
+ "applicant": {
308
+ "type": "string",
309
+ "description": "The company that applied, matched as words. 2 to 100 characters."
310
+ },
311
+ "device_name": {
312
+ "type": "string",
313
+ "description": "The device name, matched as words. 2 to 100 characters."
314
+ },
315
+ "product_code": {
316
+ "type": "string",
317
+ "description": "The FDA product code"
318
+ },
319
+ "limit": {
320
+ "type": "integer",
321
+ "description": "The most clearance records to return. From 1 to 25. Defaults to `10`."
322
+ }
323
+ },
324
+ "required": []
325
+ },
326
+ "example": {
327
+ "applicant": "Apple Inc",
328
+ "limit": 2
329
+ }
330
+ },
272
331
  {
273
332
  "name": "place_disasters",
274
333
  "path": "lookup/place-disasters",
@@ -1357,6 +1416,48 @@ export const ROUTE_MANIFEST = [
1357
1416
  "profile_id": "prof_01HX"
1358
1417
  }
1359
1418
  },
1419
+ {
1420
+ "name": "physical_mail_approve",
1421
+ "method": "POST",
1422
+ "path": "/v1/tools/physical-mail/approve",
1423
+ "paid": false,
1424
+ "readOnly": false,
1425
+ "title": "Approve a Mail Quote",
1426
+ "description": "Approve the exact mailpiece and price of a quote. Repeat the input_hash and total_usdc the quote returned: a changed piece or price is refused. Nothing is charged yet. Returns approval_id, which oneshot_physical_mail_send needs with the quote_id.",
1427
+ "inputSchema": {
1428
+ "type": "object",
1429
+ "properties": {
1430
+ "quote_id": {
1431
+ "type": "string",
1432
+ "description": "Quote id from oneshot_physical_mail_preview"
1433
+ },
1434
+ "input_hash": {
1435
+ "type": "string",
1436
+ "description": "input_hash the quote returned (64 hex characters)"
1437
+ },
1438
+ "total_usdc": {
1439
+ "type": "string",
1440
+ "description": "total_usdc the quote returned, as a decimal string"
1441
+ },
1442
+ "approved": {
1443
+ "type": "boolean",
1444
+ "description": "Must be true: an explicit approval of this piece at this price"
1445
+ }
1446
+ },
1447
+ "required": [
1448
+ "quote_id",
1449
+ "input_hash",
1450
+ "total_usdc",
1451
+ "approved"
1452
+ ]
1453
+ },
1454
+ "example": {
1455
+ "quote_id": "00000000-0000-4000-8000-000000000000",
1456
+ "input_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
1457
+ "total_usdc": "1.85",
1458
+ "approved": true
1459
+ }
1460
+ },
1360
1461
  {
1361
1462
  "name": "benchmark",
1362
1463
  "method": "POST",
@@ -1467,6 +1568,30 @@ export const ROUTE_MANIFEST = [
1467
1568
  "state": "IL"
1468
1569
  }
1469
1570
  },
1571
+ {
1572
+ "name": "physical_mail_cancel",
1573
+ "method": "POST",
1574
+ "path": "/v1/tools/physical-mail/orders/{order_id}/cancel",
1575
+ "paid": false,
1576
+ "readOnly": false,
1577
+ "title": "Cancel a Mail Order",
1578
+ "description": "Ask to cancel a mail order. Accepted (202) as a request: cancellation is not guaranteed once the mail has been submitted for printing; read the order to see whether it was canceled and refunded. Free.",
1579
+ "inputSchema": {
1580
+ "type": "object",
1581
+ "properties": {
1582
+ "order_id": {
1583
+ "type": "string",
1584
+ "description": "Order id from oneshot_physical_mail_send"
1585
+ }
1586
+ },
1587
+ "required": [
1588
+ "order_id"
1589
+ ]
1590
+ },
1591
+ "example": {
1592
+ "order_id": "00000000-0000-4000-8000-000000000000"
1593
+ }
1594
+ },
1470
1595
  {
1471
1596
  "name": "watch_update",
1472
1597
  "method": "PATCH",
@@ -1655,6 +1780,54 @@ export const ROUTE_MANIFEST = [
1655
1780
  "domain": "mail.example.com"
1656
1781
  }
1657
1782
  },
1783
+ {
1784
+ "name": "physical_mail_order",
1785
+ "method": "GET",
1786
+ "path": "/v1/tools/physical-mail/orders/{order_id}",
1787
+ "paid": false,
1788
+ "readOnly": true,
1789
+ "title": "Get a Mail Order",
1790
+ "description": "Read a mail order: order_status, payment_status, fulfillment_status, events, cancel_before and the signed receipt. Free. Delivery events are postal observations, never evidence the mail was read.",
1791
+ "inputSchema": {
1792
+ "type": "object",
1793
+ "properties": {
1794
+ "order_id": {
1795
+ "type": "string",
1796
+ "description": "Order id from oneshot_physical_mail_send or oneshot_physical_mail_recover"
1797
+ }
1798
+ },
1799
+ "required": [
1800
+ "order_id"
1801
+ ]
1802
+ },
1803
+ "example": {
1804
+ "order_id": "00000000-0000-4000-8000-000000000000"
1805
+ }
1806
+ },
1807
+ {
1808
+ "name": "physical_mail_quote",
1809
+ "method": "GET",
1810
+ "path": "/v1/tools/physical-mail/quotes/{quote_id}",
1811
+ "paid": false,
1812
+ "readOnly": true,
1813
+ "title": "Get a Mail Quote",
1814
+ "description": "Read a quote from oneshot_physical_mail_preview: its status (rendering or ready), input_hash, total_usdc, proof url, expires_at and approval_id once approved. Free. Poll it while status is rendering.",
1815
+ "inputSchema": {
1816
+ "type": "object",
1817
+ "properties": {
1818
+ "quote_id": {
1819
+ "type": "string",
1820
+ "description": "Quote id from oneshot_physical_mail_preview"
1821
+ }
1822
+ },
1823
+ "required": [
1824
+ "quote_id"
1825
+ ]
1826
+ },
1827
+ "example": {
1828
+ "quote_id": "00000000-0000-4000-8000-000000000000"
1829
+ }
1830
+ },
1658
1831
  {
1659
1832
  "name": "voice_inbound_get",
1660
1833
  "method": "GET",
@@ -1733,6 +1906,83 @@ export const ROUTE_MANIFEST = [
1733
1906
  },
1734
1907
  "example": {}
1735
1908
  },
1909
+ {
1910
+ "name": "physical_mail_preview",
1911
+ "method": "POST",
1912
+ "path": "/v1/tools/physical-mail/preview",
1913
+ "paid": false,
1914
+ "readOnly": false,
1915
+ "title": "Preview and Quote a Mailpiece",
1916
+ "description": "Quote a letter or postcard and render an uncharged test proof. Nothing is printed or charged. Returns quote_id, input_hash, total_usdc (the price of this piece; see Pricing), the proof url and expires_at. Upload the artwork first with oneshot_physical_mail_upload; its asset ids go in artwork.file (letter) or artwork.front and artwork.back (postcard). Next: oneshot_physical_mail_approve with the returned quote_id, input_hash and total_usdc.",
1917
+ "inputSchema": {
1918
+ "type": "object",
1919
+ "properties": {
1920
+ "to": {
1921
+ "type": "object",
1922
+ "description": "Recipient address: { name, address_line1, address_line2?, address_city, address_state (two letters), address_zip (5 or 9 digits), address_country? (\"US\") }"
1923
+ },
1924
+ "from": {
1925
+ "type": "object",
1926
+ "description": "Return address, same shape as to"
1927
+ },
1928
+ "artwork": {
1929
+ "type": "object",
1930
+ "description": "Letter: { kind: \"letter\", file: <asset id of a PDF>, color?: boolean, double_sided?: boolean }. Postcard: { kind: \"postcard\", front: <asset id>, back: <asset id> }"
1931
+ }
1932
+ },
1933
+ "required": [
1934
+ "to",
1935
+ "from",
1936
+ "artwork"
1937
+ ]
1938
+ },
1939
+ "example": {
1940
+ "to": {
1941
+ "name": "Jane Doe",
1942
+ "address_line1": "185 Berry St",
1943
+ "address_city": "San Francisco",
1944
+ "address_state": "CA",
1945
+ "address_zip": "94107"
1946
+ },
1947
+ "from": {
1948
+ "name": "Acme Inc",
1949
+ "address_line1": "1 Main St",
1950
+ "address_city": "Austin",
1951
+ "address_state": "TX",
1952
+ "address_zip": "78701"
1953
+ },
1954
+ "artwork": {
1955
+ "kind": "letter",
1956
+ "file": "00000000-0000-4000-8000-000000000000",
1957
+ "color": false,
1958
+ "double_sided": false
1959
+ }
1960
+ }
1961
+ },
1962
+ {
1963
+ "name": "physical_mail_recover",
1964
+ "method": "GET",
1965
+ "path": "/v1/tools/physical-mail/orders/recover",
1966
+ "paid": false,
1967
+ "readOnly": true,
1968
+ "title": "Recover a Mail Order",
1969
+ "description": "Find the order a send created, by the idempotency key given to oneshot_physical_mail_send. Use it after a timeout or lost response instead of sending again. Free. Answers 404 when no send with that key reached the API.",
1970
+ "inputSchema": {
1971
+ "type": "object",
1972
+ "properties": {
1973
+ "key": {
1974
+ "type": "string",
1975
+ "description": "The idempotency key of the send (1-255 characters: letters, digits, _ . : -)"
1976
+ }
1977
+ },
1978
+ "required": [
1979
+ "key"
1980
+ ]
1981
+ },
1982
+ "example": {
1983
+ "key": "thank-you-2026-q3-jane-doe"
1984
+ }
1985
+ },
1736
1986
  {
1737
1987
  "name": "company_sanctions",
1738
1988
  "method": "POST",
@@ -1802,62 +2052,55 @@ export const ROUTE_MANIFEST = [
1802
2052
  }
1803
2053
  },
1804
2054
  {
1805
- "name": "solve",
2055
+ "name": "physical_mail_validate_address",
1806
2056
  "method": "POST",
1807
- "path": "/v1/tools/solve",
1808
- "paid": true,
2057
+ "path": "/v1/tools/physical-mail/validate-address",
2058
+ "paid": false,
1809
2059
  "readOnly": true,
1810
- "title": "Solve",
1811
- "description": "Plan vehicle routes within capacity and time windows. Returns the best route found, constraint checks and conflicts.",
2060
+ "title": "Validate a Mailing Address",
2061
+ "description": "Check that a U.S. postal address is deliverable before quoting mail to it. Returns deliverable, a deliverability code and the standardised address. Free; changes nothing.",
1812
2062
  "inputSchema": {
1813
2063
  "type": "object",
1814
2064
  "properties": {
1815
- "problem": {
2065
+ "name": {
1816
2066
  "type": "string",
1817
- "enum": [
1818
- "routing"
1819
- ]
2067
+ "description": "Recipient name (1-100 characters)"
1820
2068
  },
1821
- "time_limit_s": {
1822
- "type": "integer",
1823
- "minimum": 1,
1824
- "maximum": 300,
1825
- "default": 30
2069
+ "address_line1": {
2070
+ "type": "string",
2071
+ "description": "Street address (1-200 characters)"
1826
2072
  },
1827
- "routing": {
1828
- "type": "object",
1829
- "description": "Vehicles, stops, objective and optional travel matrix."
2073
+ "address_line2": {
2074
+ "type": "string",
2075
+ "description": "Apartment, suite or unit (optional)"
2076
+ },
2077
+ "address_city": {
2078
+ "type": "string"
2079
+ },
2080
+ "address_state": {
2081
+ "type": "string",
2082
+ "description": "Two-letter state code, e.g. CA"
2083
+ },
2084
+ "address_zip": {
2085
+ "type": "string",
2086
+ "description": "ZIP code, 5 digits or ZIP+4"
1830
2087
  }
1831
2088
  },
1832
2089
  "required": [
1833
- "problem",
1834
- "routing"
2090
+ "name",
2091
+ "address_line1",
2092
+ "address_city",
2093
+ "address_state",
2094
+ "address_zip"
1835
2095
  ]
1836
2096
  },
1837
2097
  "example": {
1838
- "problem": "routing",
1839
- "routing": {
1840
- "objective": "distance",
1841
- "vehicles": [
1842
- {
1843
- "id": "van",
1844
- "capacity": 10,
1845
- "start": {
1846
- "lat": 48,
1847
- "lng": 16
1848
- }
1849
- }
1850
- ],
1851
- "stops": [
1852
- {
1853
- "id": "stop",
1854
- "location": {
1855
- "lat": 48.01,
1856
- "lng": 16.01
1857
- }
1858
- }
1859
- ]
1860
- }
2098
+ "name": "Jane Doe",
2099
+ "address_line1": "185 Berry St",
2100
+ "address_line2": "Suite 6100",
2101
+ "address_city": "San Francisco",
2102
+ "address_state": "CA",
2103
+ "address_zip": "94107"
1861
2104
  }
1862
2105
  },
1863
2106
  {
@@ -0,0 +1,34 @@
1
+ import type { Tool } from "@modelcontextprotocol/sdk/types.js";
2
+ import type { OneShot } from "@oneshot-agent/sdk";
3
+ /**
4
+ * The two Physical Mail steps that a generated route tool cannot carry (#1883): the send,
5
+ * which needs an idempotency key header beside the quote id and the approval id, and the
6
+ * artwork upload, whose body is a raw PDF, PNG or JPEG. Both go through the SDK's
7
+ * `physicalMail` (libs/agent-sdk/src/physical-mail.ts). The other seven routes (validate,
8
+ * preview, quote, approve, recover, order, cancel) are generated from the declaration.
9
+ */
10
+ /** The route's own limit (`uploadArtwork` in services/physical-mail/service.ts): 20 MB. */
11
+ export declare const PHYSICAL_MAIL_ASSET_LIMIT_BYTES: number;
12
+ export declare const PHYSICAL_MAIL_ASSET_TYPES: readonly ["application/pdf", "image/png", "image/jpeg"];
13
+ type AssetType = (typeof PHYSICAL_MAIL_ASSET_TYPES)[number];
14
+ export declare const physicalMailSendTool: Tool;
15
+ export declare function handlePhysicalMailSend(agent: OneShot, args: Record<string, unknown>): Promise<import("@oneshot-agent/sdk").MailOrder>;
16
+ export declare const physicalMailUploadTool: Tool;
17
+ /** Decodes the base64 `content`, refusing anything that is not base64 or that would exceed the route's limit before any bytes are sent. */
18
+ export declare function decodeArtwork(args: Record<string, unknown>): {
19
+ data: Uint8Array;
20
+ mime: AssetType;
21
+ };
22
+ /**
23
+ * The decoded size of structurally valid base64 (standard or URL-safe alphabet, padding optional), or null.
24
+ * `Buffer.from(text, "base64")` tolerates a length one past a quartet, missing, short and excess padding
25
+ * (`AAAAA`, `AAA==`, `AAAA==`), so counting characters and subtracting the `=`s would undercount such
26
+ * input; the bound has to come from a shape the decoder cannot read differently.
27
+ */
28
+ export declare function base64Size(text: string): number | null;
29
+ export declare function handlePhysicalMailUpload(agent: OneShot, args: Record<string, unknown>): Promise<{
30
+ asset_id: string;
31
+ content_hash: string;
32
+ mime_type: string;
33
+ }>;
34
+ export {};
@@ -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
+ }