@floatonnear/mcp 0.6.3 → 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,4 +16,31 @@ 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[];
21
+ /**
22
+ * EVERY credential in the keyring, named or not.
23
+ *
24
+ * `listRoles` filters to entries carrying a `role`, which is right for
25
+ * SELECTION — you cannot pick by a name that does not exist. It is wrong for
26
+ * REPORTING, and `vault_roles` used it for reporting.
27
+ *
28
+ * `vault_create({ bindAgent: true })` calls `generateAgentKey({ vault })` with
29
+ * no role, so `JSON.stringify` drops the field and the entry is invisible to
30
+ * `listRoles`. Measured 2026-11-10: a vault with a funded, correctly-pinned
31
+ * gas key and a live agent node reported `vault_roles` -> `[]`, which reads as
32
+ * "you have no agents" rather than "you have one with no name". The user then
33
+ * cannot tell an empty keyring from a working unnamed agent, and the obvious
34
+ * next move — bind another — is the wrong one.
35
+ *
36
+ * Returns `role: null` for those rather than inventing a name, because the
37
+ * name is genuinely absent and the fix is `vault_add_role`, not a guess.
38
+ */
39
+ export declare function listAgents(cfg: {
40
+ agentKeysPath?: string;
41
+ vault?: string;
42
+ }): {
43
+ agentId: string;
44
+ role: string | null;
45
+ nodeId?: number;
46
+ }[];
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,
@@ -59,3 +90,28 @@ export function listRoles(cfg) {
59
90
  nodeId: typeof e.nodeId === "number" ? e.nodeId : undefined,
60
91
  }));
61
92
  }
93
+ /**
94
+ * EVERY credential in the keyring, named or not.
95
+ *
96
+ * `listRoles` filters to entries carrying a `role`, which is right for
97
+ * SELECTION — you cannot pick by a name that does not exist. It is wrong for
98
+ * REPORTING, and `vault_roles` used it for reporting.
99
+ *
100
+ * `vault_create({ bindAgent: true })` calls `generateAgentKey({ vault })` with
101
+ * no role, so `JSON.stringify` drops the field and the entry is invisible to
102
+ * `listRoles`. Measured 2026-11-10: a vault with a funded, correctly-pinned
103
+ * gas key and a live agent node reported `vault_roles` -> `[]`, which reads as
104
+ * "you have no agents" rather than "you have one with no name". The user then
105
+ * cannot tell an empty keyring from a working unnamed agent, and the obvious
106
+ * next move — bind another — is the wrong one.
107
+ *
108
+ * Returns `role: null` for those rather than inventing a name, because the
109
+ * name is genuinely absent and the fix is `vault_add_role`, not a guess.
110
+ */
111
+ export function listAgents(cfg) {
112
+ return forVault(cfg).map((e) => ({
113
+ agentId: e.agentId,
114
+ role: typeof e.role === "string" ? e.role : null,
115
+ nodeId: typeof e.nodeId === "number" ? e.nodeId : undefined,
116
+ }));
117
+ }
package/dist/server.d.ts CHANGED
@@ -16,7 +16,11 @@
16
16
  *
17
17
  * Configure with:
18
18
  * NEAR_VAULT_CONTRACT required by every tool EXCEPT vault_create
19
- * NEAR_VAULT_RPC_URL default https://test.rpc.fastnear.com
19
+ * NEAR_VAULT_RPC_URL OPTIONAL. Defaults to the RPC for
20
+ * NEAR_VAULT_NETWORK — free.rpc.fastnear.com on
21
+ * mainnet, test.rpc.fastnear.com on testnet. It
22
+ * used to default to the testnet URL regardless,
23
+ * so every mainnet install read the wrong chain.
20
24
  * NEAR_VAULT_TOKEN default token for pay/budget calls
21
25
  * NEAR_VAULT_TOKEN_DECIMALS default 6
22
26
  * NEAR_VAULT_AGENT_ID only needed to PAY. Taken from the key file's
package/dist/server.js CHANGED
@@ -16,7 +16,11 @@
16
16
  *
17
17
  * Configure with:
18
18
  * NEAR_VAULT_CONTRACT required by every tool EXCEPT vault_create
19
- * NEAR_VAULT_RPC_URL default https://test.rpc.fastnear.com
19
+ * NEAR_VAULT_RPC_URL OPTIONAL. Defaults to the RPC for
20
+ * NEAR_VAULT_NETWORK — free.rpc.fastnear.com on
21
+ * mainnet, test.rpc.fastnear.com on testnet. It
22
+ * used to default to the testnet URL regardless,
23
+ * so every mainnet install read the wrong chain.
20
24
  * NEAR_VAULT_TOKEN default token for pay/budget calls
21
25
  * NEAR_VAULT_TOKEN_DECIMALS default 6
