@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 +35 -0
- package/dist/amounts.js +32 -0
- package/dist/roles.d.ts +3 -0
- package/dist/roles.js +34 -3
- package/dist/server.js +8 -3
- package/package.json +1 -1
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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 () =>
|
|
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.
|
|
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",
|