@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 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
@@ -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
- const writeCount = exposed.filter((t) => !isReadOnly(t.name)).length;
215
- console.error(`Bridge: ${exposed.length} tools -> ${opts.model}` +
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
- const [enrolledThisCycleRaw, enrolledNextCycleRaw, minimumAmountRaw, aprDataRaw, cycleOverviewRaw,] = await Promise.all([
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 false;
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.61.0",
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.61.0 # x-release-please-version
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
  ---