22
26
  * NEAR_VAULT_AGENT_ID only needed to PAY. Taken from the key file's
@@ -49,13 +53,13 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
49
53
  import { topUpFor, paymentsAfterTopUp } from "./gas-math.js";
50
54
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
51
55
  import { z } from "zod";
52
- import { fromBaseUnits, toBaseUnits } from "./amounts.js";
56
+ import { fromBaseUnits, humaniseTreeNode, toBaseUnits } from "./amounts.js";
53
57
  import { DEFAULT_GAS_TGAS, planAddNode, planAddRole, planBindVirtualAgent, planUnbindVirtualAgent, planFreezeNode, planPurgeSettled, planSetCooldown, planSetNodeCaps, planSetPaused, planSetRecipients, toCliCommand, planSetApprovalThreshold, planApprovePayment, planRejectPayment, planWithdraw, } from "./payloads.js";
54
- import { accountExists, rpcForNetwork, DEFAULT_RPC_URL, newRequestId, requireAgentKey, viewAccessKeys, viewAccount, viewFunction, } from "./rpc.js";
58
+ import { accountExists, rpcForNetwork, newRequestId, requireAgentKey, viewAccessKeys, viewAccount, viewFunction, } from "./rpc.js";
55
59
  import { AGENT_NODE_ID, generateAgentKey } from "./agent-key.js";
56
60
  import { resolveKeySource } from "./key-file.js";
57
61
  import { dirname } from "node:path";
