@aibtc/mcp-server 1.61.0 → 1.62.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/README.md +2 -0
- package/dist/bridge/index.js +26 -6
- package/dist/tools/dual-stacking.tools.js +25 -3
- package/dist/tools/lightning.tools.js +30 -0
- package/package.json +1 -1
- package/skill/SKILL.md +1 -1
package/README.md
CHANGED
|
@@ -99,6 +99,8 @@ npx @aibtc/mcp-server@latest bridge --read-only --list-tools
|
|
|
99
99
|
npx @aibtc/mcp-server@latest bridge --read-only --allow transfer_stx "send 1 STX to SP3..."
|
|
100
100
|
```
|
|
101
101
|
|
|
102
|
+
Before any agent loop runs (and on `--list-tools`), the bridge prints a compact **safety receipt** to stderr — network, read-only mode, exposed/write/blocked tool counts, the session spend cap, and the number of known x402 endpoints — so the configured execution boundaries are visible up front. It reports boundaries only; it never claims any value moved.
|
|
103
|
+
|
|
102
104
|
The allowlist is re-enforced at execution time, so a model can never call a tool outside the exposed set. Any MCP-capable agent framework (`@openrouter/agent`, OpenAI Agents SDK, Claude Agent SDK) can also point at this server directly — the bridge is for driving it through OpenRouter's raw API without adopting a framework.
|
|
103
105
|
|
|
104
106
|
### Testnet Mode
|
package/dist/bridge/index.js
CHANGED
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
*/
|
|
30
30
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
31
31
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
32
|
+
import { ALL_ENDPOINTS } from "../endpoints/registry.js";
|
|
32
33
|
const OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions";
|
|
33
34
|
const DEFAULT_MODEL = "anthropic/claude-3.5-haiku";
|
|
34
35
|
/**
|
|
@@ -136,6 +137,28 @@ function parseArgs(argv) {
|
|
|
136
137
|
opts.prompt = promptWords.join(" ").trim();
|
|
137
138
|
return opts;
|
|
138
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* Compact safety receipt printed before any agent loop runs (#583). Makes the
|
|
142
|
+
* configured execution boundaries visible up front so a user can trust the
|
|
143
|
+
* bridge without digging through flags/config. It reports ONLY configured
|
|
144
|
+
* boundaries — it never claims that any value moved. Emitted on stderr so it
|
|
145
|
+
* can't pollute stdout (the final assistant reply / --list-tools piping).
|
|
146
|
+
*/
|
|
147
|
+
function printSafetyReceipt(opts, exposed, totalTools) {
|
|
148
|
+
const writeToolCount = exposed.filter((t) => !isReadOnly(t.name)).length;
|
|
149
|
+
const lines = [
|
|
150
|
+
"aibtc-mcp bridge — safety receipt",
|
|
151
|
+
` network=${opts.network}`,
|
|
152
|
+
` read_only=${opts.readOnly}`,
|
|
153
|
+
` exposed_tools=${exposed.length}/${totalTools}`,
|
|
154
|
+
` write_tool_count=${writeToolCount}`,
|
|
155
|
+
` blocked_tool_count=${opts.block.size}`,
|
|
156
|
+
` session_spend_cap=${opts.maxSpendUstx ?? "default"} uSTX / ${opts.maxSpendSats ?? "default"} sats`,
|
|
157
|
+
` x402_endpoints_known=${ALL_ENDPOINTS.length}`,
|
|
158
|
+
" (reports configured execution boundaries only; no value has moved)",
|
|
159
|
+
];
|
|
160
|
+
console.error(lines.join("\n"));
|
|
161
|
+
}
|
|
139
162
|
/** Apply read-only + allow + block to the raw MCP tool list. */
|
|
140
163
|
function selectTools(tools, opts) {
|
|
141
164
|
return tools.filter((t) => {
|
|
@@ -193,6 +216,7 @@ export async function runBridge(argv) {
|
|
|
193
216
|
const { tools: allTools } = await client.listTools();
|
|
194
217
|
const exposed = selectTools(allTools, opts);
|
|
195
218
|
if (opts.listTools) {
|
|
219
|
+
printSafetyReceipt(opts, exposed, allTools.length);
|
|
196
220
|
console.log(`Exposed ${exposed.length}/${allTools.length} tools` +
|
|
197
221
|
(opts.readOnly ? " (read-only)" : "") + ":");
|
|
198
222
|
for (const t of exposed)
|
|
@@ -211,12 +235,8 @@ export async function runBridge(argv) {
|
|
|
211
235
|
await client.close();
|
|
212
236
|
process.exit(1);
|
|
213
237
|
}
|
|
214
|
-
|
|
215
|
-
console.error(`
|
|
216
|
-
(opts.readOnly ? " [read-only]" : ` [${writeCount} can write/spend]`) +
|
|
217
|
-
(opts.maxSpendUstx || opts.maxSpendSats
|
|
218
|
-
? ` (cap ${opts.maxSpendUstx ?? "-"} uSTX / ${opts.maxSpendSats ?? "-"} sats)`
|
|
219
|
-
: ""));
|
|
238
|
+
printSafetyReceipt(opts, exposed, allTools.length);
|
|
239
|
+
console.error(` model=${opts.model}, max_turns=${opts.maxTurns}`);
|
|
220
240
|
const openaiTools = exposed.map(toOpenAITool);
|
|
221
241
|
const messages = [
|
|
222
242
|
{
|
|
@@ -64,13 +64,33 @@ Note: Dual Stacking is only available on mainnet.`,
|
|
|
64
64
|
});
|
|
65
65
|
}
|
|
66
66
|
const resolvedAddress = address || (await getWalletAddress());
|
|
67
|
-
|
|
67
|
+
// Use allSettled, not all: `is-enrolled-this-cycle` fails contract-side
|
|
68
|
+
// on mainnet with RuntimeCheck(AtBlockUnavailable) (it reads historical
|
|
69
|
+
// block state Hiro has pruned). With Promise.all a single failing read
|
|
70
|
+
// sinks the whole response, hiding the other 4 useful fields. Recover
|
|
71
|
+
// per-call and surface a `warnings` array instead. (#554)
|
|
72
|
+
const warnings = [];
|
|
73
|
+
const settled = await Promise.allSettled([
|
|
68
74
|
callDualStackingReadOnly("is-enrolled-this-cycle", [principalCV(resolvedAddress)]),
|
|
69
75
|
callDualStackingReadOnly("is-enrolled-in-next-cycle", [principalCV(resolvedAddress)]),
|
|
70
76
|
callDualStackingReadOnly("get-minimum-enrollment-amount", []),
|
|
71
77
|
callDualStackingReadOnly("get-apr-data", []),
|
|
72
78
|
callDualStackingReadOnly("current-overview-data", []),
|
|
73
79
|
]);
|
|
80
|
+
const readNames = [
|
|
81
|
+
"is-enrolled-this-cycle",
|
|
82
|
+
"is-enrolled-in-next-cycle",
|
|
83
|
+
"get-minimum-enrollment-amount",
|
|
84
|
+
"get-apr-data",
|
|
85
|
+
"current-overview-data",
|
|
86
|
+
];
|
|
87
|
+
const values = settled.map((r, i) => {
|
|
88
|
+
if (r.status === "fulfilled")
|
|
89
|
+
return r.value;
|
|
90
|
+
warnings.push(`${readNames[i]}: ${r.reason instanceof Error ? r.reason.message : String(r.reason)}`);
|
|
91
|
+
return null;
|
|
92
|
+
});
|
|
93
|
+
const [enrolledThisCycleRaw, enrolledNextCycleRaw, minimumAmountRaw, aprDataRaw, cycleOverviewRaw,] = values;
|
|
74
94
|
// Parse APR data — returns {min-apr: uint, max-apr: uint} divided by 1_000_000 for %
|
|
75
95
|
let apr = {
|
|
76
96
|
minApr: 0,
|
|
@@ -107,10 +127,11 @@ Note: Dual Stacking is only available on mainnet.`,
|
|
|
107
127
|
const raw = minimumAmountRaw;
|
|
108
128
|
minimumEnrollmentSats = raw.value !== undefined ? Number(raw.value) : 0;
|
|
109
129
|
}
|
|
110
|
-
// Parse boolean enrollment flags
|
|
130
|
+
// Parse boolean enrollment flags. A failed read stays null (unknown)
|
|
131
|
+
// rather than collapsing to a misleading `false`.
|
|
111
132
|
const parseBoolean = (raw) => {
|
|
112
133
|
if (raw === null || raw === undefined)
|
|
113
|
-
return
|
|
134
|
+
return null;
|
|
114
135
|
if (typeof raw === "boolean")
|
|
115
136
|
return raw;
|
|
116
137
|
const obj = raw;
|
|
@@ -126,6 +147,7 @@ Note: Dual Stacking is only available on mainnet.`,
|
|
|
126
147
|
minimumEnrollmentSats,
|
|
127
148
|
apr,
|
|
128
149
|
cycleOverview,
|
|
150
|
+
...(warnings.length > 0 && { warnings }),
|
|
129
151
|
});
|
|
130
152
|
}
|
|
131
153
|
catch (error) {
|
|
@@ -7,9 +7,11 @@
|
|
|
7
7
|
* same LightningProvider singleton to auto-pay invoice challenges on 402s.
|
|
8
8
|
*/
|
|
9
9
|
import { z } from "zod";
|
|
10
|
+
import { decode as decodeBolt11 } from "light-bolt11-decoder";
|
|
10
11
|
import { createJsonResponse, createErrorResponse } from "../utils/index.js";
|
|
11
12
|
import { getLightningManager } from "../services/lightning-manager.js";
|
|
12
13
|
import { getWalletManager } from "../services/wallet-manager.js";
|
|
14
|
+
import { getSpendLimiter } from "../services/spend-limiter.js";
|
|
13
15
|
import { MempoolApi, getMempoolTxUrl, } from "../services/mempool-api.js";
|
|
14
16
|
import { buildAndSignBtcTransaction } from "../transactions/bitcoin-builder.js";
|
|
15
17
|
import { UnisatIndexer } from "../services/unisat-indexer.js";
|
|
@@ -284,11 +286,39 @@ export function registerLightningTools(server) {
|
|
|
284
286
|
if (!provider) {
|
|
285
287
|
throw new Error("Lightning wallet is locked. Use lightning_unlock first.");
|
|
286
288
|
}
|
|
289
|
+
// Meter the invoice face value against the cumulative sats spend limit
|
|
290
|
+
// (#572). maxFeeSats only bounds the routing fee, not the payable amount,
|
|
291
|
+
// so without this a single invoice can pay out an unbounded amount.
|
|
292
|
+
// Decode the invoice to read the amount (millisats), mirroring the L402
|
|
293
|
+
// auto-pay path in x402.service.ts.
|
|
294
|
+
let decoded;
|
|
295
|
+
try {
|
|
296
|
+
decoded = decodeBolt11(bolt11);
|
|
297
|
+
}
|
|
298
|
+
catch (decodeErr) {
|
|
299
|
+
throw new Error(`BOLT-11 invoice could not be decoded: ${decodeErr instanceof Error ? decodeErr.message : String(decodeErr)}`);
|
|
300
|
+
}
|
|
301
|
+
const amountSection = decoded.sections.find((s) => s.name === "amount");
|
|
302
|
+
const amountMsat = amountSection?.value ? BigInt(amountSection.value) : 0n;
|
|
303
|
+
const amountSats = Number(amountMsat / 1000n);
|
|
304
|
+
if (amountSats === 0) {
|
|
305
|
+
throw new Error("Invoice has no amount; refusing to pay amountless invoices for safety (an unmeterable spend defeats the spending cap).");
|
|
306
|
+
}
|
|
307
|
+
// Key the sats ledger by the active Stacks address so BTC L1 / sBTC /
|
|
308
|
+
// L402 / Lightning spends share one budget. When the main wallet is
|
|
309
|
+
// locked (Lightning has its own session), fall back to a dedicated
|
|
310
|
+
// Lightning bucket so the pay is always metered. Note: a locked-STX
|
|
311
|
+
// user therefore gets a separate Lightning ledger from their STX-address
|
|
312
|
+
// ledger — see SECURITY.md.
|
|
313
|
+
const ledgerKey = getWalletManager().getActiveAccount()?.address ?? "__lightning__";
|
|
314
|
+
await getSpendLimiter().check("sats", BigInt(amountSats), ledgerKey);
|
|
287
315
|
const result = await provider.payInvoice(bolt11, maxFeeSats);
|
|
316
|
+
await getSpendLimiter().record("sats", BigInt(amountSats), ledgerKey);
|
|
288
317
|
return createJsonResponse({
|
|
289
318
|
success: true,
|
|
290
319
|
preimage: result.preimage,
|
|
291
320
|
feesPaidSats: result.feesPaid,
|
|
321
|
+
amountSats,
|
|
292
322
|
});
|
|
293
323
|
}
|
|
294
324
|
catch (error) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aibtc/mcp-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.62.0",
|
|
4
4
|
"description": "Bitcoin-native MCP server for AI agents: BTC/STX wallets, DeFi yield, sBTC peg, NFTs, and x402 payments.",
|
|
5
5
|
"mcpName": "io.github.aibtcdev/mcp-server",
|
|
6
6
|
"type": "module",
|
package/skill/SKILL.md
CHANGED
|
@@ -4,7 +4,7 @@ description: Bitcoin L1 wallet for agents - check balances, send BTC, manage UTX
|
|
|
4
4
|
license: MIT
|
|
5
5
|
metadata:
|
|
6
6
|
author: aibtcdev
|
|
7
|
-
version: 1.
|
|
7
|
+
version: 1.62.0 # x-release-please-version
|
|
8
8
|
npm: "@aibtc/mcp-server"
|
|
9
9
|
github: https://github.com/aibtcdev/aibtc-mcp-server
|
|
10
10
|
---
|