@walletbrake/mcp 0.1.0 → 0.1.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/README.md +1 -3
- package/dist/cli.js +10 -1
- package/dist/tools.d.ts +6 -2
- package/dist/tools.js +29 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,8 +5,6 @@ An [MCP](https://modelcontextprotocol.io) server that lets any AI agent pay from
|
|
|
5
5
|
The server holds the agent's key. The model only sees tools. Every payment is checked against the vault's limits and trusted addresses, and anything else goes to the owner to approve. The vault contract enforces the result on chain, so a prompt injection can't empty it.
|
|
6
6
|
|
|
7
7
|
> Test networks only for now (Base Sepolia, BNB testnet, Ethereum Sepolia). Mainnet comes after the security audit.
|
|
8
|
-
>
|
|
9
|
-
> **Coming soon to npm.** Want early access? Email support@walletbrake.com.
|
|
10
8
|
|
|
11
9
|
## Set up
|
|
12
10
|
|
|
@@ -19,7 +17,7 @@ The server holds the agent's key. The model only sees tools. Every payment is ch
|
|
|
19
17
|
chmod 600 ~/.walletbrake/agent.key
|
|
20
18
|
```
|
|
21
19
|
|
|
22
|
-
3. **Add the server to your AI app**, with your vault's address from the app.
|
|
20
|
+
3. **Add the server to your AI app**, with your vault's address from the app. Your app runs it with `npx -y @walletbrake/mcp` (Node 20 or newer), which fetches it from npm.
|
|
23
21
|
|
|
24
22
|
### Claude Desktop
|
|
25
23
|
|
package/dist/cli.js
CHANGED
|
@@ -35,7 +35,10 @@ async function main() {
|
|
|
35
35
|
if (!url)
|
|
36
36
|
throw new Error("set WALLETBRAKE_URL to your vault's address in the WalletBrake app");
|
|
37
37
|
const privateKey = readKey(env.WALLETBRAKE_AGENT_KEY, env.WALLETBRAKE_AGENT_KEY_FILE);
|
|
38
|
-
|
|
38
|
+
// looked up on first use, so the server starts even while WalletBrake is loading the network
|
|
39
|
+
const rpcUrl = env.WALLETBRAKE_RPC_URL?.trim() || (() => publicNode(url));
|
|
40
|
+
if (typeof rpcUrl !== "string" && !/^https?:\/\/[^/]+\/c\/\d+\/v\//.test(url))
|
|
41
|
+
throw new Error("set WALLETBRAKE_RPC_URL: a node for the vault's network");
|
|
39
42
|
const waitSecs = Number(env.WALLETBRAKE_WAIT_FOR_OWNER_SECONDS ?? "0");
|
|
40
43
|
if (!Number.isFinite(waitSecs) || waitSecs < 0 || waitSecs > 900)
|
|
41
44
|
throw new Error("WALLETBRAKE_WAIT_FOR_OWNER_SECONDS: between 0 and 900");
|
|
@@ -79,6 +82,12 @@ async function publicNode(url) {
|
|
|
79
82
|
if (!res.ok)
|
|
80
83
|
throw new Error(`couldn't read ${m[1]}/api/config (${res.status}); set WALLETBRAKE_RPC_URL instead`);
|
|
81
84
|
const cfg = (await res.json());
|
|
85
|
+
const down = cfg.down?.find((d) => d.chainId === Number(m[2]));
|
|
86
|
+
if (down) {
|
|
87
|
+
throw new Error(down.starting
|
|
88
|
+
? `WalletBrake is loading ${down.name} after an update. Try again in a few minutes.`
|
|
89
|
+
: `WalletBrake can't reach ${down.name} right now. Try again in a few minutes.`);
|
|
90
|
+
}
|
|
82
91
|
const rpc = cfg.chains?.find((c) => c.chainId === Number(m[2]))?.rpc?.[0];
|
|
83
92
|
if (!rpc)
|
|
84
93
|
throw new Error(`WalletBrake lists no public node for chain ${m[2]}; set WALLETBRAKE_RPC_URL`);
|
package/dist/tools.d.ts
CHANGED
|
@@ -17,8 +17,12 @@ export interface WalletBrakeToolsOptions {
|
|
|
17
17
|
url: string;
|
|
18
18
|
/** The vault's spending-wallet key. */
|
|
19
19
|
privateKey: Hex;
|
|
20
|
-
/**
|
|
21
|
-
|
|
20
|
+
/**
|
|
21
|
+
* A node for the vault's network, or how to find one. A function is
|
|
22
|
+
* called on first use (and again after a failure), so the server can
|
|
23
|
+
* start while WalletBrake is still loading the network.
|
|
24
|
+
*/
|
|
25
|
+
rpcUrl: string | (() => Promise<string>);
|
|
22
26
|
/**
|
|
23
27
|
* How long walletbrake_pay waits for the owner when a payment needs their
|
|
24
28
|
* approval. Default 0: it answers at once, and the agent asks again later.
|
package/dist/tools.js
CHANGED
|
@@ -25,19 +25,28 @@ export function walletBrakeTools(o) {
|
|
|
25
25
|
const x402Max = o.x402Max === undefined ? undefined : parseUnits(o.x402Max, 6);
|
|
26
26
|
if (x402Max !== undefined && x402Max <= 0n)
|
|
27
27
|
throw new RangeError("x402Max must be more than 0");
|
|
28
|
-
const
|
|
29
|
-
let
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (getAddress(wb.account.address) !== info.operator) {
|
|
33
|
-
throw new WalletBrakeError(`this key (${getAddress(wb.account.address)}) isn't the vault's spending wallet (${info.operator}). ` +
|
|
34
|
-
"Give the MCP server the spending wallet's key, or set this wallet as the spending wallet in the vault's Safety tab.");
|
|
35
|
-
}
|
|
36
|
-
return info;
|
|
37
|
-
})).catch((err) => {
|
|
38
|
-
checked = undefined;
|
|
28
|
+
const rpc = typeof o.rpcUrl === "string" ? ((u) => async () => u)(o.rpcUrl) : o.rpcUrl;
|
|
29
|
+
let made;
|
|
30
|
+
const client = () => (made ??= rpc().then((rpcUrl) => new WalletBrake({ url: o.url, privateKey: o.privateKey, rpcUrl, fetch: o.fetch, requestTimeoutMs: o.requestTimeoutMs }))).catch((err) => {
|
|
31
|
+
made = undefined;
|
|
39
32
|
throw err;
|
|
40
33
|
});
|
|
34
|
+
let checked;
|
|
35
|
+
/** The client, once the vault is known: and a clear message if the key isn't its spending wallet. */
|
|
36
|
+
const vault = async () => {
|
|
37
|
+
const wb = await client();
|
|
38
|
+
await (checked ??= wb.info().then((info) => {
|
|
39
|
+
if (getAddress(wb.account.address) !== info.operator) {
|
|
40
|
+
throw new WalletBrakeError(`this key (${getAddress(wb.account.address)}) isn't the vault's spending wallet (${info.operator}). ` +
|
|
41
|
+
"Give the MCP server the spending wallet's key, or set this wallet as the spending wallet in the vault's Safety tab.");
|
|
42
|
+
}
|
|
43
|
+
return info;
|
|
44
|
+
})).catch((err) => {
|
|
45
|
+
checked = undefined;
|
|
46
|
+
throw err;
|
|
47
|
+
});
|
|
48
|
+
return wb;
|
|
49
|
+
};
|
|
41
50
|
// One payment at a time: each sends a transaction from the same key.
|
|
42
51
|
let queue = Promise.resolve();
|
|
43
52
|
const oneAtATime = (f) => {
|
|
@@ -45,7 +54,7 @@ export function walletBrakeTools(o) {
|
|
|
45
54
|
queue = p.catch(() => { });
|
|
46
55
|
return p;
|
|
47
56
|
};
|
|
48
|
-
const coin = async (token) => {
|
|
57
|
+
const coin = async (wb, token) => {
|
|
49
58
|
const t = (await wb.info()).tokens.find((x) => x.address === getAddress(token));
|
|
50
59
|
return t ? { symbol: t.symbol, decimals: t.decimals } : { symbol: `of token ${token} (base units)`, decimals: 0 };
|
|
51
60
|
};
|
|
@@ -56,7 +65,7 @@ export function walletBrakeTools(o) {
|
|
|
56
65
|
description: "Shows the WalletBrake vault you pay from: the coins it holds, how much you can still spend this hour and today, the largest payment, and whether it is frozen.",
|
|
57
66
|
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
58
67
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
59
|
-
call: () => run(async () => describeVault(await vault()
|
|
68
|
+
call: () => run(async () => describeVault(await (await vault()).info())),
|
|
60
69
|
},
|
|
61
70
|
{
|
|
62
71
|
name: "walletbrake_pay",
|
|
@@ -84,7 +93,7 @@ export function walletBrakeTools(o) {
|
|
|
84
93
|
const to = str(a, "to", true), amount = str(a, "amount", true);
|
|
85
94
|
if (!/^\d+(\.\d+)?$/.test(amount))
|
|
86
95
|
return fail(`Not paid: "${amount}" isn't an amount. Give coin units, like "10" or "0.5".`);
|
|
87
|
-
await vault();
|
|
96
|
+
const wb = await vault();
|
|
88
97
|
const p = { to, amount, token: str(a, "token"), note: str(a, "note"), task: str(a, "task") };
|
|
89
98
|
const until = Date.now() + wait;
|
|
90
99
|
let approval;
|
|
@@ -106,7 +115,7 @@ export function walletBrakeTools(o) {
|
|
|
106
115
|
}
|
|
107
116
|
}
|
|
108
117
|
const res = await wb.send(approval);
|
|
109
|
-
const c = await coin(approval.token);
|
|
118
|
+
const c = await coin(wb, approval.token);
|
|
110
119
|
const what = `${formatUnits(approval.amount, c.decimals)} ${c.symbol} to ${approval.to}`;
|
|
111
120
|
if (res.queued) {
|
|
112
121
|
return {
|
|
@@ -131,7 +140,7 @@ export function walletBrakeTools(o) {
|
|
|
131
140
|
const id = str(a, "queueId", true).trim();
|
|
132
141
|
if (!/^\d+$/.test(id))
|
|
133
142
|
return fail(`Not sent: ${id} isn't a queue id.`);
|
|
134
|
-
await vault();
|
|
143
|
+
const wb = await vault();
|
|
135
144
|
const hash = await wb.executeQueued(BigInt(id));
|
|
136
145
|
return { text: `Sent the held payment ${id}. Transaction: ${hash}` };
|
|
137
146
|
})),
|
|
@@ -159,7 +168,7 @@ export function walletBrakeTools(o) {
|
|
|
159
168
|
},
|
|
160
169
|
annotations: { destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
161
170
|
call: (a) => run(async () => {
|
|
162
|
-
await vault();
|
|
171
|
+
const wb = await vault();
|
|
163
172
|
const payOnly = a.payOnly;
|
|
164
173
|
if (payOnly !== undefined && (!Array.isArray(payOnly) || payOnly.some((x) => typeof x !== "string"))) {
|
|
165
174
|
return fail("Not opened: payOnly must be a list of addresses.");
|
|
@@ -174,7 +183,7 @@ export function walletBrakeTools(o) {
|
|
|
174
183
|
payOnly: payOnly,
|
|
175
184
|
minutes: minutes,
|
|
176
185
|
});
|
|
177
|
-
const c = await coin(t.token);
|
|
186
|
+
const c = await coin(wb, t.token);
|
|
178
187
|
return {
|
|
179
188
|
text: `Task ${t.id} is open until ${t.expires.toISOString()}: "${t.description}", up to ${formatUnits(t.budget, c.decimals)} ${c.symbol}. Pass task "${t.id}" when paying for it.`,
|
|
180
189
|
};
|
|
@@ -210,7 +219,7 @@ export function walletBrakeTools(o) {
|
|
|
210
219
|
const method = (str(a, "method") ?? "GET").toUpperCase();
|
|
211
220
|
if (method !== "GET" && method !== "POST")
|
|
212
221
|
return fail("Not fetched: method must be GET or POST.");
|
|
213
|
-
await vault();
|
|
222
|
+
const wb = await vault();
|
|
214
223
|
let paid;
|
|
215
224
|
const pay = wb.x402Fetch({ max: x402Max, task: str(a, "task"), onPayment: (p) => (paid = p) });
|
|
216
225
|
const init = { method, redirect: "error" };
|
|
@@ -225,7 +234,7 @@ export function walletBrakeTools(o) {
|
|
|
225
234
|
: `(${type || "no content type"}, ${(await res.arrayBuffer()).byteLength} bytes, not shown)`;
|
|
226
235
|
const head = [`HTTP ${res.status}${type ? ` (${type.split(";")[0]})` : ""}.`];
|
|
227
236
|
if (paid) {
|
|
228
|
-
const c = await coin(paid.token);
|
|
237
|
+
const c = await coin(wb, paid.token);
|
|
229
238
|
head.push(`Paid ${formatUnits(paid.amount, c.decimals)} ${c.symbol} to ${paid.to} (transaction ${paid.txHash}).`);
|
|
230
239
|
}
|
|
231
240
|
const body = text.length > BODY_LIMIT ? text.slice(0, BODY_LIMIT) + `\n… (${text.length - BODY_LIMIT} more characters not shown)` : text;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@walletbrake/mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "An MCP server that lets any AI agent pay from a WalletBrake vault without ever seeing a key: spending limits, trusted addresses and owner approval, enforced on chain.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "WalletBrake <support@walletbrake.com>",
|