@floatonnear/mcp 0.1.1 → 0.3.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/payloads.d.ts +43 -0
- package/dist/payloads.js +84 -0
- package/dist/server.js +16 -2
- package/package.json +42 -42
package/dist/payloads.d.ts
CHANGED
|
@@ -103,3 +103,46 @@ export declare const OWNER_PLANNERS: {
|
|
|
103
103
|
};
|
|
104
104
|
/** A ready-to-run command, so approving does not require trusting this server. */
|
|
105
105
|
export declare function toCliCommand(p: OwnerPayload, network?: string): string;
|
|
106
|
+
/**
|
|
107
|
+
* Require the owner to approve payments above `amount` at this node.
|
|
108
|
+
*
|
|
109
|
+
* A threshold is a gate INSIDE the caps: it can only make the vault more
|
|
110
|
+
* conservative, never less, so there is no value of it that moves more money
|
|
111
|
+
* than the budget tree already allowed. `null` removes it.
|
|
112
|
+
*
|
|
113
|
+
* ---- `amount` IS HUMAN UNITS, and it used to be base units.
|
|
114
|
+
*
|
|
115
|
+
* Breaking change, 2026-10-31, made the same day the tool was published and
|
|
116
|
+
* before anyone used it. The parameter was `amountBase` and took raw base
|
|
117
|
+
* units, which made it the ONLY tool in this module on that convention:
|
|
118
|
+
*
|
|
119
|
+
* vault_pay.amount human "12.50"
|
|
120
|
+
* planSetNodeCaps dailyCap/perTxCap human (the zod schema says so)
|
|
121
|
+
* planSetApprovalThreshold BASE <- the odd one out
|
|
122
|
+
*
|
|
123
|
+
* And it is the one that configures a safety gate, so it FAILED OPEN. An owner
|
|
124
|
+
* thinking in human units who passed a figure at or above the per-tx cap in
|
|
125
|
+
* base units got a threshold ABOVE every cap in the tree: the gate never fires,
|
|
126
|
+
* nothing is ever held, and the transaction succeeds — so the owner believes the
|
|
127
|
+
* control is on. Measured against the live mainnet vault (per-tx cap 510000
|
|
128
|
+
* base = 0.51 USDC): "1000000" meaning "a million" yields a 1.0 USDC threshold,
|
|
129
|
+
* above every cap.
|
|
130
|
+
*
|
|
131
|
+
* The other direction fails closed and is merely annoying ("1" meaning 1 USDC
|
|
132
|
+
* became 0.000001, so everything got held), which is why only one direction is
|
|
133
|
+
* dangerous and why this is worth a breaking change rather than a doc fix.
|
|
134
|
+
*
|
|
135
|
+
* `toBaseUnits` throws rather than truncating excess precision — correct here:
|
|
136
|
+
* silently rounding 0.1234567 would set a threshold the owner did not choose.
|
|
137
|
+
*/
|
|
138
|
+
export declare function planSetApprovalThreshold(vault: string, nodeId: number, amount: string | null, decimals: number): OwnerPayload;
|
|
139
|
+
/**
|
|
140
|
+
* Execute a held payment.
|
|
141
|
+
*
|
|
142
|
+
* Every limit is re-checked at approval time, so this can still be refused --
|
|
143
|
+
* by a cap tightened while it waited, or by an Intents quote that expired. That
|
|
144
|
+
* is the honest outcome and the warning says so.
|
|
145
|
+
*/
|
|
146
|
+
export declare function planApprovePayment(vault: string, requestId: string): OwnerPayload;
|
|
147
|
+
/** Decline a held payment. The ticket is discarded and nothing moves. */
|
|
148
|
+
export declare function planRejectPayment(vault: string, requestId: string): OwnerPayload;
|
package/dist/payloads.js
CHANGED
|
@@ -290,3 +290,87 @@ export function toCliCommand(p, network = "testnet") {
|
|
|
290
290
|
`attached-deposit '${p.depositYocto} yoctoNEAR' ` +
|
|
291
291
|
`sign-as <YOUR-OWNER-ACCOUNT> network-config ${network} sign-with-keychain send`);
|
|
292
292
|
}
|
|
293
|
+
/**
|
|
294
|
+
* Require the owner to approve payments above `amount` at this node.
|
|
295
|
+
*
|
|
296
|
+
* A threshold is a gate INSIDE the caps: it can only make the vault more
|
|
297
|
+
* conservative, never less, so there is no value of it that moves more money
|
|
298
|
+
* than the budget tree already allowed. `null` removes it.
|
|
299
|
+
*
|
|
300
|
+
* ---- `amount` IS HUMAN UNITS, and it used to be base units.
|
|
301
|
+
*
|
|
302
|
+
* Breaking change, 2026-10-31, made the same day the tool was published and
|
|
303
|
+
* before anyone used it. The parameter was `amountBase` and took raw base
|
|
304
|
+
* units, which made it the ONLY tool in this module on that convention:
|
|
305
|
+
*
|
|
306
|
+
* vault_pay.amount human "12.50"
|
|
307
|
+
* planSetNodeCaps dailyCap/perTxCap human (the zod schema says so)
|
|
308
|
+
* planSetApprovalThreshold BASE <- the odd one out
|
|
309
|
+
*
|
|
310
|
+
* And it is the one that configures a safety gate, so it FAILED OPEN. An owner
|
|
311
|
+
* thinking in human units who passed a figure at or above the per-tx cap in
|
|
312
|
+
* base units got a threshold ABOVE every cap in the tree: the gate never fires,
|
|
313
|
+
* nothing is ever held, and the transaction succeeds — so the owner believes the
|
|
314
|
+
* control is on. Measured against the live mainnet vault (per-tx cap 510000
|
|
315
|
+
* base = 0.51 USDC): "1000000" meaning "a million" yields a 1.0 USDC threshold,
|
|
316
|
+
* above every cap.
|
|
317
|
+
*
|
|
318
|
+
* The other direction fails closed and is merely annoying ("1" meaning 1 USDC
|
|
319
|
+
* became 0.000001, so everything got held), which is why only one direction is
|
|
320
|
+
* dangerous and why this is worth a breaking change rather than a doc fix.
|
|
321
|
+
*
|
|
322
|
+
* `toBaseUnits` throws rather than truncating excess precision — correct here:
|
|
323
|
+
* silently rounding 0.1234567 would set a threshold the owner did not choose.
|
|
324
|
+
*/
|
|
325
|
+
export function planSetApprovalThreshold(vault, nodeId, amount, decimals) {
|
|
326
|
+
const base = amount === null ? null : toBaseUnits(amount, decimals).toString();
|
|
327
|
+
return {
|
|
328
|
+
receiverId: vault,
|
|
329
|
+
methodName: "set_approval_threshold",
|
|
330
|
+
args: { node_id: nodeId, amount: base },
|
|
331
|
+
gasTgas: DEFAULT_GAS_TGAS,
|
|
332
|
+
depositYocto: ONE_YOCTO,
|
|
333
|
+
// Human units in the summary too. The old line read "Payments above
|
|
334
|
+
// 1000000 (base units)", which is unreadable in precisely the moment the
|
|
335
|
+
// owner is checking the plan before signing it.
|
|
336
|
+
summary: amount
|
|
337
|
+
? `Payments above ${amount} from node ${nodeId} will need your approval`
|
|
338
|
+
: `Remove the approval threshold on node ${nodeId} — payments up to the caps run unattended`,
|
|
339
|
+
warnings: amount
|
|
340
|
+
? []
|
|
341
|
+
: ["After this, nothing at this node waits for a human: only the caps apply."],
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Execute a held payment.
|
|
346
|
+
*
|
|
347
|
+
* Every limit is re-checked at approval time, so this can still be refused --
|
|
348
|
+
* by a cap tightened while it waited, or by an Intents quote that expired. That
|
|
349
|
+
* is the honest outcome and the warning says so.
|
|
350
|
+
*/
|
|
351
|
+
export function planApprovePayment(vault, requestId) {
|
|
352
|
+
return {
|
|
353
|
+
receiverId: vault,
|
|
354
|
+
methodName: "approve_payment",
|
|
355
|
+
args: { request_id: requestId },
|
|
356
|
+
gasTgas: DEFAULT_GAS_TGAS,
|
|
357
|
+
depositYocto: ONE_YOCTO,
|
|
358
|
+
summary: `Approve and send held payment ${requestId}`,
|
|
359
|
+
warnings: [
|
|
360
|
+
"This MOVES MONEY. Check the amount and payee with vault_pending_approval first.",
|
|
361
|
+
"Limits are re-checked now, so it can still be refused — a cap lowered since the request, or a cross-chain quote that has expired.",
|
|
362
|
+
],
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
/** Decline a held payment. The ticket is discarded and nothing moves. */
|
|
366
|
+
export function planRejectPayment(vault, requestId) {
|
|
367
|
+
return {
|
|
368
|
+
receiverId: vault,
|
|
369
|
+
methodName: "reject_payment",
|
|
370
|
+
args: { request_id: requestId },
|
|
371
|
+
gasTgas: DEFAULT_GAS_TGAS,
|
|
372
|
+
depositYocto: ONE_YOCTO,
|
|
373
|
+
summary: `Reject held payment ${requestId}`,
|
|
374
|
+
warnings: ["The agent must request again if it still needs the money."],
|
|
375
|
+
};
|
|
376
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -22,7 +22,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
22
22
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
23
23
|
import { z } from "zod";
|
|
24
24
|
import { fromBaseUnits, toBaseUnits } from "./amounts.js";
|
|
25
|
-
import { DEFAULT_GAS_TGAS, planAddNode, planCreateAgentAccount, planFreezeNode, planPurgeSettled, planSetCooldown, planSetNodeCaps, planSetPaused, planSetRecipients, toCliCommand, } from "./payloads.js";
|
|
25
|
+
import { DEFAULT_GAS_TGAS, planAddNode, planCreateAgentAccount, planFreezeNode, planPurgeSettled, planSetCooldown, planSetNodeCaps, planSetPaused, planSetRecipients, toCliCommand, planSetApprovalThreshold, planApprovePayment, planRejectPayment, } from "./payloads.js";
|
|
26
26
|
import { DEFAULT_RPC_URL, newRequestId, requireAgentKey, viewFunction } from "./rpc.js";
|
|
27
27
|
import { buildRoute, sendRequestPayment } from "./sign.js";
|
|
28
28
|
import { getQuote, signBinding, toBase64 } from "./quote.js";
|
|
@@ -87,7 +87,7 @@ const limitShape = z
|
|
|
87
87
|
// startup: a wrong relative path would break the server outright instead of
|
|
88
88
|
// merely reporting a stale version. server.smoke.test.ts asserts the two agree,
|
|
89
89
|
// so a bump cannot land with only one of them moved.
|
|
90
|
-
const server = new McpServer({ name: "float", version: "0.
|
|
90
|
+
const server = new McpServer({ name: "float", version: "0.3.0" });
|
|
91
91
|
// ----------------------------------------------------------------- reads
|
|
92
92
|
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", {})));
|
|
93
93
|
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.", {
|
|
@@ -208,6 +208,20 @@ server.tool("vault_plan_create_agent", "Build an UNSIGNED payload to create an a
|
|
|
208
208
|
server.tool("vault_plan_freeze", "Build an UNSIGNED payload to freeze or unfreeze a node. Freezing blocks the node AND its whole subtree. Not signed by this server.", { nodeId: z.number().int().min(0), frozen: z.boolean() }, async ({ nodeId, frozen }) => renderPlan(planFreezeNode(requireVault(), nodeId, frozen)));
|
|
209
209
|
server.tool("vault_plan_set_recipients", "Build an UNSIGNED payload to set a node's payee allowlist. This REPLACES the inherited list rather than adding to it; pass null to inherit. Not signed by this server.", { nodeId: z.number().int().min(0), recipients: z.array(z.string()).nullable() }, async ({ nodeId, recipients }) => renderPlan(planSetRecipients(requireVault(), nodeId, recipients)));
|
|
210
210
|
server.tool("vault_plan_set_cooldown", "Build an UNSIGNED payload to set the minimum seconds between a node's payments. Not signed by this server.", { nodeId: z.number().int().min(0), cooldownSec: z.number().int().min(0) }, async ({ nodeId, cooldownSec }) => renderPlan(planSetCooldown(requireVault(), nodeId, cooldownSec)));
|
|
211
|
+
server.tool("vault_pending_approval", "Look up a payment that is HELD awaiting the owner's approval. Returns null if there is no ticket for that id — already approved, already rejected, or never held.", { requestId: z.string().min(1) }, async ({ requestId }) => json((await viewFunction(cfg.rpcUrl, requireVault(), "get_pending_approval", {
|
|
212
|
+
request_id: requestId,
|
|
213
|
+
})) ?? null));
|
|
214
|
+
server.tool("vault_plan_set_approval_threshold", 'Build an UNSIGNED payload requiring the owner\'s approval for payments above an amount at this node. A threshold sits INSIDE the caps and can only make the vault more conservative. `amount` is HUMAN units, the same convention as vault_pay — "0.25" is a quarter of a USDC. Pass amount null to remove the threshold. Not signed by this server.', {
|
|
215
|
+
nodeId: z.number().int().min(0),
|
|
216
|
+
// HUMAN units, matching vault_pay and vault_plan_set_caps. This took BASE
|
|
217
|
+
// units until 2026-10-31 and was the only tool here that did — on the one
|
|
218
|
+
// control that configures a safety gate, where the mistake fails OPEN. See
|
|
219
|
+
// planSetApprovalThreshold in payloads.ts for the measurement.
|
|
220
|
+
amount: z.string().min(1).nullable().describe('human units, e.g. "0.25" — NOT base units'),
|
|
221
|
+
decimals: z.number().int().min(0).max(38).default(6),
|
|
222
|
+
}, async ({ nodeId, amount, decimals }) => renderPlan(planSetApprovalThreshold(requireVault(), nodeId, amount, decimals)));
|
|
223
|
+
server.tool("vault_plan_approve_payment", "Build an UNSIGNED payload to APPROVE a held payment, which sends it. Every limit is re-checked at approval, so it can still be refused. Not signed by this server.", { requestId: z.string().min(1) }, async ({ requestId }) => renderPlan(planApprovePayment(requireVault(), requestId)));
|
|
224
|
+
server.tool("vault_plan_reject_payment", "Build an UNSIGNED payload to REJECT a held payment. Nothing moves and the ticket is discarded. Not signed by this server.", { requestId: z.string().min(1) }, async ({ requestId }) => renderPlan(planRejectPayment(requireVault(), requestId)));
|
|
211
225
|
server.tool("vault_plan_pause", "Build an UNSIGNED payload to pause or resume the whole vault. Pausing halts every agent payment. Not signed by this server.", { paused: z.boolean() }, async ({ paused }) => renderPlan(planSetPaused(requireVault(), paused)));
|
|
212
226
|
server.tool("vault_plan_purge", "Build an UNSIGNED payload to delete settled payment records older than a UTC day. Spend totals are NOT refunded, but a deleted request id becomes reusable. Not signed by this server.", {
|
|
213
227
|
beforeDay: z.number().int().min(0),
|
package/package.json
CHANGED
|
@@ -1,42 +1,42 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@floatonnear/mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"type": "module",
|
|
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
|
-
"license": "MIT",
|
|
7
|
-
"bin": {
|
|
8
|
-
"mcp": "dist/server.js",
|
|
9
|
-
"float-mcp": "dist/server.js"
|
|
10
|
-
},
|
|
11
|
-
"files": [
|
|
12
|
-
"dist/"
|
|
13
|
-
],
|
|
14
|
-
"scripts": {
|
|
15
|
-
"build": "tsc -p tsconfig.json",
|
|
16
|
-
"prepack": "npm run build",
|
|
17
|
-
"test": "vitest run",
|
|
18
|
-
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
19
|
-
},
|
|
20
|
-
"dependencies": {
|
|
21
|
-
"@modelcontextprotocol/sdk": "^1.32.2",
|
|
22
|
-
"@noble/curves": "^1.9.7",
|
|
23
|
-
"@noble/hashes": "^1.8.0",
|
|
24
|
-
"near-api-js": "^7.4.0",
|
|
25
|
-
"zod": "^4.6.7"
|
|
26
|
-
},
|
|
27
|
-
"devDependencies": {
|
|
28
|
-
"@types/node": "^24.0.0",
|
|
29
|
-
"typescript": "^5.9.0",
|
|
30
|
-
"vitest": "^5.0.5"
|
|
31
|
-
},
|
|
32
|
-
"exports": {
|
|
33
|
-
".": "./dist/index.js",
|
|
34
|
-
"./amounts": "./dist/amounts.js",
|
|
35
|
-
"./sign": "./dist/sign.js",
|
|
36
|
-
"./payloads": "./dist/payloads.js",
|
|
37
|
-
"./quote": {
|
|
38
|
-
"types": "./dist/quote.d.ts",
|
|
39
|
-
"import": "./dist/quote.js"
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@floatonnear/mcp",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"type": "module",
|
|
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
|
+
"license": "MIT",
|
|
7
|
+
"bin": {
|
|
8
|
+
"mcp": "dist/server.js",
|
|
9
|
+
"float-mcp": "dist/server.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist/"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc -p tsconfig.json",
|
|
16
|
+
"prepack": "npm run build",
|
|
17
|
+
"test": "vitest run",
|
|
18
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@modelcontextprotocol/sdk": "^1.32.2",
|
|
22
|
+
"@noble/curves": "^1.9.7",
|
|
23
|
+
"@noble/hashes": "^1.8.0",
|
|
24
|
+
"near-api-js": "^7.4.0",
|
|
25
|
+
"zod": "^4.6.7"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/node": "^24.0.0",
|
|
29
|
+
"typescript": "^5.9.0",
|
|
30
|
+
"vitest": "^5.0.5"
|
|
31
|
+
},
|
|
32
|
+
"exports": {
|
|
33
|
+
".": "./dist/index.js",
|
|
34
|
+
"./amounts": "./dist/amounts.js",
|
|
35
|
+
"./sign": "./dist/sign.js",
|
|
36
|
+
"./payloads": "./dist/payloads.js",
|
|
37
|
+
"./quote": {
|
|
38
|
+
"types": "./dist/quote.d.ts",
|
|
39
|
+
"import": "./dist/quote.js"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|