58
- import { listRoles, selectRole } from "./roles.js";
62
+ import { listAgents, listRoles, selectRole } from "./roles.js";
59
63
  /**
60
64
  * A decimal NEAR string to yoctoNEAR, without floating point.
61
65
  *
@@ -114,9 +118,46 @@ for (const err of [agentKeySource.error, quoteSignerSource.error]) {
114
118
  if (err)
115
119
  console.error(`float: ${err}`);
116
120
  }
121
+ /**
122
+ * The network, resolved BEFORE `cfg` so the RPC can follow it.
123
+ *
124
+ * It used to be read inside the object literal, which meant `rpcUrl` could not
125
+ * see it and fell back to a constant. See below.
126
+ */
127
+ const NETWORK = process.env.NEAR_VAULT_NETWORK ?? "testnet";
117
128
  const cfg = {
118
129
  vault: process.env.NEAR_VAULT_CONTRACT ?? "",
119
- rpcUrl: process.env.NEAR_VAULT_RPC_URL ?? DEFAULT_RPC_URL,
130
+ /**
131
+ * THE RPC FOLLOWS THE NETWORK. It did not, and that broke every mainnet
132
+ * install.
133
+ *
134
+ * This was `process.env.NEAR_VAULT_RPC_URL ?? DEFAULT_RPC_URL`, and
135
+ * `DEFAULT_RPC_URL` is the TESTNET endpoint. So a user who set
136
+ * `NEAR_VAULT_NETWORK=mainnet` — the documented way to be on mainnet — and
137
+ * did not also happen to set `NEAR_VAULT_RPC_URL` had all fourteen tools
138
+ * that use `cfg.rpcUrl` querying TESTNET for a mainnet vault. Reads answered
139
+ * "account does not exist" about an account holding 3.35 NEAR, and
140
+ * `vault_pay` would have submitted a real payment to the wrong chain.
141
+ *
142
+ * Measured 2026-11-10 on test015-near.usefloat.near: `vault_config` and
143
+ * `vault_tree` reported the vault missing while `vault_await_create` read
144
+ * its config fine — the two tools differ only in that `vault_create` and
145
+ * `vault_await_create` call `rpcForNetwork()` and everything else took this
146
+ * constant.
147
+ *
148
+ * THAT ASYMMETRY ALSO DEFEATED THE DIAGNOSIS. The testnet theory was correct
149
+ * and was "refuted" by testing it with `vault_await_create`, the one tool
150
+ * immune to the bug — so the right answer was discarded on the strength of a
151
+ * check that could not fail. Pick the probe that exercises the broken path,
152
+ * not the convenient one.
153
+ *
154
+ * `rpcForNetwork` honours `NEAR_VAULT_RPC_URL` only when it applies to the
155
+ * network in play, which is the behaviour `vault_create` already relied on.
156
+ */
157
+ rpcUrl: rpcForNetwork(NETWORK, {
158
+ configuredNetwork: NETWORK,
159
+ override: process.env.NEAR_VAULT_RPC_URL,
160
+ }),
120
161
  token: process.env.NEAR_VAULT_TOKEN ?? "",
121
162
  decimals: Number(process.env.NEAR_VAULT_TOKEN_DECIMALS ?? "6"),
122
163
  // The credentials file carries the account as well as the key, so one path
@@ -126,7 +167,7 @@ const cfg = {
126
167
  agentKey: agentKeySource.secret,
127
168
  agentKeyError: agentKeySource.error,
128
169
  agentKeyAccountId: agentKeySource.accountId,
129
- network: process.env.NEAR_VAULT_NETWORK ?? "testnet",
170
+ network: NETWORK,
130
171
  // Onboarding only. Both have safe defaults and neither is needed by any
131
172
  // tool that touches an existing vault.
132
173
  factory: process.env.NEAR_VAULT_FACTORY,
@@ -251,7 +292,7 @@ WORTH KNOWING
251
292
  An implicit account (a 64-character hex id) cannot own a vault at all. Tell the user to use a
252
293
  named account rather than retrying.
253
294
  Amounts are whole tokens in these tools (1.5 means 1.5 USDC), never base units.`;
254
- const server = new McpServer({ name: "float", version: "0.6.3" }, { instructions: INSTRUCTIONS });
295
+ const server = new McpServer({ name: "float", version: "0.6.5" }, { instructions: INSTRUCTIONS });
255
296
  // ----------------------------------------------------------------- reads
256
297
  /**
257
298
  * Wait for a create to land, and prove the result belongs to the user.
@@ -275,7 +316,10 @@ const server = new McpServer({ name: "float", version: "0.6.3" }, { instructions
275
316
  * after a write can be a whole transaction behind.
276
317
  */
277
318
  server.tool("vault_await_create", "After the user signs the create link, confirm the vault is live and VERIFY IT IS THEIRS. Reads the chain, so it needs no key. Call this instead of asking the user to paste their vault id back — vault_create already told you the address.", {
278
- vault: z.string().describe("the derived vault id, from vault_create's `confirm.vault`"),
319
+ vault: z
320
+ .string()
321
+ .optional()
322
+ .describe("the derived vault id, from vault_create's `confirm.vault`. OMIT IT if the user already created their vault and you never built the link — it is derived from `owner` via the factory's own vault_id_for view."),
279
323
  owner: z.string().describe("who it must belong to — compared against the vault's own get_config.owner"),
280
324
  network: z.enum(["mainnet", "testnet"]).optional(),
281
325
  timeoutSeconds: z
@@ -291,10 +335,41 @@ server.tool("vault_await_create", "After the user signs the create link, confirm
291
335
  configuredNetwork: cfg.network,
292
336
  override: process.env.NEAR_VAULT_RPC_URL,
293
337
  });
338
+ // DERIVE IT WHEN THE CALLER HAS NO LINK TO QUOTE.
339
+ //
340
+ // A user who says "I already created it" never gave this server a plan to
341
+ // read `confirm.vault` out of, and does not know their vault id — it is
342
+ // derived, not chosen. Requiring it here left the agent with only one tool
343
+ // that knows the derivation: `vault_create`. Measured 2026-11-10, that is
344
+ // exactly what an agent did, and `vault_create` THREW — correctly —
345
+ // "already exists and test015.near owns it". The agent then reported that
346
+ // refusal to the user as "Confirmed ✅". It reached the right answer by
347
+ // reinterpreting an error, which is luck, not a workflow.
348
+ //
349
+ // So adoption gets a first-class path: `owner` alone is enough. The
350
+ // derivation comes from the factory's `vault_id_for` view, the same single
351
+ // source `buildCreatePlan` uses and deliberately does not reimplement.
352
+ let target = vault;
353
+ if (!target) {
354
+ const fac = cfg.factory ?? FACTORY_BY_NETWORK[net];
355
+ if (!fac) {
356
+ return text(`No factory is configured for ${net}, so the vault id cannot be derived. ` +
357
+ `Pass \`vault\` directly, or set NEAR_VAULT_FACTORY.`);
358
+ }
359
+ const derived = await viewFunction(rpc, fac, "vault_id_for", {
360
+ owner,
361
+ }).catch(() => null);
362
+ if (!derived) {
363
+ return text(`${fac} could not derive a vault id for ${owner} on ${net}. Either the ` +
364
+ `owner id is wrong, or it has no legal derived id (an implicit account ` +
365
+ `has none — the derived name exceeds 64 characters).`);
366
+ }
367
+ target = derived;
368
+ }
294
369
  const deadline = Date.now() + (timeoutSeconds ?? 90) * 1000;
295
370
  let appeared = false;
