@floatonnear/mcp 0.5.0 → 0.6.1
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/create.d.ts +18 -4
- package/dist/create.js +19 -5
- package/dist/rpc.d.ts +28 -0
- package/dist/rpc.js +32 -0
- package/dist/server.js +60 -3
- package/package.json +1 -1
package/dist/create.d.ts
CHANGED
|
@@ -109,10 +109,24 @@ export declare const GAS_RESERVATION_YOCTO: bigint;
|
|
|
109
109
|
/**
|
|
110
110
|
* Known factories.
|
|
111
111
|
*
|
|
112
|
-
* MAINNET
|
|
113
|
-
* default here does not fail safely: it sends a real user with real
|
|
114
|
-
* sign a 3.4 NEAR deposit into an account we do not control.
|
|
115
|
-
*
|
|
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.
|
|
116
130
|
*/
|
|
117
131
|
export declare const FACTORY_BY_NETWORK: Record<string, string | undefined>;
|
|
118
132
|
export interface CreatePlanInput {
|
package/dist/create.js
CHANGED
|
@@ -109,14 +109,28 @@ export const GAS_RESERVATION_YOCTO = CREATE_GAS_TGAS * 10n ** 12n * 10n ** 9n;
|
|
|
109
109
|
/**
|
|
110
110
|
* Known factories.
|
|
111
111
|
*
|
|
112
|
-
* MAINNET
|
|
113
|
-
* default here does not fail safely: it sends a real user with real
|
|
114
|
-
* sign a 3.4 NEAR deposit into an account we do not control.
|
|
115
|
-
*
|
|
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.
|
|
116
130
|
*/
|
|
117
131
|
export const FACTORY_BY_NETWORK = {
|
|
118
132
|
testnet: "fac.floatv4.testnet",
|
|
119
|
-
mainnet:
|
|
133
|
+
mainnet: "usefloat.near",
|
|
120
134
|
};
|
|
121
135
|
/** yoctoNEAR -> a NEAR string, without floating point. */
|
|
122
136
|
export function yoctoToNear(y, dp = 5) {
|
package/dist/rpc.d.ts
CHANGED
|
@@ -17,6 +17,34 @@
|
|
|
17
17
|
* https://docs.near.org/api/rpc/providers
|
|
18
18
|
*/
|
|
19
19
|
export declare const DEFAULT_RPC_URL = "https://test.rpc.fastnear.com";
|
|
20
|
+
/**
|
|
21
|
+
* The RPC to use for a given NETWORK.
|
|
22
|
+
*
|
|
23
|
+
* WHY THIS EXISTS. `vault_create` takes a `network` argument, resolved the
|
|
24
|
+
* right factory for it, and then asked `cfg.rpcUrl` — a single value
|
|
25
|
+
* defaulting to testnet. So a user asking for a mainnet vault got:
|
|
26
|
+
*
|
|
27
|
+
* account usefloat.near does not exist while viewing
|
|
28
|
+
*
|
|
29
|
+
* about an account that demonstrably does exist. The factory was right and
|
|
30
|
+
* the chain was wrong, which reads as "the factory is broken" rather than
|
|
31
|
+
* "you asked the wrong network". Shipped in 0.6.0 and found by driving the
|
|
32
|
+
* published package instead of reading its diff.
|
|
33
|
+
*/
|
|
34
|
+
export declare const RPC_BY_NETWORK: Record<string, string>;
|
|
35
|
+
/**
|
|
36
|
+
* Pick an RPC for `net`, honouring an operator override ONLY where it applies.
|
|
37
|
+
*
|
|
38
|
+
* An explicit NEAR_VAULT_RPC_URL is configured for ONE network — whichever the
|
|
39
|
+
* operator set NEAR_VAULT_NETWORK to. Applying it to the other network is the
|
|
40
|
+
* same bug in a new place: it would send a mainnet question to a testnet
|
|
41
|
+
* endpoint and report the answer as fact. So the override wins only when the
|
|
42
|
+
* requested network is the one it was configured for.
|
|
43
|
+
*/
|
|
44
|
+
export declare function rpcForNetwork(net: string, opts?: {
|
|
45
|
+
configuredNetwork?: string;
|
|
46
|
+
override?: string;
|
|
47
|
+
}): string;
|
|
20
48
|
export declare function encodeArgs(args: unknown): string;
|
|
21
49
|
interface RawViewResponse {
|
|
22
50
|
result?: number[];
|
package/dist/rpc.js
CHANGED
|
@@ -18,6 +18,38 @@ import { randomUUID } from "node:crypto";
|
|
|
18
18
|
* https://docs.near.org/api/rpc/providers
|
|
19
19
|
*/
|
|
20
20
|
export const DEFAULT_RPC_URL = "https://test.rpc.fastnear.com";
|
|
21
|
+
/**
|
|
22
|
+
* The RPC to use for a given NETWORK.
|
|
23
|
+
*
|
|
24
|
+
* WHY THIS EXISTS. `vault_create` takes a `network` argument, resolved the
|
|
25
|
+
* right factory for it, and then asked `cfg.rpcUrl` — a single value
|
|
26
|
+
* defaulting to testnet. So a user asking for a mainnet vault got:
|
|
27
|
+
*
|
|
28
|
+
* account usefloat.near does not exist while viewing
|
|
29
|
+
*
|
|
30
|
+
* about an account that demonstrably does exist. The factory was right and
|
|
31
|
+
* the chain was wrong, which reads as "the factory is broken" rather than
|
|
32
|
+
* "you asked the wrong network". Shipped in 0.6.0 and found by driving the
|
|
33
|
+
* published package instead of reading its diff.
|
|
34
|
+
*/
|
|
35
|
+
export const RPC_BY_NETWORK = {
|
|
36
|
+
testnet: "https://test.rpc.fastnear.com",
|
|
37
|
+
mainnet: "https://free.rpc.fastnear.com",
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Pick an RPC for `net`, honouring an operator override ONLY where it applies.
|
|
41
|
+
*
|
|
42
|
+
* An explicit NEAR_VAULT_RPC_URL is configured for ONE network — whichever the
|
|
43
|
+
* operator set NEAR_VAULT_NETWORK to. Applying it to the other network is the
|
|
44
|
+
* same bug in a new place: it would send a mainnet question to a testnet
|
|
45
|
+
* endpoint and report the answer as fact. So the override wins only when the
|
|
46
|
+
* requested network is the one it was configured for.
|
|
47
|
+
*/
|
|
48
|
+
export function rpcForNetwork(net, opts = {}) {
|
|
49
|
+
if (opts.override && net === opts.configuredNetwork)
|
|
50
|
+
return opts.override;
|
|
51
|
+
return RPC_BY_NETWORK[net] ?? DEFAULT_RPC_URL;
|
|
52
|
+
}
|
|
21
53
|
export function encodeArgs(args) {
|
|
22
54
|
// Explicit utf8: a memo is free text, goes on-chain, and a latin-1
|
|
23
55
|
// truncation here would corrupt it silently.
|
package/dist/server.js
CHANGED
|
@@ -49,7 +49,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
49
49
|
import { z } from "zod";
|
|
50
50
|
import { fromBaseUnits, toBaseUnits } from "./amounts.js";
|
|
51
51
|
import { DEFAULT_GAS_TGAS, planAddNode, planAddRole, planBindVirtualAgent, planUnbindVirtualAgent, planFreezeNode, planPurgeSettled, planSetCooldown, planSetNodeCaps, planSetPaused, planSetRecipients, toCliCommand, planSetApprovalThreshold, planApprovePayment, planRejectPayment, planWithdraw, } from "./payloads.js";
|
|
52
|
-
import { accountExists, DEFAULT_RPC_URL, newRequestId, requireAgentKey, viewAccessKeys, viewAccount, viewFunction, } from "./rpc.js";
|
|
52
|
+
import { accountExists, rpcForNetwork, DEFAULT_RPC_URL, newRequestId, requireAgentKey, viewAccessKeys, viewAccount, viewFunction, } from "./rpc.js";
|
|
53
53
|
import { AGENT_NODE_ID, generateAgentKey } from "./agent-key.js";
|
|
54
54
|
import { resolveKeySource } from "./key-file.js";
|
|
55
55
|
import { dirname } from "node:path";
|
|
@@ -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.1" }, { 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.", {
|
|
@@ -302,7 +352,14 @@ server.tool("vault_create", "For a user who has NO vault yet. Works out where th
|
|
|
302
352
|
`NEAR_VAULT_FACTORY. There is deliberately no built-in mainnet default: ` +
|
|
303
353
|
`guessing it wrong would send a real deposit somewhere unverified.`);
|
|
304
354
|
}
|
|
305
|
-
|
|
355
|
+
// THE RPC MUST FOLLOW THE NETWORK, not the process's default. 0.6.0
|
|
356
|
+
// resolved the right factory for `net` and then asked the testnet RPC
|
|
357
|
+
// about it, so a mainnet request answered "usefloat.near does not exist"
|
|
358
|
+
// about an account that does.
|
|
359
|
+
const rpc = rpcForNetwork(net, {
|
|
360
|
+
configuredNetwork: cfg.network,
|
|
361
|
+
override: process.env.NEAR_VAULT_RPC_URL,
|
|
362
|
+
});
|
|
306
363
|
// Only ask the factory to derive an id when there IS an owner. With none,
|
|
307
364
|
// the address is decided by whichever wallet signs, and inventing one here
|
|
308
365
|
// would print a plausible address next to a real deposit figure.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@floatonnear/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
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",
|