@floatonnear/mcp 0.6.4 → 0.6.5

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/dist/amounts.d.ts CHANGED
@@ -18,3 +18,38 @@
18
18
  export declare function toBaseUnits(amount: string, decimals: number): bigint;
19
19
  /** Render base units as a canonical human string: no trailing point, no trailing zeroes. */
20
20
  export declare function fromBaseUnits(units: bigint, decimals: number): string;
21
+ /**
22
+ * The contract speaks BASE UNITS; a person reading a budget does not.
23
+ *
24
+ * `get_tree` returns `daily_cap: "25000000"` for a 25 USDC cap, while
25
+ * `vault_budget` — reading the same number through `get_node_budget` — returns
26
+ * `"25"`. Two tools, one vault, one figure, two formats, and nothing said
27
+ * which was which. In a demo that reads as a bug even when both are correct,
28
+ * and `"25000000"` next to a `per_tx_cap` of `"10000000"` invites someone to
29
+ * conclude the caps are enormous.
30
+ *
31
+ * CONVERTED ONLY WHERE THE DECIMALS ARE KNOWN. Caps are stored PER TOKEN and a
32
+ * vault may hold limits for several, but this server knows the decimals of
33
+ * exactly one — `NEAR_VAULT_TOKEN_DECIMALS`, which describes
34
+ * `NEAR_VAULT_TOKEN`. Scaling some other token by 6 would print a number that
35
+ * is wrong by orders of magnitude while looking tidy, which is strictly worse
36
+ * than the raw figure it replaced. So a token we cannot vouch for keeps its
37
+ * base units and says so, rather than being guessed at.
38
+ *
39
+ * `decimals` is echoed on every converted entry so the base figure stays
40
+ * derivable; nothing is lost, it is only no longer the default reading.
41
+ */
42
+ export interface TreeLimit {
43
+ token: string;
44
+ daily_cap: string;
45
+ per_tx_cap: string;
46
+ }
47
+ export interface TreeNode {
48
+ limits?: TreeLimit[];
49
+ child_cap_sum?: [string, string][];
50
+ [k: string]: unknown;
51
+ }
52
+ export declare function humaniseTreeNode(node: TreeNode, cfg: {
53
+ token?: string;
54
+ decimals: number;
55
+ }): TreeNode;
package/dist/amounts.js CHANGED
@@ -53,3 +53,35 @@ export function fromBaseUnits(units, decimals) {
53
53
  const frac = (units % scale).toString().padStart(decimals, "0").replace(/0+$/, "");
54
54
  return frac ? `${whole}.${frac}` : whole.toString();
55
55
  }
56
+ export function humaniseTreeNode(node, cfg) {
57
+ if (!node || typeof node !== "object")
58
+ return node;
59
+ const known = (token) => !!cfg.token && token === cfg.token;
60
+ const human = (v) => {
61
+ // EXPLICIT SHAPE CHECK, because `BigInt("")` is `0n` rather than a throw —
62
+ // and `BigInt(" ")` is too. A try/catch alone therefore converted an
63
+ // empty or whitespace cap into "0", which is not a parse failure the
64
+ // reader can see: "0" is a meaningful budget, and the wrong one. Caught by
65
+ // its own test, which is the only reason this is not shipping.
66
+ if (typeof v !== "string" || !/^\d+$/.test(v))
67
+ return v;
68
+ try {
69
+ return fromBaseUnits(BigInt(v), cfg.decimals);
70
+ }
71
+ catch {
72
+ // Still guarded: fromBaseUnits rejects decimals outside its own range,
73
+ // and a figure the contract produced is a fact worth surfacing
74
+ // unchanged rather than an exception to swallow.
75
+ return v;
76
+ }
77
+ };
78
+ const limits = node.limits?.map((l) => known(l.token)
79
+ ? { ...l, daily_cap: human(l.daily_cap), per_tx_cap: human(l.per_tx_cap), decimals: cfg.decimals }
80
+ : { ...l, decimals: null, units: "base" });
81
+ const childCapSum = node.child_cap_sum?.map(([token, amount]) => (known(token) ? [token, human(amount)] : [token, amount]));
82
+ return {
83
+ ...node,
84
+ ...(limits ? { limits } : {}),
85
+ ...(childCapSum ? { child_cap_sum: childCapSum } : {}),
86
+ };
87
+ }
package/dist/roles.d.ts CHANGED
@@ -8,6 +8,7 @@ export declare class RoleSelectionError extends Error {
8
8
  }
