@floatonnear/mcp 0.2.0 → 0.3.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.
@@ -109,8 +109,33 @@ export declare function toCliCommand(p: OwnerPayload, network?: string): string;
109
109
  * A threshold is a gate INSIDE the caps: it can only make the vault more
110
110
  * conservative, never less, so there is no value of it that moves more money
111
111
  * than the budget tree already allowed. `null` removes it.
112
+ *
113
+ * ---- `amount` IS HUMAN UNITS, and it used to be base units.
114
+ *
115
+ * Breaking change, 2026-10-31, made the same day the tool was published and
116
+ * before anyone used it. The parameter was `amountBase` and took raw base
117
+ * units, which made it the ONLY tool in this module on that convention:
118
+ *
119
+ * vault_pay.amount human "12.50"
120
+ * planSetNodeCaps dailyCap/perTxCap human (the zod schema says so)
121
+ * planSetApprovalThreshold BASE <- the odd one out
122
+ *
123
+ * And it is the one that configures a safety gate, so it FAILED OPEN. An owner
124
+ * thinking in human units who passed a figure at or above the per-tx cap in
125
+ * base units got a threshold ABOVE every cap in the tree: the gate never fires,
126
+ * nothing is ever held, and the transaction succeeds — so the owner believes the
127
+ * control is on. Measured against the live mainnet vault (per-tx cap 510000
128
+ * base = 0.51 USDC): "1000000" meaning "a million" yields a 1.0 USDC threshold,
129
+ * above every cap.
130
+ *
131
+ * The other direction fails closed and is merely annoying ("1" meaning 1 USDC
132
+ * became 0.000001, so everything got held), which is why only one direction is
133
+ * dangerous and why this is worth a breaking change rather than a doc fix.
134
+ *
135
+ * `toBaseUnits` throws rather than truncating excess precision — correct here:
136
+ * silently rounding 0.1234567 would set a threshold the owner did not choose.
112
137
  */
113
- export declare function planSetApprovalThreshold(vault: string, nodeId: number, amountBase: string | null): OwnerPayload;
138
+ export declare function planSetApprovalThreshold(vault: string, nodeId: number, amount: string | null, decimals: number): OwnerPayload;
114
139
  /**
115
140
  * Execute a held payment.
116
141
  *
package/dist/payloads.js CHANGED
@@ -296,18 +296,47 @@ export function toCliCommand(p, network = "testnet") {
296
296
  * A threshold is a gate INSIDE the caps: it can only make the vault more
297
297
  * conservative, never less, so there is no value of it that moves more money
298
298
  * than the budget tree already allowed. `null` removes it.
299
+ *
300
+ * ---- `amount` IS HUMAN UNITS, and it used to be base units.
301
+ *
302
+ * Breaking change, 2026-10-31, made the same day the tool was published and
303
+ * before anyone used it. The parameter was `amountBase` and took raw base
304
+ * units, which made it the ONLY tool in this module on that convention:
305
+ *
306
+ * vault_pay.amount human "12.50"
307
+ * planSetNodeCaps dailyCap/perTxCap human (the zod schema says so)
308
+ * planSetApprovalThreshold BASE <- the odd one out
309
+ *
310
+ * And it is the one that configures a safety gate, so it FAILED OPEN. An owner
311
+ * thinking in human units who passed a figure at or above the per-tx cap in
312
+ * base units got a threshold ABOVE every cap in the tree: the gate never fires,
313
+ * nothing is ever held, and the transaction succeeds — so the owner believes the
314
+ * control is on. Measured against the live mainnet vault (per-tx cap 510000
315
+ * base = 0.51 USDC): "1000000" meaning "a million" yields a 1.0 USDC threshold,
316
+ * above every cap.
317
+ *
318
+ * The other direction fails closed and is merely annoying ("1" meaning 1 USDC
319
+ * became 0.000001, so everything got held), which is why only one direction is
320
+ * dangerous and why this is worth a breaking change rather than a doc fix.
321
+ *
322
+ * `toBaseUnits` throws rather than truncating excess precision — correct here:
323
+ * silently rounding 0.1234567 would set a threshold the owner did not choose.
299
324
  */
