@floatonnear/mcp 0.6.3 → 0.6.4
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/roles.d.ts +25 -0
- package/dist/roles.js +25 -0
- package/dist/server.d.ts +5 -1
- package/dist/server.js +111 -19
- package/package.json +1 -1
package/dist/roles.d.ts
CHANGED
|
@@ -16,3 +16,28 @@ export declare function selectRole(cfg: {
|
|
|
16
16
|
export declare function listRoles(cfg: {
|
|
17
17
|
agentKeysPath?: string;
|
|
18
18
|
}): RoleCredential[];
|
|
19
|
+
/**
|
|
20
|
+
* EVERY credential in the keyring, named or not.
|
|
21
|
+
*
|
|
22
|
+
* `listRoles` filters to entries carrying a `role`, which is right for
|
|
23
|
+
* SELECTION — you cannot pick by a name that does not exist. It is wrong for
|
|
24
|
+
* REPORTING, and `vault_roles` used it for reporting.
|
|
25
|
+
*
|
|
26
|
+
* `vault_create({ bindAgent: true })` calls `generateAgentKey({ vault })` with
|
|
27
|
+
* no role, so `JSON.stringify` drops the field and the entry is invisible to
|
|
28
|
+
* `listRoles`. Measured 2026-11-10: a vault with a funded, correctly-pinned
|
|
29
|
+
* gas key and a live agent node reported `vault_roles` -> `[]`, which reads as
|
|
30
|
+
* "you have no agents" rather than "you have one with no name". The user then
|
|
31
|
+
* cannot tell an empty keyring from a working unnamed agent, and the obvious
|
|
32
|
+
* next move — bind another — is the wrong one.
|
|
33
|
+
*
|
|
34
|
+
* Returns `role: null` for those rather than inventing a name, because the
|
|
35
|
+
* name is genuinely absent and the fix is `vault_add_role`, not a guess.
|
|
36
|
+
*/
|
|
37
|
+
export declare function listAgents(cfg: {
|
|
38
|
+
agentKeysPath?: string;
|
|
39
|
+
}): {
|
|
40
|
+
agentId: string;
|
|
41
|
+
role: string | null;
|
|
42
|
+
nodeId?: number;
|
|
43
|
+
}[];
|
package/dist/roles.js
CHANGED
|
@@ -59,3 +59,28 @@ export function listRoles(cfg) {
|
|
|
59
59
|
nodeId: typeof e.nodeId === "number" ? e.nodeId : undefined,
|
|
60
60
|
}));
|
|
61
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* EVERY credential in the keyring, named or not.
|
|
64
|
+
*
|
|
65
|
+
* `listRoles` filters to entries carrying a `role`, which is right for
|
|
66
|
+
* SELECTION — you cannot pick by a name that does not exist. It is wrong for
|
|
67
|
+
* REPORTING, and `vault_roles` used it for reporting.
|
|
68
|
+
*
|
|
69
|
+
* `vault_create({ bindAgent: true })` calls `generateAgentKey({ vault })` with
|
|
70
|
+
* no role, so `JSON.stringify` drops the field and the entry is invisible to
|
|
71
|
+
* `listRoles`. Measured 2026-11-10: a vault with a funded, correctly-pinned
|
|
72
|
+
* gas key and a live agent node reported `vault_roles` -> `[]`, which reads as
|
|
73
|
+
* "you have no agents" rather than "you have one with no name". The user then
|
|
74
|
+
* cannot tell an empty keyring from a working unnamed agent, and the obvious
|
|
75
|
+
* next move — bind another — is the wrong one.
|
|
76
|
+
*
|
|
77
|
+
* Returns `role: null` for those rather than inventing a name, because the
|
|
78
|
+
* name is genuinely absent and the fix is `vault_add_role`, not a guess.
|
|
79
|
+
*/
|
|
80
|
+
export function listAgents(cfg) {
|
|
81
|
+
return loadable(cfg.agentKeysPath ?? ".float/agent-keys.json").map((e) => ({
|
|
82
|
+
agentId: e.agentId,
|
|
83
|
+
role: typeof e.role === "string" ? e.role : null,
|
|
84
|
+
nodeId: typeof e.nodeId === "number" ? e.nodeId : undefined,
|
|
85
|
+
}));
|
|
86
|
+
}
|
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
|
|
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
|
|
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
|
|
@@ -51,11 +55,11 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
51
55
|
import { z } from "zod";
|
|
52
56
|
import { fromBaseUnits, 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,
|
|
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
|
-
|
|
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:
|
|
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.
|
|
295
|
+
const server = new McpServer({ name: "float", version: "0.6.4" }, { 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
|
|
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,
|
|
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: `${
|
|
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,
|
|
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: `${
|
|
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 ${
|
|
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: `${
|
|
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,11 +410,11 @@ 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=${
|
|
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
|
],
|
|
@@ -435,7 +510,24 @@ server.tool("vault_add_role", "Add a NEW departmental agent to an existing vault
|
|
|
435
510
|
],
|
|
436
511
|
});
|
|
437
512
|
});
|
|
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(
|
|
513
|
+
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(
|
|
514
|
+
// `listAgents`, NOT `listRoles`. The latter filters to entries carrying a
|
|
515
|
+
// role, which is correct for SELECTION and wrong for REPORTING: a
|
|
516
|
+
// creation-time agent from `bindAgent` has no role, so a working vault
|
|
517
|
+
// with a funded gas key reported `[]` — indistinguishable from having no
|
|
518
|
+
// agent at all, and the obvious next move is then the wrong one.
|
|
519
|
+
listAgents(cfg).map((r) => ({
|
|
520
|
+
role: r.role,
|
|
521
|
+
nodeId: r.nodeId,
|
|
522
|
+
agentId: r.agentId,
|
|
523
|
+
...(r.role === null
|
|
524
|
+
? {
|
|
525
|
+
note: "No role name — created by vault_create({bindAgent:true}), which does not " +
|
|
526
|
+
"name one. It can still sign; vault_pay selects it by nodeId or as the only " +
|
|
527
|
+
"credential. Give it a name with vault_add_role if you want several agents.",
|
|
528
|
+
}
|
|
529
|
+
: {}),
|
|
530
|
+
}))));
|
|
439
531
|
// ------------------------------------------------------------- onboarding
|
|
440
532
|
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
533
|
owner: z
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@floatonnear/mcp",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
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",
|