9
9
  export declare function selectRole(cfg: {
10
10
  agentKeysPath?: string;
11
+ vault?: string;
11
12
  }, role: string): {
12
13
  agentId: string;
13
14
  agentKey: string;
@@ -15,6 +16,7 @@ export declare function selectRole(cfg: {
15
16
  /** Every role that could actually sign, for listing back to a caller. */
16
17
  export declare function listRoles(cfg: {
17
18
  agentKeysPath?: string;
19
+ vault?: string;
18
20
  }): RoleCredential[];
19
21
  /**
20
22
  * EVERY credential in the keyring, named or not.
@@ -36,6 +38,7 @@ export declare function listRoles(cfg: {
36
38
  */
37
39
  export declare function listAgents(cfg: {
38
40
  agentKeysPath?: string;
41
+ vault?: string;
39
42
  }): {
40
43
  agentId: string;
41
44
  role: string | null;
package/dist/roles.js CHANGED
@@ -29,11 +29,42 @@ function loadable(path) {
29
29
  // never be selected.
30
30
  return parsed.filter((e) => e && typeof e.agentId === "string" && typeof e.secret === "string");
31
31
  }
32
+ /**
33
+ * Keyring entries belonging to THIS vault.
34
+ *
35
+ * A credential's `agentId` IS the vault account it was issued for. Nothing
36
+ * filtered on it, so every function here operated on the whole file — every
37
+ * vault the user has ever created, on either network.
38
+ *
39
+ * Measured 2026-11-10 against a mainnet vault: `vault_roles` listed
40
+ * `rudeisland4178-testnet.fac.floatv4.testnet` and
41
+ * `work12-testnet-fleet.fac.floatv4.testnet` as that vault's agents. Both are
42
+ * TESTNET accounts from earlier experiments, and neither has any relationship
43
+ * to the configured mainnet vault.
44
+ *
45
+ * `vault_roles` listing them is a display bug. `selectRole` was worse: asked
46
+ * for role "root" it would hand `vault_pay` the testnet fleet key to sign a
47
+ * MAINNET payment. That fails at the chain — the key is not on the vault — but
48
+ * the tool chose a credential from a different network and the error arrives
49
+ * with no hint of why.
50
+ *
51
+ * `vault` empty means nothing is configured, so there is nothing to scope to
52
+ * and every entry is returned. That is the `vault_create` case, where the
53
+ * server legitimately runs with no vault at all.
54
+ */
55
+ function forVault(cfg) {
56
+ const entries = loadable(cfg.agentKeysPath ?? ".float/agent-keys.json");
57
+ if (!cfg.vault)
58
+ return entries;
59
+ return entries.filter((e) => e.agentId === cfg.vault);
60
+ }
32
61
  export class RoleSelectionError extends Error {
33
62
  }
34
63
  export function selectRole(cfg, role) {
35
64
  const path = cfg.agentKeysPath ?? ".float/agent-keys.json";
36
- const entries = loadable(path);
65
+ // SCOPED. A role name is not unique across vaults — "root" exists in every
66
+ // one — so an unscoped lookup picks whichever was written first.
67
+ const entries = forVault(cfg);
37
68
  const known = entries
38
69
  .map((e) => (typeof e.role === "string" ? e.role : null))
39
70
  .filter(Boolean);
@@ -50,7 +81,7 @@ export function selectRole(cfg, role) {
50
81
  }
51
82
  /** Every role that could actually sign, for listing back to a caller. */
52
83
  export function listRoles(cfg) {
53
- return loadable(cfg.agentKeysPath ?? ".float/agent-keys.json")
84
+ return forVault(cfg)
54
85
  .filter((e) => typeof e.role === "string")
55
86
  .map((e) => ({
56
87
  agentId: e.agentId,
@@ -78,7 +109,7 @@ export function listRoles(cfg) {
78
109
  * name is genuinely absent and the fix is `vault_add_role`, not a guess.
79
110
  */
80
111
  export function listAgents(cfg) {
81
- return loadable(cfg.agentKeysPath ?? ".float/agent-keys.json").map((e) => ({
112
+ return forVault(cfg).map((e) => ({
82
113
  agentId: e.agentId,
83
114
  role: typeof e.role === "string" ? e.role : null,
84
115
  nodeId: typeof e.nodeId === "number" ? e.nodeId : undefined,
package/dist/server.js CHANGED
@@ -53,7 +53,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
53
53
  import { topUpFor, paymentsAfterTopUp } from "./gas-math.js";
54
54
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
55
55
  import { z } from "zod";
56
- import { fromBaseUnits, toBaseUnits } from "./amounts.js";
56
+ import { fromBaseUnits, humaniseTreeNode, toBaseUnits } from "./amounts.js";
57
57
  import { DEFAULT_GAS_TGAS, planAddNode, planAddRole, planBindVirtualAgent, planUnbindVirtualAgent, planFreezeNode, planPurgeSettled, planSetCooldown, planSetNodeCaps, planSetPaused, planSetRecipients, toCliCommand, planSetApprovalThreshold, planApprovePayment, planRejectPayment, planWithdraw, } from "./payloads.js";
58
58
  import { accountExists, rpcForNetwork, newRequestId, requireAgentKey, viewAccessKeys, viewAccount, viewFunction, } from "./rpc.js";
59
59
  import { AGENT_NODE_ID, generateAgentKey } from "./agent-key.js";
@@ -292,7 +292,7 @@ WORTH KNOWING
292
292
  An implicit account (a 64-character hex id) cannot own a vault at all. Tell the user to use a
293
293
  named account rather than retrying.
294
294
  Amounts are whole tokens in these tools (1.5 means 1.5 USDC), never base units.`;
295
- const server = new McpServer({ name: "float", version: "0.6.4" }, { instructions: INSTRUCTIONS });
295
+ const server = new McpServer({ name: "float", version: "0.6.5" }, { instructions: INSTRUCTIONS });
296
296
  // ----------------------------------------------------------------- reads
297
297
  /**
298
298
  * Wait for a create to land, and prove the result belongs to the user.
@@ -420,7 +420,12 @@ server.tool("vault_await_create", "After the user signs the create link, confirm
420
420
  ],
421
421
  });
422
422
  });
423
- 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", {})));
423
+ 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 () => {
424
+ const tree = (await viewFunction(cfg.rpcUrl, requireVault(), "get_tree", {}));
425
+ return json(Array.isArray(tree)
426
+ ? tree.map((n) => humaniseTreeNode(n, { token: cfg.token, decimals: cfg.decimals }))
427
+ : tree);
428
+ });
424
429
  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.", {
425
430
  nodeId: z.number().int().min(0),
426
431
  token: z.string().optional(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floatonnear/mcp",
3
- "version": "0.6.4",
3
+ "version": "0.6.5",
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",