300
- export function planSetApprovalThreshold(vault, nodeId, amountBase) {
325
+ export function planSetApprovalThreshold(vault, nodeId, amount, decimals) {
326
+ const base = amount === null ? null : toBaseUnits(amount, decimals).toString();
301
327
  return {
302
328
  receiverId: vault,
303
329
  methodName: "set_approval_threshold",
304
- args: { node_id: nodeId, amount: amountBase },
330
+ args: { node_id: nodeId, amount: base },
305
331
  gasTgas: DEFAULT_GAS_TGAS,
306
332
  depositYocto: ONE_YOCTO,
307
- summary: amountBase
308
- ? `Payments above ${amountBase} (base units) from node ${nodeId} will need your approval`
333
+ // Human units in the summary too. The old line read "Payments above
334
+ // 1000000 (base units)", which is unreadable in precisely the moment the
335
+ // owner is checking the plan before signing it.
336
+ summary: amount
337
+ ? `Payments above ${amount} from node ${nodeId} will need your approval`
309
338
  : `Remove the approval threshold on node ${nodeId} — payments up to the caps run unattended`,
310
- warnings: amountBase
339
+ warnings: amount
311
340
  ? []
312
341
  : ["After this, nothing at this node waits for a human: only the caps apply."],
313
342
  };
package/dist/server.js CHANGED
@@ -87,7 +87,7 @@ const limitShape = z
87
87
  // startup: a wrong relative path would break the server outright instead of
88
88
  // merely reporting a stale version. server.smoke.test.ts asserts the two agree,
89
89
  // so a bump cannot land with only one of them moved.
90
- const server = new McpServer({ name: "float", version: "0.2.0" });
90
+ const server = new McpServer({ name: "float", version: "0.3.0" });
91
91
  // ----------------------------------------------------------------- reads
92
92
  server.tool("vault_tree", "Read the whole budget tree: every node, its caps, parent, bound agent account, cooldown and freeze state. No key required.", {}, async () => json(await viewFunction(cfg.rpcUrl, requireVault(), "get_tree", {})));
93
93
  server.tool("vault_budget", "Read one node's budget for a UTC day: cap, executed, in-flight (pending) and available, in human units. Omit `day` for today. No key required.", {
@@ -211,7 +211,15 @@ server.tool("vault_plan_set_cooldown", "Build an UNSIGNED payload to set the min
211
211
  server.tool("vault_pending_approval", "Look up a payment that is HELD awaiting the owner's approval. Returns null if there is no ticket for that id — already approved, already rejected, or never held.", { requestId: z.string().min(1) }, async ({ requestId }) => json((await viewFunction(cfg.rpcUrl, requireVault(), "get_pending_approval", {
212
212
  request_id: requestId,
213
213
  })) ?? null));
214
- server.tool("vault_plan_set_approval_threshold", "Build an UNSIGNED payload requiring the owner's approval for payments above an amount at this node. A threshold sits INSIDE the caps and can only make the vault more conservative. Pass amountBase null to remove it. Not signed by this server.", { nodeId: z.number().int().min(0), amountBase: z.string().min(1).nullable() }, async ({ nodeId, amountBase }) => renderPlan(planSetApprovalThreshold(requireVault(), nodeId, amountBase)));
214
+ server.tool("vault_plan_set_approval_threshold", 'Build an UNSIGNED payload requiring the owner\'s approval for payments above an amount at this node. A threshold sits INSIDE the caps and can only make the vault more conservative. `amount` is HUMAN units, the same convention as vault_pay — "0.25" is a quarter of a USDC. Pass amount null to remove the threshold. Not signed by this server.', {
215
+ nodeId: z.number().int().min(0),
216
+ // HUMAN units, matching vault_pay and vault_plan_set_caps. This took BASE
217
+ // units until 2026-10-31 and was the only tool here that did — on the one
218
+ // control that configures a safety gate, where the mistake fails OPEN. See
219
+ // planSetApprovalThreshold in payloads.ts for the measurement.
220
+ amount: z.string().min(1).nullable().describe('human units, e.g. "0.25" — NOT base units'),
221
+ decimals: z.number().int().min(0).max(38).default(6),
222
+ }, async ({ nodeId, amount, decimals }) => renderPlan(planSetApprovalThreshold(requireVault(), nodeId, amount, decimals)));
215
223
  server.tool("vault_plan_approve_payment", "Build an UNSIGNED payload to APPROVE a held payment, which sends it. Every limit is re-checked at approval, so it can still be refused. Not signed by this server.", { requestId: z.string().min(1) }, async ({ requestId }) => renderPlan(planApprovePayment(requireVault(), requestId)));
216
224
  server.tool("vault_plan_reject_payment", "Build an UNSIGNED payload to REJECT a held payment. Nothing moves and the ticket is discarded. Not signed by this server.", { requestId: z.string().min(1) }, async ({ requestId }) => renderPlan(planRejectPayment(requireVault(), requestId)));
217
225
  server.tool("vault_plan_pause", "Build an UNSIGNED payload to pause or resume the whole vault. Pausing halts every agent payment. Not signed by this server.", { paused: z.boolean() }, async ({ paused }) => renderPlan(planSetPaused(requireVault(), paused)));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floatonnear/mcp",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "description": "MCP server for the hierarchical budget vault. The agent key signs request_payment only; owner methods emit unsigned payloads for wallet approval.",
6
6
  "license": "MIT",