296
371
  for (;;) {
297
- if (await accountExists(rpc, vault).catch(() => false)) {
372
+ if (await accountExists(rpc, target).catch(() => false)) {
298
373
  appeared = true;
299
374
  break;
300
375
  }
@@ -305,29 +380,29 @@ server.tool("vault_await_create", "After the user signs the create link, confirm
305
380
  if (!appeared) {
306
381
  return json({
307
382
  status: "NOT_YET",
308
- vault,
309
- instruction: `${vault} does not exist yet. That is not a failure — the user may not have ` +
383
+ vault: target,
384
+ instruction: `${target} does not exist yet. That is not a failure — the user may not have ` +
310
385
  `signed, or may have cancelled. Ask whether they approved it, and call this ` +
311
386
  `again. Do NOT tell them the vault is created.`,
312
387
  });
313
388
  }
314
389
  // THE OWNER READ IS THE POINT. Getting here only proves an account exists.
315
- const config = await viewFunction(rpc, vault, "get_config", {}).catch(() => null);
390
+ const config = await viewFunction(rpc, target, "get_config", {}).catch(() => null);
316
391
  if (!config || typeof config.owner !== "string") {
317
392
  return json({
318
393
  status: "UNVERIFIED",
319
- vault,
320
- instruction: `${vault} exists but its get_config could not be read, so whether it is ${owner}'s ` +
394
+ vault: target,
395
+ instruction: `${target} exists but its get_config could not be read, so whether it is ${owner}'s ` +
321
396
  `is UNKNOWN — not assumed. It may not be a Float vault at all. Do not point an ` +
322
- `agent at it until this is resolved: near view ${vault} get_config`,
397
+ `agent at it until this is resolved: near view ${target} get_config`,
323
398
  });
324
399
  }
325
400
  if (config.owner !== owner) {
326
401
  return json({
327
402
  status: "NOT_YOURS",
328
- vault,
403
+ vault: target,
329
404
  actualOwner: config.owner,
330
- instruction: `${vault} exists but is owned by ${config.owner}, NOT ${owner}. The vault id ` +
405
+ instruction: `${target} exists but is owned by ${config.owner}, NOT ${owner}. The vault id ` +
331
406
  `derivation is not injective, so a different account can hold this address — ` +
332
407
  `whoever created it first. Do NOT configure anything against it. Create one ` +
333
408
  `under a label instead, which lands on a different address.`,
@@ -335,17 +410,22 @@ server.tool("vault_await_create", "After the user signs the create link, confirm
335
410
  }
336
411
  return json({
337
412
  status: "LIVE",
338
- vault,
413
+ vault: target,
339
414
  owner: config.owner,
340
415
  config,
341
416
  nextSteps: [
342
- `Set NEAR_VAULT_CONTRACT=${vault} so every other tool reads this vault.`,
417
+ `Set NEAR_VAULT_CONTRACT=${target} so every other tool reads this vault.`,
343
418
  `vault_tree shows the budget; vault_budget shows what is left today.`,
344
419
  `Nothing can spend from it until an agent is bound with a budget — vault_add_role.`,
345
420
  ],
346
421
  });
347
422
  });
348
- 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
+ });
349
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.", {
350
430
  nodeId: z.number().int().min(0),
351
431
  token: z.string().optional(),
@@ -435,7 +515,24 @@ server.tool("vault_add_role", "Add a NEW departmental agent to an existing vault
435
515
  ],
436
516
  });
437
517
  });
438
- server.tool("vault_roles", "List the agents this server can sign as, by role. Use it before vault_pay when a vault has more than one agent — the signing key decides which budget is charged. No key material is returned.", {}, async () => json(listRoles(cfg).map((r) => ({ role: r.role, nodeId: r.nodeId, agentId: r.agentId }))));
518
+ server.tool("vault_roles", "List the agents this server can sign as, by role. Use it before vault_pay when a vault has more than one agent — the signing key decides which budget is charged. No key material is returned.", {}, async () => json(
519
+ // `listAgents`, NOT `listRoles`. The latter filters to entries carrying a
520
+ // role, which is correct for SELECTION and wrong for REPORTING: a
521
+ // creation-time agent from `bindAgent` has no role, so a working vault
522
+ // with a funded gas key reported `[]` — indistinguishable from having no
523
+ // agent at all, and the obvious next move is then the wrong one.
524
+ listAgents(cfg).map((r) => ({
525
+ role: r.role,
526
+ nodeId: r.nodeId,
527
+ agentId: r.agentId,
528
+ ...(r.role === null
529
+ ? {
530
+ note: "No role name — created by vault_create({bindAgent:true}), which does not " +
531
+ "name one. It can still sign; vault_pay selects it by nodeId or as the only " +
532
+ "credential. Give it a name with vault_add_role if you want several agents.",
533
+ }
534
+ : {}),
535
+ }))));
439
536
  // ------------------------------------------------------------- onboarding
440
537
  server.tool("vault_create", "For a user who has NO vault yet. Works out where their vault will live, exactly what it costs to create, and returns a URL they approve in their own wallet. Needs no key and no existing vault — this is the only tool that works before NEAR_VAULT_CONTRACT is set. This server never signs it.", {
441
538
  owner: z
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floatonnear/mcp",
3
- "version": "0.6.3",
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",