@aibtc/mcp-server 1.68.0 → 1.69.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/services/spend-limiter.d.ts +53 -0
- package/dist/services/spend-limiter.js +55 -21
- package/dist/tools/legion.tools.d.ts +4 -3
- package/dist/tools/legion.tools.js +9 -12
- package/dist/tools/ordinals.tools.js +28 -2
- package/dist/transactions/builder.js +14 -1
- package/package.json +1 -1
- package/skill/SKILL.md +1 -1
|
@@ -1,4 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default-on cumulative spending limit for the wallet.
|
|
3
|
+
*
|
|
4
|
+
* The MCP server lets an LLM move real funds with no human click-through, so a
|
|
5
|
+
* single poisoned instruction (or a malicious x402 endpoint) could otherwise
|
|
6
|
+
* drain the wallet. This module is the safety rail: every outbound spend path
|
|
7
|
+
* (native STX transfer, BTC L1 transfer, x402/L402 auto-payments, Lightning
|
|
8
|
+
* pays, and contract calls carrying a bounded caller-owned post condition)
|
|
9
|
+
* routes through it, and the limiter blocks once cumulative outflow would
|
|
10
|
+
* exceed a per-session OR per-day cap.
|
|
11
|
+
*
|
|
12
|
+
* Two independent ledgers are tracked — micro-STX (`ustx`) and satoshis
|
|
13
|
+
* (`sats`); there is no cross-asset USD normalization. Daily totals are
|
|
14
|
+
* persisted to ~/.aibtc/spend-state.json (keyed by wallet address + UTC day) so
|
|
15
|
+
* the cap survives process restarts; session totals live in memory and reset on
|
|
16
|
+
* wallet unlock/lock/switch.
|
|
17
|
+
*
|
|
18
|
+
* Exceeding the cap is the intended human-in-the-loop checkpoint: it blocks and
|
|
19
|
+
* surfaces the remaining budget + the env var to raise. An LLM cannot set env
|
|
20
|
+
* vars mid-session, so it cannot raise its own ceiling.
|
|
21
|
+
*
|
|
22
|
+
* Disable entirely with SPEND_LIMIT_ENABLED=false. Override caps with
|
|
23
|
+
* SPEND_LIMIT_DAILY_USTX / _SESSION_USTX / _DAILY_SATS / _SESSION_SATS.
|
|
24
|
+
*/
|
|
25
|
+
import type { PostCondition } from "@stacks/transactions";
|
|
1
26
|
export type SpendUnit = "ustx" | "sats";
|
|
27
|
+
/**
|
|
28
|
+
* Read spend ceilings out of a contract call's post conditions.
|
|
29
|
+
*
|
|
30
|
+
* A post condition already says "at most N of asset A leaves principal P", in
|
|
31
|
+
* native units, enforced by the chain — which is exactly the bound this rail
|
|
32
|
+
* wants, without taking a price-oracle dependency inside a safety rail.
|
|
33
|
+
*
|
|
34
|
+
* Only conditions that BOUND THE SPEND FROM ABOVE count: `eq`, `lt` and `lte`.
|
|
35
|
+
* A `gt`/`gte` condition is a floor, not a cap, so metering it would invent a
|
|
36
|
+
* ceiling that does not exist. Conditions on another principal are somebody
|
|
37
|
+
* else's funds, and an FT that is not sBTC has no ledger here — both are
|
|
38
|
+
* skipped rather than guessed at.
|
|
39
|
+
*
|
|
40
|
+
* `lt` is metered at its face value (one micro-unit above the true ceiling).
|
|
41
|
+
* Over-metering by one is the safe direction for a cap.
|
|
42
|
+
*/
|
|
43
|
+
export declare function boundedPostConditionSpends(postConditions: PostCondition[] | undefined, accountAddress: string): Array<{
|
|
44
|
+
unit: SpendUnit;
|
|
45
|
+
amount: bigint;
|
|
46
|
+
}>;
|
|
47
|
+
/**
|
|
48
|
+
* Collapse per-unit so one contract call is metered once per ledger, not once
|
|
49
|
+
* per post condition.
|
|
50
|
+
*/
|
|
51
|
+
export declare function totalBoundedSpends(postConditions: PostCondition[] | undefined, accountAddress: string): Array<{
|
|
52
|
+
unit: SpendUnit;
|
|
53
|
+
amount: bigint;
|
|
54
|
+
}>;
|
|
2
55
|
export declare class SpendLimitError extends Error {
|
|
3
56
|
readonly unit: SpendUnit;
|
|
4
57
|
readonly attempted: number;
|
|
@@ -1,29 +1,63 @@
|
|
|
1
|
+
import { promises as fs } from "fs";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { MAINNET_CONTRACTS, TESTNET_CONTRACTS } from "../config/contracts.js";
|
|
5
|
+
/** Asset ids this rail can meter. Any other FT has no ledger to bill. */
|
|
6
|
+
const SBTC_ASSETS = new Set([
|
|
7
|
+
`${MAINNET_CONTRACTS.SBTC_TOKEN}::sbtc-token`,
|
|
8
|
+
`${TESTNET_CONTRACTS.SBTC_TOKEN}::sbtc-token`,
|
|
9
|
+
]);
|
|
1
10
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* The MCP server lets an LLM move real funds with no human click-through, so a
|
|
5
|
-
* single poisoned instruction (or a malicious x402 endpoint) could otherwise
|
|
6
|
-
* drain the wallet. This module is the safety rail: every outbound spend path
|
|
7
|
-
* (native STX transfer, BTC L1 transfer, x402/L402 auto-payments, Lightning
|
|
8
|
-
* pays) routes through it, and the limiter blocks once cumulative outflow would
|
|
9
|
-
* exceed a per-session OR per-day cap.
|
|
11
|
+
* Read spend ceilings out of a contract call's post conditions.
|
|
10
12
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* the cap survives process restarts; session totals live in memory and reset on
|
|
15
|
-
* wallet unlock/lock/switch.
|
|
13
|
+
* A post condition already says "at most N of asset A leaves principal P", in
|
|
14
|
+
* native units, enforced by the chain — which is exactly the bound this rail
|
|
15
|
+
* wants, without taking a price-oracle dependency inside a safety rail.
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* Only conditions that BOUND THE SPEND FROM ABOVE count: `eq`, `lt` and `lte`.
|
|
18
|
+
* A `gt`/`gte` condition is a floor, not a cap, so metering it would invent a
|
|
19
|
+
* ceiling that does not exist. Conditions on another principal are somebody
|
|
20
|
+
* else's funds, and an FT that is not sBTC has no ledger here — both are
|
|
21
|
+
* skipped rather than guessed at.
|
|
20
22
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
+
* `lt` is metered at its face value (one micro-unit above the true ceiling).
|
|
24
|
+
* Over-metering by one is the safe direction for a cap.
|
|
23
25
|
*/
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
export function boundedPostConditionSpends(postConditions, accountAddress) {
|
|
27
|
+
const spends = [];
|
|
28
|
+
for (const condition of postConditions ?? []) {
|
|
29
|
+
const pc = condition;
|
|
30
|
+
if (pc.address !== accountAddress)
|
|
31
|
+
continue;
|
|
32
|
+
if (pc.condition !== "eq" && pc.condition !== "lt" && pc.condition !== "lte")
|
|
33
|
+
continue;
|
|
34
|
+
if (pc.amount === undefined)
|
|
35
|
+
continue;
|
|
36
|
+
const amount = BigInt(pc.amount);
|
|
37
|
+
if (amount < 0n)
|
|
38
|
+
continue;
|
|
39
|
+
if (pc.type === "stx-postcondition") {
|
|
40
|
+
spends.push({ unit: "ustx", amount });
|
|
41
|
+
}
|
|
42
|
+
else if (pc.type === "ft-postcondition" &&
|
|
43
|
+
pc.asset !== undefined &&
|
|
44
|
+
SBTC_ASSETS.has(pc.asset)) {
|
|
45
|
+
spends.push({ unit: "sats", amount });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return spends;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Collapse per-unit so one contract call is metered once per ledger, not once
|
|
52
|
+
* per post condition.
|
|
53
|
+
*/
|
|
54
|
+
export function totalBoundedSpends(postConditions, accountAddress) {
|
|
55
|
+
const totals = new Map();
|
|
56
|
+
for (const { unit, amount } of boundedPostConditionSpends(postConditions, accountAddress)) {
|
|
57
|
+
totals.set(unit, (totals.get(unit) ?? 0n) + amount);
|
|
58
|
+
}
|
|
59
|
+
return [...totals].map(([unit, amount]) => ({ unit, amount }));
|
|
60
|
+
}
|
|
27
61
|
const STORAGE_DIR = path.join(os.homedir(), ".aibtc");
|
|
28
62
|
const DEFAULT_STATE_FILE = path.join(STORAGE_DIR, "spend-state.json");
|
|
29
63
|
/**
|
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
*
|
|
8
8
|
* EVERYTHING HERE MOVES REAL VALUE. The legion is on Stacks mainnet holding
|
|
9
9
|
* real sBTC, and neither `contribute` nor `sponsor` has a withdrawal path, so
|
|
10
|
-
* both meter against the wallet's `sats` spending rail before signing
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* both meter against the wallet's `sats` spending rail before signing. That
|
|
11
|
+
* metering happens inside `callContract`, which reads the exact sBTC
|
|
12
|
+
* post-condition these calls already sign in DENY mode — so it must NOT be
|
|
13
|
+
* repeated here, or every contribute would bill the rail twice.
|
|
13
14
|
*
|
|
14
15
|
* These tools pin their own network: `getLegionAccount()` re-derives the
|
|
15
16
|
* unlocked wallet for the legion's chain, so a testnet-configured server still
|
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
*
|
|
8
8
|
* EVERYTHING HERE MOVES REAL VALUE. The legion is on Stacks mainnet holding
|
|
9
9
|
* real sBTC, and neither `contribute` nor `sponsor` has a withdrawal path, so
|
|
10
|
-
* both meter against the wallet's `sats` spending rail before signing
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* both meter against the wallet's `sats` spending rail before signing. That
|
|
11
|
+
* metering happens inside `callContract`, which reads the exact sBTC
|
|
12
|
+
* post-condition these calls already sign in DENY mode — so it must NOT be
|
|
13
|
+
* repeated here, or every contribute would bill the rail twice.
|
|
13
14
|
*
|
|
14
15
|
* These tools pin their own network: `getLegionAccount()` re-derives the
|
|
15
16
|
* unlocked wallet for the legion's chain, so a testnet-configured server still
|
|
@@ -27,7 +28,6 @@ import { DUST_THRESHOLD } from "../config/bitcoin-constants.js";
|
|
|
27
28
|
import { LEGION_ERAS, LEGION_ERRORS, LEGION_NETWORK, LEGION_SITE_URL, LIVE_ERA, MAX_DESCRIPTION_LENGTH, MAX_LINK_LENGTH, MAX_RATIONALE_LENGTH, MAX_SPONSOR_LINK_LENGTH, MAX_SPONSOR_MEMO_LENGTH, MAX_SPONSOR_NAME_LENGTH, MAX_TITLE_LENGTH, ORDINALS_CONTENT_BASE, ORDINALS_INSCRIPTION_BASE, STORY_STATUS, resolveEra, } from "../config/legion.js";
|
|
28
29
|
import { assertAscii, buildTimeline, contentUrlFromLink, explainError, getClockHeight, getLastProposalId, getLegionAccount, getLegionToken, getMembership, getParams, getPhase, getProposeStatus, getStory, getStoryMeta, getVoteRecord, inscriptionIdFromLink, normalizeOrdinalsLink, num, predictOutcome, proposeBlockers, readGov, readLegionContract, readTreasury, } from "../services/legion.service.js";
|
|
29
30
|
import { callContract } from "../transactions/builder.js";
|
|
30
|
-
import { getSpendLimiter } from "../services/spend-limiter.js";
|
|
31
31
|
import { getWalletManager } from "../services/wallet-manager.js";
|
|
32
32
|
import { MempoolApi, getMempoolTxUrl } from "../services/mempool-api.js";
|
|
33
33
|
import { buildCommitTransaction, buildRevealTransaction, deriveRevealScript, } from "../transactions/inscription-builder.js";
|
|
@@ -596,10 +596,9 @@ export function registerLegionTools(server) {
|
|
|
596
596
|
return createErrorResponse(new Error(`Your sBTC balance is ${balance} sats — not enough to contribute ${sats}. ` +
|
|
597
597
|
`Acquire sBTC with sbtc_deposit or styx_deposit first.`));
|
|
598
598
|
}
|
|
599
|
-
// Real, unrecoverable sBTC leaves the wallet here.
|
|
600
|
-
//
|
|
601
|
-
//
|
|
602
|
-
await getSpendLimiter().check("sats", BigInt(sats), account.address);
|
|
599
|
+
// Real, unrecoverable sBTC leaves the wallet here. The `sats` rail is
|
|
600
|
+
// metered by callContract off the willSendEq post-condition below —
|
|
601
|
+
// metering again here would double-bill the cap.
|
|
603
602
|
const quotedWeight = num(await readGov("quote-weight", [uintCV(sats)]));
|
|
604
603
|
const result = await callContract(account, {
|
|
605
604
|
contractAddress: GOV_ADDRESS,
|
|
@@ -613,7 +612,6 @@ export function registerLegionTools(server) {
|
|
|
613
612
|
.ft(token.contract, token.assetName),
|
|
614
613
|
],
|
|
615
614
|
});
|
|
616
|
-
await getSpendLimiter().record("sats", BigInt(sats), account.address);
|
|
617
615
|
const weightAfter = num(weightBefore) + quotedWeight;
|
|
618
616
|
const wasMember = num(weightBefore) >= params.minWeightToAct;
|
|
619
617
|
const willBeMember = weightAfter >= params.minWeightToAct;
|
|
@@ -700,8 +698,8 @@ export function registerLegionTools(server) {
|
|
|
700
698
|
if (balance < sats) {
|
|
701
699
|
return createErrorResponse(new Error(`Your sBTC balance is ${balance} sats — not enough to sponsor ${sats}.`));
|
|
702
700
|
}
|
|
703
|
-
// Same rail as contribute: real sBTC, no refund,
|
|
704
|
-
|
|
701
|
+
// Same rail as contribute: real sBTC, no refund, metered by callContract
|
|
702
|
+
// off the post-condition below.
|
|
705
703
|
const result = await callContract(account, {
|
|
706
704
|
contractAddress: TREASURY_ADDRESS,
|
|
707
705
|
contractName: TREASURY_NAME,
|
|
@@ -719,7 +717,6 @@ export function registerLegionTools(server) {
|
|
|
719
717
|
.ft(token.contract, token.assetName),
|
|
720
718
|
],
|
|
721
719
|
});
|
|
722
|
-
await getSpendLimiter().record("sats", BigInt(sats), account.address);
|
|
723
720
|
return createJsonResponse({
|
|
724
721
|
success: true,
|
|
725
722
|
txid: result.txid,
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Uses micro-ordinals library to parse and create inscriptions.
|
|
12
12
|
*/
|
|
13
|
+
import { createHash } from "node:crypto";
|
|
13
14
|
import { z } from "zod";
|
|
14
15
|
import { NETWORK } from "../config/networks.js";
|
|
15
16
|
import { P2WPKH_INPUT_VBYTES, P2WPKH_OUTPUT_VBYTES, P2TR_OUTPUT_VBYTES, TX_OVERHEAD_VBYTES, DUST_THRESHOLD, P2TR_INPUT_BASE_VBYTES, WITNESS_OVERHEAD_VBYTES, } from "../config/bitcoin-constants.js";
|
|
@@ -223,8 +224,10 @@ export function registerOrdinalsTools(server) {
|
|
|
223
224
|
feeRate: actualFeeRate,
|
|
224
225
|
contentType,
|
|
225
226
|
contentSize: body.length,
|
|
227
|
+
contentSha256: createHash("sha256").update(body).digest("hex"),
|
|
226
228
|
nextStep: "After commit confirms, call inscribe_reveal with the same contentType, contentBase64, " +
|
|
227
|
-
"plus commitTxid and revealAmount from this response."
|
|
229
|
+
"plus commitTxid and revealAmount from this response. Pass contentSha256 back to inscribe_reveal " +
|
|
230
|
+
"to have the server abort before the reveal fee is spent if the decoded body differs by even one byte.",
|
|
228
231
|
});
|
|
229
232
|
}
|
|
230
233
|
catch (error) {
|
|
@@ -252,12 +255,20 @@ export function registerOrdinalsTools(server) {
|
|
|
252
255
|
contentBase64: z
|
|
253
256
|
.string()
|
|
254
257
|
.describe("Content as base64-encoded string (must match the commit step)"),
|
|
258
|
+
contentSha256: z
|
|
259
|
+
.string()
|
|
260
|
+
.regex(/^[0-9a-f]{64}$/i)
|
|
261
|
+
.optional()
|
|
262
|
+
.describe("Optional: sha256 hex digest of the decoded body from the inscribe commit response. " +
|
|
263
|
+
"If provided, the server computes sha256 over the locally-decoded body and aborts before " +
|
|
264
|
+
"broadcasting the reveal if they differ — prevents burning the reveal fee on a witness-hash " +
|
|
265
|
+
"mismatch when the base64 param drifts between the inscribe and inscribe_reveal calls."),
|
|
255
266
|
feeRate: z
|
|
256
267
|
.union([z.enum(["fast", "medium", "slow"]), z.number().positive()])
|
|
257
268
|
.optional()
|
|
258
269
|
.describe("Fee rate for reveal tx (default: medium)"),
|
|
259
270
|
},
|
|
260
|
-
}, async ({ commitTxid, revealAmount, contentType, contentBase64, feeRate }) => {
|
|
271
|
+
}, async ({ commitTxid, revealAmount, contentType, contentBase64, contentSha256, feeRate }) => {
|
|
261
272
|
try {
|
|
262
273
|
// Check wallet session
|
|
263
274
|
const walletManager = getWalletManager();
|
|
@@ -277,6 +288,21 @@ export function registerOrdinalsTools(server) {
|
|
|
277
288
|
const mempoolApi = new MempoolApi(NETWORK);
|
|
278
289
|
// Reconstruct the inscription and reveal script
|
|
279
290
|
const body = Buffer.from(contentBase64, "base64");
|
|
291
|
+
// If the caller supplied the sha256 digest from the commit response, verify the
|
|
292
|
+
// decoded body matches BEFORE we spend the reveal fee. Silent byte drift between
|
|
293
|
+
// the two calls would otherwise commit a tapscript hash that Bitcoin Core rejects
|
|
294
|
+
// as witness-hash-mismatch, burning the reveal-address sats permanently.
|
|
295
|
+
if (contentSha256) {
|
|
296
|
+
const localSha256 = createHash("sha256").update(body).digest("hex");
|
|
297
|
+
if (localSha256.toLowerCase() !== contentSha256.toLowerCase()) {
|
|
298
|
+
return createErrorResponse(new Error(`contentSha256 mismatch: the decoded body's sha256 (${localSha256}) does not match ` +
|
|
299
|
+
`the digest from the inscribe commit response (${contentSha256}). This means the ` +
|
|
300
|
+
`base64 payload passed to inscribe_reveal is not the exact bytes committed on-chain. ` +
|
|
301
|
+
`Aborting BEFORE broadcasting the reveal so the reveal-address sats are not burned. ` +
|
|
302
|
+
`Re-send the exact base64 from the successful inscribe call (or fix the transport / ` +
|
|
303
|
+
`serialization layer that mutated it).`));
|
|
304
|
+
}
|
|
305
|
+
}
|
|
280
306
|
const inscription = {
|
|
281
307
|
contentType,
|
|
282
308
|
body,
|
|
@@ -3,7 +3,7 @@ import { hexToBytes } from "@stacks/common";
|
|
|
3
3
|
import { getStacksNetwork, getApiBaseUrl } from "../config/networks.js";
|
|
4
4
|
import { getHiroApi } from "../services/hiro-api.js";
|
|
5
5
|
import { resolveDefaultFee } from "../utils/fee.js";
|
|
6
|
-
import { getSpendLimiter } from "../services/spend-limiter.js";
|
|
6
|
+
import { getSpendLimiter, totalBoundedSpends } from "../services/spend-limiter.js";
|
|
7
7
|
import { getTrackedNonce, recordNonceUsed, resetTrackedNonce, reconcileWithChain, } from "../services/nonce-tracker.js";
|
|
8
8
|
// ---------------------------------------------------------------------------
|
|
9
9
|
// Pending nonce tracking (unified via SharedNonceTracker, issue #413)
|
|
@@ -117,6 +117,16 @@ export async function transferStx(account, recipient, amount, memo, fee) {
|
|
|
117
117
|
* Call a smart contract function
|
|
118
118
|
*/
|
|
119
119
|
export async function callContract(account, options) {
|
|
120
|
+
// Safety rail. Swaps, lending and pot entries all funnel through here, and
|
|
121
|
+
// their amounts are denominated in whatever token the call moves — there is no
|
|
122
|
+
// ledger to bill 1,000 ALEX against. The post conditions are the way in: they
|
|
123
|
+
// already bound, in native units, how much of each asset may leave this
|
|
124
|
+
// address. Meter the ones this rail has a ledger for and ignore the rest.
|
|
125
|
+
// Deliberately before makeContractCall, so an over-cap call never signs.
|
|
126
|
+
const meteredSpends = totalBoundedSpends(options.postConditions, account.address);
|
|
127
|
+
for (const spend of meteredSpends) {
|
|
128
|
+
await getSpendLimiter().check(spend.unit, spend.amount, account.address);
|
|
129
|
+
}
|
|
120
130
|
const networkName = getStacksNetwork(account.network);
|
|
121
131
|
const nonce = await getNextNonce(account.address, account.network);
|
|
122
132
|
// Always resolve a clamped fee — prevents @stacks/transactions from over-estimating.
|
|
@@ -141,6 +151,9 @@ export async function callContract(account, options) {
|
|
|
141
151
|
throw new Error(`Broadcast failed: ${broadcastResponse.error} - ${broadcastResponse.reason}`);
|
|
142
152
|
}
|
|
143
153
|
advancePendingNonce(account.address, nonce, broadcastResponse.txid);
|
|
154
|
+
for (const spend of meteredSpends) {
|
|
155
|
+
await getSpendLimiter().record(spend.unit, spend.amount, account.address);
|
|
156
|
+
}
|
|
144
157
|
return {
|
|
145
158
|
txid: broadcastResponse.txid,
|
|
146
159
|
rawTx: transaction.serialize(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aibtc/mcp-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.69.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.69.0 # x-release-please-version
|
|
8
8
|
npm: "@aibtc/mcp-server"
|
|
9
9
|
github: https://github.com/aibtcdev/aibtc-mcp-server
|
|
10
10
|
---
|