@oneshot-agent/mcp-server 0.50.0 → 0.52.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
@@ -84,10 +84,10 @@ Every tool is named `oneshot_<name>`. Below are the names.
84
84
  | [Think](https://docs.oneshotagent.com/api-reference/research) | `research` `predict` `solve` |
85
85
  | [Web and buying](https://docs.oneshotagent.com/api-reference/web-search) | `web_search` `web_read` `browser` `product_find` `commerce_buy` |
86
86
  | [Physical mail](https://docs.oneshotagent.com/api-reference/physical-mail/overview) | `physical_mail_upload` `physical_mail_validate_address` `physical_mail_preview` `physical_mail_quote` `physical_mail_approve` `physical_mail_send` `physical_mail_recover` `physical_mail_order` `physical_mail_cancel` |
87
- | [Connected systems](https://docs.oneshotagent.com/api-reference/connections/overview) | `connections_list` `ledger_invoices` `ledger_invoice_lines` `ledger_bills` `ledger_reports` `ledger_bank_transactions` `crm_accounts` `crm_contacts` `crm_opportunities` `tax_rates` `tax_calculate` `shipping_shipments` `shipping_tracking` `shipping_rates` `procurement_transactions` `procurement_bills` `procurement_suppliers` `meetings_list` `meetings_get` |
87
+ | [Connected systems](https://docs.oneshotagent.com/api-reference/connections/overview) | `connections_list` `ledger_invoices` `ledger_invoice_lines` `ledger_bills` `ledger_reports` `ledger_bank_transactions` `crm_accounts` `crm_contacts` `crm_opportunities` `crm_tickets` `crm_activity` `tax_rates` `tax_calculate` `shipping_shipments` `shipping_tracking` `shipping_rates` `procurement_transactions` `procurement_bills` `procurement_suppliers` `meetings_list` `meetings_get` `field_service_clients` `field_service_jobs` `field_service_quotes` `field_service_invoices` `client_work_clients` `client_work_items` `client_work_timesheets` |
88
88
  | [LinkedIn](https://docs.oneshotagent.com/api-reference/linkedin/overview) | `linkedin_connect` `linkedin_accounts` `linkedin_invite` `linkedin_message` `linkedin_reply` and more |
89
89
  | [Build](https://docs.oneshotagent.com/api-reference/build/create) | `build` `update_build` |
90
- | [Goals](https://docs.oneshotagent.com/api-reference/compute/create) | `compute` `compute_status` `compute_tasks` `compute_respond` `compute_pause` `compute_resume` `compute_fund` `compute_cancel` |
90
+ | [Goals](https://docs.oneshotagent.com/api-reference/compute/create) | `compute` `compute_status` `compute_tasks` `compute_respond` `compute_pause` `compute_resume` `compute_fund` `compute_cancel`. `compute` adds `quote_text` for bound hires (public soul identity and service) and unmatched specialist questions, without prices. Starting proceeds without unmatched specialists. Otherwise, the usual goal result is returned. |
91
91
  | [Money and receipts](https://docs.oneshotagent.com/api-reference/analytics/overview) | `get_balance` `budget_status` `spend_breakdown` `return_on_spend` `receipts_list` `tag_receipt_value` |
92
92
  | Soul Hunt | `hunt_list` `hunt_status` `hunt_predict` `hunt_collection` `hunt_leaderboard` |
93
93
 
@@ -110,9 +110,10 @@ wallet. The server signs it for you. A read with no proof and no valid token is
110
110
  ## Tool notes
111
111
 
112
112
  - **Email.** Omit `from_domain` and `from_mailbox` and the send rotates across
113
- your warmed domains. `from_mailbox` needs `from_domain`. A new `from_domain`
114
- only quotes; nothing is charged until you confirm. `mailbox_mode` applies only
115
- when a domain is first set up. [Details](https://docs.oneshotagent.com/api-reference/email/send).
113
+ your domains, each from its main address. `from_mailbox` needs `from_domain`
114
+ and must be one of the domain's addresses. A new `from_domain` or address only
115
+ quotes its setup; nothing is charged or sent. The owner picks the domain's mode
116
+ and addresses when buying it. [Details](https://docs.oneshotagent.com/api-reference/email/send).
116
117
  - **Phone lookup** is off by default on `person_find`, `person_profile` and
117
118
  `person_dossier`. `include_phone: true` is charged for each person you ask for
118
119
  (the search limit, default 100), or once for a profile or dossier, found or not.
@@ -149,7 +150,7 @@ wallet. The server signs it for you. A read with no proof and no valid token is
149
150
 
150
151
  ## Tool discovery
151
152
 
152
- The MCP catalog contains 233 tools. `oneshot_discover_tools` is a free, read-only
153
+ The MCP catalog contains 263 tools. `oneshot_discover_tools` is a free, read-only
153
154
  search over the tools your token can call. Call it with no arguments for category
154
155
  counts, `{ "query": "send a printed letter" }` to search, or `{ "category": "email" }`
155
156
  to browse. Results contain public tool names, purposes and required inputs.
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.50.0", // keep in sync with package.json; guarded by version.test.ts
53
+ version: "0.52.0", // keep in sync with package.json; guarded by version.test.ts
54
54
  }, {
55
55
  capabilities: {
56
56
  tools: {},
@@ -3,14 +3,16 @@ import { LOOKUP_MANIFEST, ROUTE_MANIFEST } from "./lookup-manifest.generated.js"
3
3
  const read = (title, openWorld) => ({ title, readOnlyHint: true, destructiveHint: false, openWorldHint: openWorld });
4
4
  /** Changes something. `destructive` for delete / overwrite / revoke and for irreversible sends and transactions. */
5
5
  const write = (title, openWorld, destructive = false) => ({ title, readOnlyHint: false, destructiveHint: destructive, openWorldHint: openWorld });
6
+ /** Declared routes that cannot be taken back or overwrite settings (a watch's cap and webhook), so destructive. */
7
+ const IRREVERSIBLE_ROUTES = new Set(["physical_mail_approve", "physical_mail_cancel", "identity_start", "watch_update"]);
6
8
  /**
7
- * Declared routes that cannot be taken back, so destructive. Approving a mail quote posts a letter and
8
- * starting an identity check sends a person a link: both reach outside the account, so open-world too.
9
+ * Free declared routes that still reach outside the account, so open-world: approving a mail quote posts a
10
+ * letter, an identity check sends a person a link, a watch follows third-party data and may call a webhook,
11
+ * and a browser account connection signs in to another site.
9
12
  */
10
- const IRREVERSIBLE_ROUTES = new Set(["physical_mail_approve", "physical_mail_cancel", "identity_start"]);
11
- const OUTBOUND_ROUTES = new Set(["physical_mail_approve", "identity_start"]);
13
+ const OUTBOUND_ROUTES = new Set(["physical_mail_approve", "identity_start", "watch", "watch_update", "browser_connect_account"]);
12
14
  /** The connected-system reads (#2403): paid, but each reads the caller's own connected business system, so closed-world. */
13
- const CONNECTED_SYSTEM_READ = /^\/v1\/tools\/(ledger|crm|shipping|tax|procurement|meetings|mailbox|files)\//;
15
+ const CONNECTED_SYSTEM_READ = /^\/v1\/tools\/(ledger|crm|shipping|tax|procurement|matters|meetings|mailbox|files|field_service|client_work|portfolio|practice)\//;
14
16
  const OPEN = true;
15
17
  const OWN = false;
16
18
  const DESTRUCTIVE = true;
@@ -21,7 +23,7 @@ export const TOOL_ANNOTATIONS = {
21
23
  oneshot_predict: read("Predict", OWN),
22
24
  oneshot_hunt_list: read("List Soul Hunts", OPEN),
23
25
  oneshot_hunt_status: read("Get Soul Hunt Status", OPEN),
24
- oneshot_hunt_predict: write("Predict Soul Hunt", OPEN),
26
+ oneshot_hunt_predict: write("Predict Soul Hunt", OPEN, DESTRUCTIVE), // a submitted prediction cannot be withdrawn
25
27
  oneshot_hunt_collection: read("List Collected Souls", OWN),
26
28
  oneshot_hunt_leaderboard: read("Soul Hunt Leaderboard", OPEN),
27
29
  oneshot_email: write("Send email", OPEN, DESTRUCTIVE),
@@ -67,7 +69,7 @@ export const TOOL_ANNOTATIONS = {
67
69
  oneshot_compute_pause: write("Pause a goal", OWN),
68
70
  oneshot_compute_resume: write("Resume a goal", OWN),
69
71
  oneshot_compute_fund: write("Add budget to a goal", OWN, DESTRUCTIVE),
70
- oneshot_build: write("Build a website", OPEN),
72
+ oneshot_build: write("Build a website", OPEN, DESTRUCTIVE), // publishes a live site and is paid
71
73
  oneshot_update_build: write("Update a website", OPEN, DESTRUCTIVE), // overwrites the live site
72
74
  oneshot_browser: write("Browse and act on a website", OPEN),
73
75
  oneshot_browser_create_profile: write("Create a browser profile", OWN),
@@ -1,5 +1,8 @@
1
1
  import type { Tool } from "@modelcontextprotocol/sdk/types.js";
2
- import type { OneShot } from "@oneshot-agent/sdk";
2
+ import type { OneShot, ComputeQuote } from "@oneshot-agent/sdk";
3
+ export type { ComputeQuote } from "@oneshot-agent/sdk";
4
+ /** Select public quote details explicitly so prices and private identity never enter tool text. */
5
+ export declare function formatComputeQuote(quote: ComputeQuote): string;
3
6
  export declare const computeTool: Tool;
4
7
  export declare const computeStatusTool: Tool;
5
8
  export declare const computeTasksTool: Tool;
@@ -8,7 +11,26 @@ export declare const computeRespondTool: Tool;
8
11
  export declare const computePauseTool: Tool;
9
12
  export declare const computeResumeTool: Tool;
10
13
  export declare const computeFundTool: Tool;
11
- export declare function handleCompute(agent: OneShot, args: Record<string, unknown>): Promise<import("@oneshot-agent/sdk").ComputeGoalResult>;
14
+ export declare function handleCompute(agent: OneShot, args: Record<string, unknown>): Promise<import("@oneshot-agent/sdk").ComputeGoalResult | {
15
+ quote_text: string;
16
+ requires?: Array<import("@oneshot-agent/sdk").ComputeRequirement | import("@oneshot-agent/sdk").ComputeToolRequirement>;
17
+ goal_id: string;
18
+ request_id: string;
19
+ receipt_id?: string;
20
+ status: string;
21
+ message: string;
22
+ memo?: string;
23
+ cost?: number;
24
+ goal: {
25
+ objective: string;
26
+ budget_usdc: string;
27
+ deadline?: string;
28
+ schedule_cron?: string;
29
+ budget_per_run?: number;
30
+ max_runs?: number;
31
+ next_run_at?: string;
32
+ };
33
+ }>;
12
34
  export declare function handleComputeStatus(agent: OneShot, args: Record<string, unknown>): Promise<import("@oneshot-agent/sdk").ComputeGoalStatus>;
13
35
  export declare function handleComputeTasks(agent: OneShot, args: Record<string, unknown>): Promise<import("@oneshot-agent/sdk").ComputeOutputContent | import("@oneshot-agent/sdk").ComputeTask[]>;
14
36
  export declare function handleComputeCancel(agent: OneShot, args: Record<string, unknown>): Promise<{
@@ -1,10 +1,18 @@
1
+ /** Select public quote details explicitly so prices and private identity never enter tool text. */
2
+ export function formatComputeQuote(quote) {
3
+ const hires = (quote.price?.hires ?? []).map(hire => `Hires ${hire.display_name} (${hire.soul_slug}) for ${hire.service_name} (${hire.service_slug})${hire.need ? `: ${hire.need}` : ''}.`);
4
+ const specialists = (quote.requirements ?? []).filter(req => req.kind === 'specialist').map(req => req.question?.trim() ? req.question : `No specialist matched${req.need ? ` for ${req.need}` : ''}. Start the goal to go ahead without one, using the platform's tools.`);
5
+ return [...hires, ...specialists].join('\n');
6
+ }
1
7
  const KNOWN_GOAL_TOOL = { type: "string", description: 'A OneShot tool name, e.g. "email", "voice", "commerce_buy"' };
8
+ const SCOPE_DRIVERS = { type: "object", additionalProperties: { anyOf: [{ type: "number" }, { type: "boolean" }, { type: "string" }] }, description: 'Request sizes, never prices: recipients, segments, max_duration_minutes, max_steps (e.g. {"max_steps":25}), include_phone and limit, or letter kind/pages/color/double_sided; Predict/Solve size and time_limit_s. Omitted fields use tool defaults.' };
2
9
  const SCOPE_USE = {
3
10
  type: "object",
4
11
  properties: {
5
12
  tool: KNOWN_GOAL_TOOL,
6
13
  qty: { type: "integer", minimum: 1, description: "How many times inside one unit" },
7
14
  max_attempts: { type: "integer", minimum: 1, description: "Attempts each use may take" },
15
+ drivers: SCOPE_DRIVERS,
8
16
  target: { type: "string", description: 'Whom or what the use is pointed at, e.g. "the carrier\'s billing desk"' },
9
17
  persistence: {
10
18
  type: "object",
@@ -34,6 +42,11 @@ export const computeTool = {
34
42
  type: "number",
35
43
  description: "Spend ceiling in USDC. Server estimates if omitted.",
36
44
  },
45
+ purchase_limit_usdc: {
46
+ type: "number",
47
+ exclusiveMinimum: 0,
48
+ description: "The most the owner would pay for one purchase, all in. Only set a figure the owner gave you, never your own. It is added to the total and unused money comes back as credit; a live total above it waits for the owner's approval. Without it, the goal pauses at a purchase it can't afford.",
49
+ },
37
50
  params: {
38
51
  type: "object",
39
52
  description: "Additional parameters / constraints for the objective",
@@ -116,6 +129,45 @@ export const computeTool = {
116
129
  required: ["kind", "finished_states"],
117
130
  additionalProperties: false,
118
131
  },
132
+ {
133
+ type: "object",
134
+ description: "A named verifier accepts proof the agent submits (a document, photo, link or fields read from a document). The verifier needs an email or a phone number. With fields, a field the proof does not show, or one outside its bounds, is reported back without asking the verifier. One-off goals only.",
135
+ properties: {
136
+ kind: { const: "evidence" },
137
+ verifier: {
138
+ type: "object",
139
+ properties: {
140
+ name: { type: "string" },
141
+ email: { type: "string" },
142
+ phone: { type: "string", description: "E.164, such as +14155550100" },
143
+ timezone: { type: "string", description: 'IANA zone, e.g. "America/Chicago", for texts' },
144
+ },
145
+ required: ["name"],
146
+ additionalProperties: false,
147
+ },
148
+ accepts: { type: "string", description: "What proof is acceptable, in plain words; the verifier is shown it" },
149
+ fields: {
150
+ type: "array",
151
+ maxItems: 20,
152
+ items: {
153
+ type: "object",
154
+ properties: {
155
+ name: { type: "string" },
156
+ description: { type: "string" },
157
+ type: { type: "string", enum: ["string", "number", "boolean"] },
158
+ equals: { type: ["string", "number", "boolean"] },
159
+ one_of: { type: "array", items: { type: "string" } },
160
+ at_least: { type: "number" },
161
+ at_most: { type: "number" },
162
+ },
163
+ required: ["name", "description", "type"],
164
+ additionalProperties: false,
165
+ },
166
+ },
167
+ },
168
+ required: ["kind", "verifier", "accepts"],
169
+ additionalProperties: false,
170
+ },
119
171
  ],
120
172
  },
121
173
  egress_allowlist: {
@@ -151,7 +203,7 @@ export const computeTool = {
151
203
  type: "object",
152
204
  description: "Tool uses outside any unit: setting up, reporting",
153
205
  properties: {
154
- uses: { type: "array", maxItems: 50, items: { type: "object", properties: { tool: KNOWN_GOAL_TOOL, qty: { type: "integer", minimum: 1 }, max_attempts: { type: "integer", minimum: 1 } }, required: ["tool", "qty", "max_attempts"] } },
206
+ uses: { type: "array", maxItems: 50, items: { type: "object", properties: { tool: KNOWN_GOAL_TOOL, qty: { type: "integer", minimum: 1 }, max_attempts: { type: "integer", minimum: 1 }, drivers: SCOPE_DRIVERS }, required: ["tool", "qty", "max_attempts"] } },
155
207
  step_allowance: { type: "integer", minimum: 0 },
156
208
  },
157
209
  required: ["uses", "step_allowance"],
@@ -171,11 +223,12 @@ export const computeTool = {
171
223
  properties: {
172
224
  name: { type: "string" },
173
225
  reason: { type: "string", enum: ["connection", "quoted_tool"] },
174
- assumption: { type: "string", description: 'What the price assumes, e.g. "5 minutes" for a call or "20 steps" for a browser run' },
226
+ assumption: { type: "string", description: "What remains unknown until a live quote, capped by the remaining goal budget" },
175
227
  },
176
228
  required: ["name", "reason"],
177
229
  },
178
230
  },
231
+ purchase_limit_usdc: { type: "number", exclusiveMinimum: 0, description: "The most per purchase, all in, as the owner gave it. The top-level field wins over this." },
179
232
  },
180
233
  required: ["unit", "per_unit", "overhead", "volume", "unpriced"],
181
234
  },
@@ -236,7 +289,7 @@ export const computeRespondTool = {
236
289
  };
237
290
  export const computePauseTool = {
238
291
  name: "oneshot_compute_pause",
239
- description: "Pause a recurring compute goal. Its plan and progress are kept — the goal stops taking scheduled runs until resumed. Only recurring (scheduled) goals can be paused. Free.",
292
+ description: "Pause a compute goal. A recurring goal stops taking scheduled runs until resumed; any other goal keeps its sessions, cases and reserved budget and continues where it was when resumed. Free.",
240
293
  inputSchema: {
241
294
  type: "object",
242
295
  properties: {
@@ -248,7 +301,7 @@ export const computePauseTool = {
248
301
  };
249
302
  export const computeResumeTool = {
250
303
  name: "oneshot_compute_resume",
251
- description: "Resume a paused recurring compute goal. Picks up on the existing plan at the next scheduled run — nothing is lost. Free.",
304
+ description: "Resume a paused compute goal. A recurring goal picks up at its next scheduled run; any other goal continues now where it was. Nothing is lost. Free.",
252
305
  inputSchema: {
253
306
  type: "object",
254
307
  properties: {
@@ -270,6 +323,7 @@ export const computeFundTool = {
270
323
  },
271
324
  };
272
325
  export async function handleCompute(agent, args) {
326
+ let quoteText = '';
273
327
  const options = {
274
328
  objective: args.objective,
275
329
  budget_usdc: args.budget_usdc,
@@ -281,10 +335,14 @@ export async function handleCompute(agent, args) {
281
335
  approvals: args.approvals,
282
336
  done_check: args.done_check,
283
337
  egress_allowlist: args.egress_allowlist,
338
+ onQuote: quote => { quoteText = formatComputeQuote(quote); },
284
339
  };
285
340
  if (args.scope !== undefined)
286
341
  options.scope = args.scope;
287
- return agent.compute(options);
342
+ if (args.purchase_limit_usdc !== undefined)
343
+ options.purchase_limit_usdc = args.purchase_limit_usdc;
344
+ const result = await agent.compute(options);
345
+ return quoteText ? { ...result, quote_text: quoteText } : result;
288
346
  }
289
347
  export async function handleComputeStatus(agent, args) {
290
348
  return agent.getComputeGoal(args.goal_id);
@@ -3,7 +3,7 @@ import type { OneShot } from "@oneshot-agent/sdk";
3
3
  /**
4
4
  * The agent's connections (#2403). Hand-written because `GET /v1/connections` is not a declared route: the
5
5
  * connected-system read tools (`oneshot_ledger_*`, `oneshot_crm_*`, `oneshot_shipping_*`, `oneshot_tax_*`,
6
- * `oneshot_procurement_*`, `oneshot_meetings_*`) are generated from their routes and each tells the model to call this one first.
6
+ * `oneshot_procurement_*`, `oneshot_meetings_*`, `oneshot_matters_*`, `oneshot_field_service_*`, `oneshot_client_work_*`, `oneshot_portfolio_*`, `oneshot_practice_*`) are generated from their routes and each tells the model to call this one first.
7
7
  * Connecting and revoking stay in the console.
8
8
  */
9
9
  export declare const connectionsListTool: Tool;
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * The agent's connections (#2403). Hand-written because `GET /v1/connections` is not a declared route: the
3
3
  * connected-system read tools (`oneshot_ledger_*`, `oneshot_crm_*`, `oneshot_shipping_*`, `oneshot_tax_*`,
4
- * `oneshot_procurement_*`, `oneshot_meetings_*`) are generated from their routes and each tells the model to call this one first.
4
+ * `oneshot_procurement_*`, `oneshot_meetings_*`, `oneshot_matters_*`, `oneshot_field_service_*`, `oneshot_client_work_*`, `oneshot_portfolio_*`, `oneshot_practice_*`) are generated from their routes and each tells the model to call this one first.
5
5
  * Connecting and revoking stay in the console.
6
6
  */
7
7
  export const connectionsListTool = {
8
8
  name: "oneshot_connections_list",
9
- description: "List the agent's connections to business systems (accounting, CRM, shipping, tax, spend) and GitHub. " +
9
+ description: "List the agent's connections to business systems (accounting, CRM, shipping, tax, spend, practice management, wealth) and GitHub. " +
10
10
  "Returns each connection's id, name, kind, system, account_label and status (only an active one can be read). " +
11
11
  "Call it before any connected-system read: pick the connection by system and account_label and pass its id as connection_id. " +
12
12
  "Connections are added and revoked in the console. Free.",
@@ -1,6 +1,6 @@
1
1
  export const listDomainsTool = {
2
2
  name: "oneshot_list_domains",
3
- description: "List the agent's sending domains. Each has default_from (canonical default sender, e.g. agent@domain — billed like any address on its first send), mailbox_mode (relay|mailbox), addresses[] (mailboxes already provisioned — {address, status, warmup_state}; empty means none yet), pool_status (rotation eligibility: active|paused|removed), warmup_state (reputation health: warming|warmed|degraded), pause_reason (warming|low_reputation|manual|null), warmup_score, and daily send limits. mailbox_mode is a property of the domain, fixed when it was set up: on a 'mailbox' domain EVERY address not already active in addresses[] bills the one-time mailbox_provisioning_fee on its first send, the canonical default_from included. 'relay' domains never charge it. A domain can be paused AND warming — reputation pauses auto-recover once warmed; manual pauses need a resume. Free.",
3
+ description: "List the agent's sending domains. Each has default_from (the main address, e.g. agent@domain; rotation and goals send from it), mailbox_mode (relay|mailbox), addresses[] (the addresses the domain sends and receives at: {address, status, warmup_state}), pool_status (rotation eligibility: active|paused|removed), warmup_state (reputation health: warming|warmed|degraded|not_warmed), pause_reason (warming|low_reputation|manual|null), warmup_score, and daily send limits. mailbox_mode is fixed when the domain is bought. relay: addresses send and receive with no hosted inbox; never warmed (not_warmed), they rotate at once on a daily limit that rises with the domain's age. mailbox: each address is a hosted inbox, warmed before it rotates. The owner adds addresses with the domain purchase; a send never creates one. A domain can be paused AND warming: reputation pauses auto-recover once warmed; manual pauses need a resume. Free.",
4
4
  inputSchema: { type: "object", properties: {} },
5
5
  };
6
6
  export const pauseDomainTool = {
@@ -1,7 +1,7 @@
1
1
  export const emailTool = {
2
2
  name: "oneshot_email",
3
3
  description: "Send an email. Supports HTML body, attachments, and threaded replies (set reply_to_email_id to an inbound email id from oneshot_inbox_list/oneshot_inbox_get, or one this agent sent). Paid tool — quote provided before payment.\n\n" +
4
- "Sending requires a domain the agent owns. A fresh account has none — call oneshot_list_domains first to see what's already owned. If there is no usable domain, pass from_domain with a new domain to register it. A call with a new from_domain only quotes — nothing is charged — and returns status 'confirmation_required' with the exact cost (domain registration plus fees) and a quote_id. Tell the user that exact computed number, never a guessed or approximate one, and only once they say to proceed, call this again with the same arguments and confirm_new_domain set to that quote_id: that registers the domain, charges exactly the quoted total, and sends the email in one call. The workspace balance must cover it, or the call fails with insufficient_credits.",
4
+ "Sending requires an owned, verified domain, and a domain sends only from its own addresses. Call oneshot_list_domains first. A send never sets up a domain or an address: a new from_domain, or a from_mailbox the domain does not have, returns status confirmation_required with a separate setup quote, and no agent domain at all returns setup_required with requirements. Nothing is charged or sent. Show setup.total_cost and setup.billing to the owner, who buys through POST /v1/domains/buy or the account console. Models and goals cannot buy setup. Send again once setup is done.",
5
5
  inputSchema: {
6
6
  type: "object",
7
7
  properties: {
@@ -23,21 +23,16 @@ export const emailTool = {
23
23
  },
24
24
  from_domain: {
25
25
  type: "string",
26
- description: "Domain to send from. If you already own it, pass it to pin that domain. If you don't, pass a new domain here to register it — a call with a new from_domain only quotes (status 'confirmation_required' with a quote_id, nothing charged); repeat the call with confirm_new_domain set to that quote_id to register it, charge exactly the quoted registration + service fee (and a mailbox fee if mailbox_mode is 'mailbox'), and send. Only confirm once the user has seen that amount and asked to proceed. Omit both from_domain and from_mailbox to rotate across your warmed domains — the chosen address comes back on the result. Call oneshot_list_domains first to see what you already own.",
26
+ description: "Pin an owned sending domain, or choose a new name to quote separate account setup. The owner must buy setup and wait for verification before sending. Omit both from_domain and from_mailbox to rotate across eligible owned domains, each sending from its main address.",
27
27
  },
28
28
  from_mailbox: {
29
29
  type: "string",
30
- description: "Sender mailbox / local-part (optional, defaults to 'agent', i.e. agent@from_domain). Requires from_domain to be set — pinning from_mailbox alone is rejected, since there is no domain to attach it to. You can use any local-part — a new address is auto-provisioned on first use (starts unwarmed, warms over time). The FIRST send from any address — including the default 'agent@' — adds a one-time mailbox_provisioning_fee to the quote; it's free only once that address is already provisioned (active). See the domains list for each domain's default_from and existing addresses.",
30
+ description: "Sender local-part on from_domain (optional, defaults to the domain's main address, its default_from). Requires from_domain. Must be one of the domain's addresses (see oneshot_list_domains); another returns the setup quote that adds it, and no email is sent.",
31
31
  },
32
32
  from_name: {
33
33
  type: "string",
34
34
  description: "Display name shown to the recipient (optional), e.g. 'Jane Doe' renders as 'Jane Doe <jane@domain>'",
35
35
  },
36
- mailbox_mode: {
37
- type: "string",
38
- enum: ["relay", "mailbox"],
39
- description: "How this domain sends (optional). 'relay' = no dedicated mailbox, no mailbox fee. 'mailbox' = a real dedicated mailbox per address (better deliverability + per-address warmup); each new address adds a one-time mailbox_provisioning_fee to the quote. Only honored while a domain is unprovisioned (brand new, or owned with provisioning_status 'unprovisioned') — an already-provisioned domain keeps the mode it was created with, which means OMITTING this does not make a send relay and does not avoid the fee. On a domain already in mailbox mode every address that isn't active yet bills the fee on its first send, the default agent@ included; from_mailbox does not change that. Use oneshot_list_domains to see each domain's mailbox_mode before quoting.",
40
- },
41
36
  attachments: {
42
37
  type: "array",
43
38
  items: {
@@ -55,10 +50,6 @@ export const emailTool = {
55
50
  type: "string",
56
51
  description: "Only when retrying a call the action policy held: the approval_id from the approval_required error, once a human approved it. The call then runs once.",
57
52
  },
58
- confirm_new_domain: {
59
- type: "string",
60
- description: "The quote_id from a 'confirmation_required' result, and nothing else. A call with a from_domain you don't own yet always stops before charging and returns that result with the exact total. Only after showing that total to the user and getting their go-ahead, call this again with the same arguments and confirm_new_domain set to the quote_id: the domain is registered, exactly that quote's total is charged, and the email is sent in that one call. Passing true never charges; it only quotes again. A wrong, expired or already-used quote id is rejected by the server without charge: drop it and call again without confirm_new_domain for a fresh quote.",
61
- },
62
53
  },
63
54
  required: ["body"],
64
55
  },
@@ -72,10 +63,7 @@ export async function handleEmail(agent, args) {
72
63
  from_domain: args.from_domain,
73
64
  from_mailbox: args.from_mailbox,
74
65
  from_name: args.from_name,
75
- mailbox_mode: args.mailbox_mode,
76
66
  attachments: args.attachments,
77
67
  approvalId: args.approval_id,
78
- // Only the quote id confirms; `true` or anything else is not a confirmation.
79
- confirmNewDomain: typeof args.confirm_new_domain === "string" ? args.confirm_new_domain : undefined,
80
68
  });
81
69
  }
@@ -287,6 +287,12 @@ export function structuredToolError(error) {
287
287
  };
288
288
  case "PaymentError":
289
289
  return { error: "payment_failed", message: e.message, reason: e.reason };
290
+ case "SettlementAmbiguousError":
291
+ return {
292
+ error: "settlement_ambiguous",
293
+ message: e.message,
294
+ ...(typeof e.settlementId === "string" ? { settlement_id: e.settlementId } : {}),
295
+ };
290
296
  case "ToolError": {
291
297
  // Every 4xx the SDK doesn't recognize as one of the typed errors above
292
298
  // (budget/credits/payment) lands here as a plain ToolError, and its
@@ -219,7 +219,7 @@ export async function handleLinkedinWithdrawInvitation(agent, args) {
219
219
  }
220
220
  export const linkedinMessageTool = {
221
221
  name: "oneshot_linkedin_message",
222
- description: "Message a first-degree LinkedIn connection through a connected account that granted 'reply': into the 1:1 conversation the account already has with them, or a new one. Use it once an invitation is accepted, or for a connection the account has never messaged. Returns conversation_id. The human's account sends it; a message LinkedIn accepted cannot be recalled. Shares the reply daily cap (default 50); sends are paced. A failed send with not_connected (invite them first) or rate_limited means defer, not give up. An Idempotency-Key is generated automatically so a retry never double-sends. Paid tool.",
222
+ description: "Message a first-degree LinkedIn connection through a connected account that granted 'reply': into the 1:1 conversation the account already has with them, or a new one. Use it once an invitation is accepted, or for a connection the account has never messaged. Returns conversation_id. The human's account sends it; a message LinkedIn accepted cannot be recalled. Shares the reply daily cap (default 50); sends are paced. A failed send with recipient_not_connected means invite them first, not give up. An Idempotency-Key is generated automatically so a retry never double-sends. Paid tool.",
223
223
  inputSchema: {
224
224
  type: "object",
225
225
  properties: {