@floatonnear/mcp 0.4.0 → 0.6.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.
- package/dist/agent-key.d.ts +15 -3
- package/dist/agent-key.js +15 -3
- package/dist/create.d.ts +44 -5
- package/dist/create.js +44 -8
- package/dist/server.js +71 -1
- package/package.json +1 -1
package/dist/agent-key.d.ts
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The node a creation-time agent is bound to.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* node that
|
|
4
|
+
* ONE, NOT ZERO, SINCE 2026-11-08. `new` used to bind at ROOT_NODE because it
|
|
5
|
+
* was the only node that existed — which left the FIRST agent holding the
|
|
6
|
+
* whole vault budget while every agent added later through `add_role` got a
|
|
7
|
+
* child of its own. The hierarchy only began constraining from agent #2.
|
|
8
|
+
*
|
|
9
|
+
* `new` now creates the agent its own child of root, carrying the same limits,
|
|
10
|
+
* exactly as `add_role` does. Root is 0 and that child is the first node
|
|
11
|
+
* inserted after it, so the agent is 1.
|
|
12
|
+
*
|
|
13
|
+
* PREDICTED, NOT READ, and that is unavoidable: the credential is generated
|
|
14
|
+
* before the vault exists, so there is nothing to ask. It is pinned by
|
|
15
|
+
* `binding_at_init_matches_binding_afterwards` in the contract, which asserts
|
|
16
|
+
* the node is 1 and not 0 — if `new` ever inserts another node first, that
|
|
17
|
+
* test fails rather than this constant drifting in silence.
|
|
6
18
|
*/
|
|
7
|
-
export declare const AGENT_NODE_ID =
|
|
19
|
+
export declare const AGENT_NODE_ID = 1;
|
|
8
20
|
export interface GeneratedAgentKey {
|
|
9
21
|
publicKey: string;
|
|
10
22
|
/** The role this credential was filed under, when one was given. */
|
package/dist/agent-key.js
CHANGED
|
@@ -21,10 +21,22 @@ import { KeyPair } from "near-api-js";
|
|
|
21
21
|
/**
|
|
22
22
|
* The node a creation-time agent is bound to.
|
|
23
23
|
*
|
|
24
|
-
*
|
|
25
|
-
* node that
|
|
24
|
+
* ONE, NOT ZERO, SINCE 2026-11-08. `new` used to bind at ROOT_NODE because it
|
|
25
|
+
* was the only node that existed — which left the FIRST agent holding the
|
|
26
|
+
* whole vault budget while every agent added later through `add_role` got a
|
|
27
|
+
* child of its own. The hierarchy only began constraining from agent #2.
|
|
28
|
+
*
|
|
29
|
+
* `new` now creates the agent its own child of root, carrying the same limits,
|
|
30
|
+
* exactly as `add_role` does. Root is 0 and that child is the first node
|
|
31
|
+
* inserted after it, so the agent is 1.
|
|
32
|
+
*
|
|
33
|
+
* PREDICTED, NOT READ, and that is unavoidable: the credential is generated
|
|
34
|
+
* before the vault exists, so there is nothing to ask. It is pinned by
|
|
35
|
+
* `binding_at_init_matches_binding_afterwards` in the contract, which asserts
|
|
36
|
+
* the node is 1 and not 0 — if `new` ever inserts another node first, that
|
|
37
|
+
* test fails rather than this constant drifting in silence.
|
|
26
38
|
*/
|
|
27
|
-
export const AGENT_NODE_ID =
|
|
39
|
+
export const AGENT_NODE_ID = 1;
|
|
28
40
|
/**
|
|
29
41
|
* Write the agent's credential where the adapter will actually find it.
|
|
30
42
|
*
|
package/dist/create.d.ts
CHANGED
|
@@ -24,13 +24,18 @@
|
|
|
24
24
|
* caught the drift the moment the contract shrank, which is the entire
|
|
25
25
|
* reason it exists.
|
|
26
26
|
*
|
|
27
|
+
* 298,819 -> 298,920 on 2026-11-08, when `new` began giving a creation-time
|
|
28
|
+
* agent its OWN child node instead of binding it to root. +101 bytes, about
|
|
29
|
+
* 0.00101 NEAR more per vault. The pin caught this one too, on the first gate
|
|
30
|
+
* run after the rebuild.
|
|
31
|
+
*
|
|
27
32
|
* Mirrors `packages/create-float/res/vault.wasm`, and `create.pins.test.ts`
|
|
28
33
|
* reads that file and fails if the two disagree — so a contract rebuild cannot
|
|
29
34
|
* silently leave this quoting a deposit that is too small. Too small is not a
|
|
30
35
|
* clean failure: CreateAccount succeeds and the vault sits below its own
|
|
31
36
|
* storage requirement, bricking on the first write.
|
|
32
37
|
*/
|
|
33
|
-
export declare const CONTRACT_BYTES =
|
|
38
|
+
export declare const CONTRACT_BYTES = 298920;
|
|
34
39
|
/** NEAR charges 1e19 yoctoNEAR per byte of storage. */
|
|
35
40
|
export declare const STORAGE_COST_PER_BYTE: bigint;
|
|
36
41
|
/**
|
|
@@ -104,10 +109,24 @@ export declare const GAS_RESERVATION_YOCTO: bigint;
|
|
|
104
109
|
/**
|
|
105
110
|
* Known factories.
|
|
106
111
|
*
|
|
107
|
-
* MAINNET
|
|
108
|
-
* default here does not fail safely: it sends a real user with real
|
|
109
|
-
* sign a 3.4 NEAR deposit into an account we do not control.
|
|
110
|
-
*
|
|
112
|
+
* MAINNET WAS DELIBERATELY ABSENT until one was deployed AND VERIFIED, because
|
|
113
|
+
* a wrong default here does not fail safely: it sends a real user with real
|
|
114
|
+
* NEAR to sign a ~3.4 NEAR deposit into an account we do not control.
|
|
115
|
+
*
|
|
116
|
+
* That precondition is now met. `usefloat.near` was registered on 2026-11-09
|
|
117
|
+
* (tx 2Gfh9H5T1VhEZ465NJ15CnuxFZpyMgsjchY66Lgsvntm) and the factory deployed
|
|
118
|
+
* into it, verified against the chain rather than against the deploy's own
|
|
119
|
+
* report:
|
|
120
|
+
*
|
|
121
|
+
* local sha256 ee925937c7b90771c477dd5e0a36770cd2c9654e…
|
|
122
|
+
* chain code_hash (b58 -> hex) ee925937c7b90771c477dd5e0a36770cd2c9654e…
|
|
123
|
+
* deployed bytes fetched 163,525 — byte-for-byte equal to the artifact
|
|
124
|
+
* get_config network mainnet, not paused, deposit 0.1
|
|
125
|
+
* vault_id_for(test015.near) test015-near.usefloat.near
|
|
126
|
+
*
|
|
127
|
+
* The name is TOP-LEVEL on purpose (DECISIONS.md 2026-11-08): it is embedded
|
|
128
|
+
* in every user's vault address permanently, because a vault is a sub-account
|
|
129
|
+
* of its factory, and `float.near` was already taken.
|
|
111
130
|
*/
|
|
112
131
|
export declare const FACTORY_BY_NETWORK: Record<string, string | undefined>;
|
|
113
132
|
export interface CreatePlanInput {
|
|
@@ -131,6 +150,26 @@ export interface CreatePlanInput {
|
|
|
131
150
|
appUrl: string;
|
|
132
151
|
/** Whether the derived account is already on chain. */
|
|
133
152
|
exists?: boolean;
|
|
153
|
+
/**
|
|
154
|
+
* The EXISTING account's own `get_config.owner`, when it could be read.
|
|
155
|
+
*
|
|
156
|
+
* WHY THIS IS NOT OPTIONAL REASONING. "The account at your derived id exists,
|
|
157
|
+
* therefore it is yours" is an inference from the derivation, not a fact
|
|
158
|
+
* about the account — and `derive_prefix` IS NOT INJECTIVE. The factory says
|
|
159
|
+
* so in capitals, and `derive_prefix_collides_between_a_dot_and_a_hyphen`
|
|
160
|
+
* asserts it: `a.b.testnet` and `a-b.testnet` both land on `a-b-testnet`,
|
|
161
|
+
* because replacing the dot merges it into an alphabet that already contains
|
|
162
|
+
* the hyphen. Whoever creates first takes the id.
|
|
163
|
+
*
|
|
164
|
+
* Without reading the vault back, this tool would tell the LOSER of that
|
|
165
|
+
* collision to point their agent at the WINNER's vault. Narrow, but the
|
|
166
|
+
* failure is "you are now configured against someone else's money".
|
|
167
|
+
*
|
|
168
|
+
* `null` means the read failed, or the account is not a Float vault at all.
|
|
169
|
+
* That is a THIRD outcome and must not be flattened into either of the
|
|
170
|
+
* others — "I could not check" is not "it is yours".
|
|
171
|
+
*/
|
|
172
|
+
existingOwner?: string | null;
|
|
134
173
|
/**
|
|
135
174
|
* The agent public key to bind at creation, if any.
|
|
136
175
|
*
|
package/dist/create.js
CHANGED
|
@@ -24,13 +24,18 @@
|
|
|
24
24
|
* caught the drift the moment the contract shrank, which is the entire
|
|
25
25
|
* reason it exists.
|
|
26
26
|
*
|
|
27
|
+
* 298,819 -> 298,920 on 2026-11-08, when `new` began giving a creation-time
|
|
28
|
+
* agent its OWN child node instead of binding it to root. +101 bytes, about
|
|
29
|
+
* 0.00101 NEAR more per vault. The pin caught this one too, on the first gate
|
|
30
|
+
* run after the rebuild.
|
|
31
|
+
*
|
|
27
32
|
* Mirrors `packages/create-float/res/vault.wasm`, and `create.pins.test.ts`
|
|
28
33
|
* reads that file and fails if the two disagree — so a contract rebuild cannot
|
|
29
34
|
* silently leave this quoting a deposit that is too small. Too small is not a
|
|
30
35
|
* clean failure: CreateAccount succeeds and the vault sits below its own
|
|
31
36
|
* storage requirement, bricking on the first write.
|
|
32
37
|
*/
|
|
33
|
-
export const CONTRACT_BYTES =
|
|
38
|
+
export const CONTRACT_BYTES = 298920;
|
|
34
39
|
/** NEAR charges 1e19 yoctoNEAR per byte of storage. */
|
|
35
40
|
export const STORAGE_COST_PER_BYTE = 10n ** 19n;
|
|
36
41
|
/**
|
|
@@ -104,14 +109,28 @@ export const GAS_RESERVATION_YOCTO = CREATE_GAS_TGAS * 10n ** 12n * 10n ** 9n;
|
|
|
104
109
|
/**
|
|
105
110
|
* Known factories.
|
|
106
111
|
*
|
|
107
|
-
* MAINNET
|
|
108
|
-
* default here does not fail safely: it sends a real user with real
|
|
109
|
-
* sign a 3.4 NEAR deposit into an account we do not control.
|
|
110
|
-
*
|
|
112
|
+
* MAINNET WAS DELIBERATELY ABSENT until one was deployed AND VERIFIED, because
|
|
113
|
+
* a wrong default here does not fail safely: it sends a real user with real
|
|
114
|
+
* NEAR to sign a ~3.4 NEAR deposit into an account we do not control.
|
|
115
|
+
*
|
|
116
|
+
* That precondition is now met. `usefloat.near` was registered on 2026-11-09
|
|
117
|
+
* (tx 2Gfh9H5T1VhEZ465NJ15CnuxFZpyMgsjchY66Lgsvntm) and the factory deployed
|
|
118
|
+
* into it, verified against the chain rather than against the deploy's own
|
|
119
|
+
* report:
|
|
120
|
+
*
|
|
121
|
+
* local sha256 ee925937c7b90771c477dd5e0a36770cd2c9654e…
|
|
122
|
+
* chain code_hash (b58 -> hex) ee925937c7b90771c477dd5e0a36770cd2c9654e…
|
|
123
|
+
* deployed bytes fetched 163,525 — byte-for-byte equal to the artifact
|
|
124
|
+
* get_config network mainnet, not paused, deposit 0.1
|
|
125
|
+
* vault_id_for(test015.near) test015-near.usefloat.near
|
|
126
|
+
*
|
|
127
|
+
* The name is TOP-LEVEL on purpose (DECISIONS.md 2026-11-08): it is embedded
|
|
128
|
+
* in every user's vault address permanently, because a vault is a sub-account
|
|
129
|
+
* of its factory, and `float.near` was already taken.
|
|
111
130
|
*/
|
|
112
131
|
export const FACTORY_BY_NETWORK = {
|
|
113
132
|
testnet: "fac.floatv4.testnet",
|
|
114
|
-
mainnet:
|
|
133
|
+
mainnet: "usefloat.near",
|
|
115
134
|
};
|
|
116
135
|
/** yoctoNEAR -> a NEAR string, without floating point. */
|
|
117
136
|
export function yoctoToNear(y, dp = 5) {
|
|
@@ -139,8 +158,25 @@ export function buildCreatePlan(i) {
|
|
|
139
158
|
`64-character limit. Use a named account.`);
|
|
140
159
|
}
|
|
141
160
|
if (known && i.exists) {
|
|
142
|
-
|
|
143
|
-
|
|
161
|
+
// Three outcomes, deliberately not two. The old message asserted the
|
|
162
|
+
// first one for all three.
|
|
163
|
+
if (i.existingOwner && i.existingOwner === i.owner) {
|
|
164
|
+
throw new Error(`${i.vaultId} already exists and ${i.owner} owns it — confirmed by reading ` +
|
|
165
|
+
`its get_config. Connect to it with NEAR_VAULT_CONTRACT=${i.vaultId} ` +
|
|
166
|
+
`rather than creating another.`);
|
|
167
|
+
}
|
|
168
|
+
if (i.existingOwner) {
|
|
169
|
+
throw new Error(`${i.vaultId} already exists and is owned by ${i.existingOwner}, NOT by ` +
|
|
170
|
+
`${i.owner}. Do NOT point an agent at it.\n\n` +
|
|
171
|
+
`The factory derives a vault id by replacing dots with hyphens, and that ` +
|
|
172
|
+
`is not injective: "a.b.near" and "a-b.near" both derive "a-b-near", and ` +
|
|
173
|
+
`whoever created first holds the id. Create yours under a label instead, ` +
|
|
174
|
+
`which puts it at a different address.`);
|
|
175
|
+
}
|
|
176
|
+
throw new Error(`${i.vaultId} already exists, but its owner could NOT be read, so whether it ` +
|
|
177
|
+
`is yours is unknown — not assumed. It may not be a Float vault at all. ` +
|
|
178
|
+
`Check it before pointing anything at it:\n` +
|
|
179
|
+
` near view ${i.vaultId} get_config`);
|
|
144
180
|
}
|
|
145
181
|
// Two different floors, and the quote must clear BOTH.
|
|
146
182
|
//
|
package/dist/server.js
CHANGED
|
@@ -183,7 +183,57 @@ const limitShape = z
|
|
|
183
183
|
// startup: a wrong relative path would break the server outright instead of
|
|
184
184
|
// merely reporting a stale version. server.smoke.test.ts asserts the two agree,
|
|
185
185
|
// so a bump cannot land with only one of them moved.
|
|
186
|
-
|
|
186
|
+
/**
|
|
187
|
+
* WHAT THE AGENT READS BEFORE IT TOUCHES A TOOL.
|
|
188
|
+
*
|
|
189
|
+
* 23 tool descriptions tell a model what each CALL does and nothing about what
|
|
190
|
+
* the product IS, when to reach for it, or which of the three trust lanes a
|
|
191
|
+
* given task belongs in. The MCP `instructions` field is the one place that
|
|
192
|
+
* can say so, and it is injected into the client's system prompt — which is
|
|
193
|
+
* why servers that set it feel like the agent "already understands the
|
|
194
|
+
* project", and why ours did not.
|
|
195
|
+
*
|
|
196
|
+
* Kept to what a model cannot infer from a tool list: the workflow order, the
|
|
197
|
+
* fact that this server never signs policy, and the three refusals that are
|
|
198
|
+
* answers rather than errors. Everything that IS inferable from a schema is
|
|
199
|
+
* left to the schema.
|
|
200
|
+
*/
|
|
201
|
+
const INSTRUCTIONS = `Float gives an AI agent a spending budget that a smart contract enforces, on NEAR.
|
|
202
|
+
The agent's key can call exactly one method on exactly one contract: it cannot move tokens
|
|
203
|
+
directly, cannot raise its own limit, and cannot reach owner functions. Those are protocol-level
|
|
204
|
+
key restrictions, not policy checks.
|
|
205
|
+
|
|
206
|
+
Reach for these tools when the user wants to pay for something from an agent budget, set up or
|
|
207
|
+
change spending limits, see what an agent has spent, or wind a vault down.
|
|
208
|
+
|
|
209
|
+
THREE LANES, and the difference is the whole design:
|
|
210
|
+
READ vault_tree, vault_budget, vault_config, vault_roles, vault_request. No key, no risk.
|
|
211
|
+
PAY vault_pay. The ONLY tool that signs, and only with a restricted agent key.
|
|
212
|
+
PLAN vault_plan_*. These RETURN AN UNSIGNED PAYLOAD. This server never signs a policy
|
|
213
|
+
change; the human approves it in their own wallet. Do not describe a plan as done.
|
|
214
|
+
|
|
215
|
+
WORKFLOW
|
|
216
|
+
1. No vault yet? vault_create. It needs no key and no existing vault, and returns a URL the
|
|
217
|
+
user approves in their own wallet. Never ask anyone for a private key or seed phrase.
|
|
218
|
+
2. Look before acting: vault_tree shows the budget shape, vault_budget what is left today.
|
|
219
|
+
3. Pay with vault_pay. Check the result: a refusal is a recorded outcome, not an exception.
|
|
220
|
+
4. Change policy with a vault_plan_* tool, then hand the payload to the user to sign.
|
|
221
|
+
5. Winding down is a CLI job, not a tool: npx @floatonnear/float-exit. It returns the tokens,
|
|
222
|
+
the gas-key balances and the storage deposit. vault_teardown_report says what is recoverable.
|
|
223
|
+
|
|
224
|
+
REFUSALS ARE ANSWERS. Report the reason code, do not retry blindly:
|
|
225
|
+
DAILY_BUDGET this agent's own cap is spent.
|
|
226
|
+
PARENT_DAILY_BUDGET a SIBLING drained the shared pool; this agent's own cap may look free.
|
|
227
|
+
PER_TX_LIMIT one payment was too large; splitting it may still be refused by the cap.
|
|
228
|
+
NO_RECIPIENTS_SET the node pays nobody yet; that is an owner decision, via vault_plan_set_recipients.
|
|
229
|
+
|
|
230
|
+
WORTH KNOWING
|
|
231
|
+
Budgets are hierarchical: a payment charges every ancestor, so a vault-wide ceiling holds even
|
|
232
|
+
when a child's cap looks free.
|
|
233
|
+
An implicit account (a 64-character hex id) cannot own a vault at all. Tell the user to use a
|
|
234
|
+
named account rather than retrying.
|
|
235
|
+
Amounts are whole tokens in these tools (1.5 means 1.5 USDC), never base units.`;
|
|
236
|
+
const server = new McpServer({ name: "float", version: "0.6.0" }, { instructions: INSTRUCTIONS });
|
|
187
237
|
// ----------------------------------------------------------------- reads
|
|
188
238
|
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", {})));
|
|
189
239
|
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.", {
|
|
@@ -316,6 +366,25 @@ server.tool("vault_create", "For a user who has NO vault yet. Works out where th
|
|
|
316
366
|
`the vault will be initialised with "${config.network}".`);
|
|
317
367
|
}
|
|
318
368
|
const exists = owner && vaultId ? await accountExists(rpc, vaultId) : false;
|
|
369
|
+
/**
|
|
370
|
+
* ASK THE EXISTING VAULT WHO OWNS IT, rather than inferring it from the
|
|
371
|
+
* derivation that produced the id.
|
|
372
|
+
*
|
|
373
|
+
* `derive_prefix` replaces dots with hyphens and is NOT injective:
|
|
374
|
+
* `a.b.near` and `a-b.near` both derive `a-b-near`, and the factory ships
|
|
375
|
+
* a test asserting exactly that. So "an account exists at your derived id"
|
|
376
|
+
* does not mean "it is yours" — and the message here used to say it did,
|
|
377
|
+
* which would point the loser of a collision at the winner's vault.
|
|
378
|
+
*
|
|
379
|
+
* `null` on any failure, which the planner treats as a THIRD outcome.
|
|
380
|
+
* Collapsing the error into "not yours" would be as wrong as collapsing it
|
|
381
|
+
* into "yours": a vault that is merely unreadable right now is neither.
|
|
382
|
+
*/
|
|
383
|
+
const existingOwner = exists && vaultId
|
|
384
|
+
? await viewFunction(rpc, vaultId, "get_config", {})
|
|
385
|
+
.then((c) => c?.owner ?? null)
|
|
386
|
+
.catch(() => null)
|
|
387
|
+
: null;
|
|
319
388
|
// AN AGENT NEEDS A VAULT ID, and not for the transaction — the page
|
|
320
389
|
// derives that from the connected wallet. It is needed for the CREDENTIAL,
|
|
321
390
|
// which the adapter files under the account the key signs as. A virtual
|
|
@@ -345,6 +414,7 @@ server.tool("vault_create", "For a user who has NO vault yet. Works out where th
|
|
|
345
414
|
paused: config.paused,
|
|
346
415
|
appUrl: cfg.appUrl,
|
|
347
416
|
exists,
|
|
417
|
+
existingOwner,
|
|
348
418
|
agentPk: generated?.publicKey ?? null,
|
|
349
419
|
});
|
|
350
420
|
return json({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@floatonnear/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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",
|