@aibtc/mcp-server 1.62.0 → 1.63.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.
@@ -1,192 +1,26 @@
1
1
  import { z } from "zod";
2
- import { makeContractCall, uintCV, principalCV, noneCV, someCV, bufferCV, } from "@stacks/transactions";
3
- import { decodePaymentRequired, decodePaymentResponse, encodePaymentPayload, generatePaymentId, buildPaymentIdentifierExtension, X402_HEADERS } from "../utils/x402-protocol.js";
4
- import { getAccount, NETWORK } from "../services/x402.service.js";
5
- import { getSbtcService } from "../services/sbtc.service.js";
6
- import { getStacksNetwork, getExplorerTxUrl } from "../config/networks.js";
7
- import { getContracts, parseContractId } from "../config/contracts.js";
8
- import { createFungiblePostCondition } from "../transactions/post-conditions.js";
9
- import { createJsonResponse, createErrorResponse } from "../utils/index.js";
10
- import { InsufficientBalanceError } from "../utils/errors.js";
11
- import { formatSbtc } from "../utils/formatting.js";
12
- import { getHiroApi } from "../services/hiro-api.js";
13
- import { extractPaymentIdFromPaymentSignature, extractTxidFromPaymentSignature, pollTransactionConfirmation, } from "../utils/x402-recovery.js";
14
- import { classifyCanonicalPaymentStatus, extractCanonicalPaymentHints, formatCanonicalPaymentStatus, normalizeCallerFacingStatus, resolveCanonicalPaymentStatus, } from "../utils/x402-payment-state.js";
15
- import { emitCanonicalPaymentDecisionLogs, emitCanonicalPaymentPollLogs, emitPaymentLog, } from "../utils/x402-payment-logging.js";
16
- const INBOX_BASE = "https://aibtc.com/api/inbox";
17
- // Deprecation switch for the sponsored relay send. The full implementation is
18
- // retained below but gated off behind this flag — the relay-sponsored path was
19
- // unstable, so all inbox sends go through send_inbox_message_direct. Flip to
20
- // `false` to re-enable. Typed as `boolean` (not a literal) so TypeScript keeps
21
- // type-checking the retained handler body instead of treating it as dead code.
22
- const SPONSORED_INBOX_DISABLED = true;
2
+ import { createJsonResponse } from "../utils/index.js";
23
3
  // ============================================================================
24
- // Nonce Manager (delegated to SharedNonceTracker — issue #413)
4
+ // send_inbox_message — DEPRECATED redirect
25
5
  //
26
- // All nonce tracking is now handled by the shared tracker, which persists to
27
- // ~/.aibtc/nonce-state.json and is shared across MCP tools and CLI skills.
28
- // ============================================================================
29
- import { getTrackedNonce, recordNonceUsed, reconcileWithChain, } from "../services/nonce-tracker.js";
30
- /**
31
- * Compute the next safe nonce for a sender address.
32
- *
33
- * Always fetches chain state and reconciles with the shared tracker,
34
- * consistent with the builder path (both take max of local vs chain).
35
- */
36
- async function getNextNonce(address) {
37
- // 1. Check shared tracker (fast, no network)
38
- const localNext = await getTrackedNonce(address);
39
- // 2. Fetch chain state for reconciliation
40
- const hiroApi = getHiroApi(NETWORK);
41
- const accountInfo = await hiroApi.getAccountInfo(address);
42
- const confirmedNonce = accountInfo.nonce;
43
- // The mempool scan is what makes the nonce safe against pending txs —
44
- // silently skipping it on failure risks a nonce collision, so let it fail.
45
- let highestMempoolNonce = -1;
46
- const mempool = await hiroApi.getMempoolTransactions({
47
- sender_address: address,
48
- limit: 50,
49
- });
50
- for (const tx of mempool.results) {
51
- if (tx.nonce > highestMempoolNonce) {
52
- highestMempoolNonce = tx.nonce;
53
- }
54
- }
55
- const chainNext = Math.max(confirmedNonce, highestMempoolNonce + 1);
56
- // 3. Reconcile tracker with chain state
57
- await reconcileWithChain(address, chainNext);
58
- // 4. Return max(chain, local) — same logic as builder path
59
- return Math.max(chainNext, localNext ?? 0);
60
- }
61
- /**
62
- * Record that we used a nonce for an address so subsequent calls use a higher value.
63
- */
64
- async function advanceNonceCache(address, usedNonce, txid = "") {
65
- await recordNonceUsed(address, usedNonce, txid);
66
- }
67
- // ============================================================================
68
- // Transaction Builder
69
- // ============================================================================
70
- /**
71
- * Build a sponsored sBTC transfer transaction (signed, not broadcast).
72
- * The inbox API handles settlement via the x402 relay.
73
- * Explicit nonce avoids ConflictingNonceInMempool; optional memo (max 34 bytes)
74
- * can be used for on-chain labeling.
75
- */
76
- async function buildSponsoredSbtcTransfer(senderKey, senderAddress, recipient, amount, nonce, memo) {
77
- const contracts = getContracts(NETWORK);
78
- const { address: contractAddress, name: contractName } = parseContractId(contracts.SBTC_TOKEN);
79
- const networkName = getStacksNetwork(NETWORK);
80
- const postCondition = createFungiblePostCondition(senderAddress, contracts.SBTC_TOKEN, "sbtc-token", "eq", amount);
81
- // Encode memo as (optional (buff 34)): some(buff) if provided, none() otherwise.
82
- const memoArg = memo
83
- ? someCV(bufferCV(Buffer.from(memo).slice(0, 34)))
84
- : noneCV();
85
- const transaction = await makeContractCall({
86
- contractAddress,
87
- contractName,
88
- functionName: "transfer",
89
- functionArgs: [
90
- uintCV(amount),
91
- principalCV(senderAddress),
92
- principalCV(recipient),
93
- memoArg,
94
- ],
95
- senderKey,
96
- network: networkName,
97
- postConditions: [postCondition],
98
- sponsored: true,
99
- fee: 0n,
100
- nonce,
101
- });
102
- // serialize() returns Hex string (no 0x prefix) in @stacks/transactions v7+
103
- return "0x" + transaction.serialize();
104
- }
105
- /** Default delay when no retryAfter hint is provided. */
106
- const DEFAULT_RETRY_DELAY_MS = 2_000;
107
- /** Cap retryAfter to avoid blocking too long (seconds). */
108
- const MAX_RETRY_AFTER_CAP_S = 60;
109
- /**
110
- * Classify a response as retryable and extract retry timing.
111
- */
112
- function classifyLegacyRetryableError(status, body) {
113
- const NOT_RETRYABLE = { retryable: false, delayMs: 0, relaySideConflict: false };
114
- // Duplicate-message 409 from the inbox API must NOT be retried —
115
- // the message was already delivered and retrying would re-pay.
116
- if (status === 409) {
117
- const bodyStr = typeof body === "string" ? body : JSON.stringify(body);
118
- if (/already exists|duplicate/i.test(bodyStr)) {
119
- return NOT_RETRYABLE;
120
- }
121
- }
122
- if (typeof body === "object" && body !== null) {
123
- const b = body;
124
- // Parse relay's retryAfter hint (seconds → ms), capped.
125
- const rawRetryAfter = typeof b["retryAfter"] === "number" ? b["retryAfter"] : 0;
126
- const retryAfterMs = rawRetryAfter > 0
127
- ? Math.min(rawRetryAfter, MAX_RETRY_AFTER_CAP_S) * 1000
128
- : DEFAULT_RETRY_DELAY_MS;
129
- // Relay returns retryable: true for SETTLEMENT_BROADCAST_FAILED (issue #157)
130
- if (b["retryable"] === true) {
131
- return { retryable: true, delayMs: retryAfterMs, relaySideConflict: false };
132
- }
133
- // Relay returns HTTP 409 with code: "NONCE_CONFLICT" — relay-side conflict,
134
- // safe to resubmit the same transaction for dedup.
135
- if (status === 409 && b["code"] === "NONCE_CONFLICT") {
136
- return { retryable: true, delayMs: retryAfterMs, relaySideConflict: true };
137
- }
138
- }
139
- // Sender-side nonce conflict from the Stacks node (not relay) — needs fresh tx.
140
- if (typeof body === "string") {
141
- if (body.includes("ConflictingNonceInMempool") || body.includes("BadNonce")) {
142
- return { retryable: true, delayMs: DEFAULT_RETRY_DELAY_MS, relaySideConflict: false };
143
- }
144
- }
145
- return NOT_RETRYABLE;
146
- }
147
- function getInboxPaymentStatusUrl(paymentId) {
148
- const inboxBaseUrl = new URL(INBOX_BASE);
149
- return `${inboxBaseUrl.origin}/api/payment-status/${paymentId}`;
150
- }
151
- /**
152
- * Sleep for ms milliseconds.
153
- */
154
- function sleep(ms) {
155
- return new Promise((resolve) => setTimeout(resolve, ms));
156
- }
157
- /**
158
- * POST a message to the inbox using a confirmed txid as payment proof.
159
- * Returns a structured result so callers can decide how to handle failure
160
- * (manual recovery throws, auto-recovery tries the next txid).
161
- */
162
- async function submitWithPaymentTxid(recipientBtcAddress, recipientStxAddress, content, txid) {
163
- const url = `${INBOX_BASE}/${recipientBtcAddress}`;
164
- const res = await fetch(url, {
165
- method: "POST",
166
- headers: { "Content-Type": "application/json" },
167
- body: JSON.stringify({
168
- toBtcAddress: recipientBtcAddress,
169
- toStxAddress: recipientStxAddress,
170
- content,
171
- paymentTxid: txid,
172
- }),
173
- });
174
- const body = await res.text();
175
- const ok = res.status === 200 || res.status === 201 || res.status === 409;
176
- return { ok, status: res.status, body };
177
- }
178
- // ============================================================================
179
- // Tool Registration
6
+ // The sponsored (relay) inbox send has been removed. The relay-sponsored path
7
+ // was unstable: a burst of sends with an uninitialized local nonce could wedge
8
+ // the relay's sponsor queue (nonces stuck `held(gap)`), leaving payments
9
+ // accepted but messages undelivered (issue #540/#592). All inbox sends now go
10
+ // through send_inbox_message_direct, which signs a standard non-sponsored sBTC
11
+ // transfer and settles directly through the x402 facilitator with no relay in
12
+ // the middle.
13
+ //
14
+ // This tool is retained only as a redirect so existing agents that still call
15
+ // send_inbox_message get pointed to the working tool instead of an
16
+ // "unknown tool" error.
180
17
  // ============================================================================
181
18
  export function registerInboxTools(server) {
182
19
  server.registerTool("send_inbox_message", {
183
- description: "⛔ DEPRECATED — do not use. This sponsored (relay) inbox send no longer executes; it returns " +
184
- "a redirect to send_inbox_message_direct.\n\n" +
20
+ description: "⛔ DEPRECATED — do not use. The sponsored (relay) inbox send has been removed. " +
185
21
  "Use send_inbox_message_direct instead. It signs a standard sBTC transfer and settles directly " +
186
22
  "through the x402 facilitator (no relay). Requires an unlocked wallet holding sBTC (message cost) " +
187
- "and STX (gas). Mainnet only.\n\n" +
188
- "The sponsored implementation below is retained but gated off behind a deprecation guard — the " +
189
- "relay-sponsored path was unstable, so it is disabled rather than removed.",
23
+ "and STX (gas). Mainnet only.",
190
24
  inputSchema: {
191
25
  recipientBtcAddress: z
192
26
  .string()
@@ -198,395 +32,18 @@ export function registerInboxTools(server) {
198
32
  .string()
199
33
  .max(500)
200
34
  .describe("Message content (max 500 characters)"),
201
- paymentTxid: z
202
- .string()
203
- .optional()
204
- .describe("Optional: a confirmed on-chain sBTC transfer txid to use as payment proof. " +
205
- "When provided, skips the x402 payment flow and resubmits the message directly. " +
206
- "Use for manual recovery after a settlement timeout left the payment confirmed but the message undelivered."),
207
35
  },
208
- }, async ({ recipientBtcAddress, recipientStxAddress, content, paymentTxid }) => {
209
- // Deprecation guard: the sponsored relay path is disabled (see
210
- // SPONSORED_INBOX_DISABLED). The full implementation below is kept intact
211
- // and can be re-enabled by flipping that flag — relay-sponsored sends were
212
- // unstable, so all inbox sends go through send_inbox_message_direct.
213
- if (SPONSORED_INBOX_DISABLED) {
214
- return createJsonResponse({
215
- success: false,
216
- deprecated: true,
217
- error: "send_inbox_message is deprecated and no longer sends. The sponsored relay path " +
218
- "is disabled because relay-sponsored transactions were unstable.",
219
- useInstead: "send_inbox_message_direct",
220
- note: "Call send_inbox_message_direct with the same recipientBtcAddress, recipientStxAddress, " +
221
- "and content. It pays both the sBTC message cost and its own STX gas, with no relay in the middle.",
222
- });
223
- }
224
- try {
225
- // Network mismatch guard: fail early if testnet MCP server targets mainnet inbox.
226
- // Match the parsed hostname exactly (not a substring) so lookalike hosts can't pass.
227
- if (NETWORK === "testnet" && new URL(INBOX_BASE).hostname === "aibtc.com") {
228
- throw new Error("Network mismatch: MCP server is configured for testnet but the inbox service at aibtc.com requires mainnet. " +
229
- "Set NETWORK=mainnet or use a testnet inbox endpoint.");
230
- }
231
- const account = await getAccount();
232
- // Manual recovery: skip x402 flow and POST with the provided txid as proof
233
- if (paymentTxid) {
234
- const result = await submitWithPaymentTxid(recipientBtcAddress, recipientStxAddress, content, paymentTxid);
235
- if (!result.ok) {
236
- throw new Error(`paymentTxid recovery failed (${result.status}): ${result.body}`);
237
- }
238
- return createJsonResponse({
239
- success: true,
240
- message: result.status === 409
241
- ? "Message already delivered"
242
- : "Message delivered (manual txid recovery)",
243
- recipient: {
244
- btcAddress: recipientBtcAddress,
245
- stxAddress: recipientStxAddress,
246
- },
247
- contentLength: content.length,
248
- payment: {
249
- txid: paymentTxid,
250
- recovered: true,
251
- explorer: getExplorerTxUrl(paymentTxid, NETWORK),
252
- },
253
- });
254
- }
255
- // Step 1: POST without payment → get 402 challenge
256
- const inboxUrl = `${INBOX_BASE}/${recipientBtcAddress}`;
257
- const body = {
258
- toBtcAddress: recipientBtcAddress,
259
- toStxAddress: recipientStxAddress,
260
- content,
261
- };
262
- const initialRes = await fetch(inboxUrl, {
263
- method: "POST",
264
- headers: { "Content-Type": "application/json" },
265
- body: JSON.stringify(body),
266
- });
267
- if (initialRes.status !== 402) {
268
- const text = await initialRes.text();
269
- if (initialRes.ok) {
270
- return createJsonResponse({
271
- success: true,
272
- message: "Message sent (no payment required)",
273
- response: text,
274
- });
275
- }
276
- throw new Error(`Expected 402 payment challenge, got ${initialRes.status}: ${text}`);
277
- }
278
- // Step 2: Parse payment requirements
279
- const paymentHeader = initialRes.headers.get(X402_HEADERS.PAYMENT_REQUIRED);
280
- if (!paymentHeader) {
281
- throw new Error("402 response missing payment-required header");
282
- }
283
- const paymentRequired = decodePaymentRequired(paymentHeader);
284
- if (!paymentRequired || !paymentRequired.accepts || paymentRequired.accepts.length === 0) {
285
- throw new Error("No accepted payment methods in 402 response");
286
- }
287
- const accept = paymentRequired.accepts[0];
288
- const amount = BigInt(accept.amount);
289
- // Pre-check sBTC balance only — sponsored txs have fee: 0n so STX gas is not required
290
- const sbtcService = getSbtcService(NETWORK);
291
- const balanceInfo = await sbtcService.getBalance(account.address);
292
- const sbtcBalance = BigInt(balanceInfo.balance);
293
- if (sbtcBalance < amount) {
294
- const shortfall = amount - sbtcBalance;
295
- throw new InsufficientBalanceError(`Insufficient sBTC balance: need ${formatSbtc(accept.amount)}, have ${formatSbtc(balanceInfo.balance)} (shortfall: ${formatSbtc(shortfall.toString())}). ` +
296
- `Deposit more sBTC via the bridge at https://bridge.stx.eco or use a different wallet.`, "sBTC", balanceInfo.balance, accept.amount, shortfall.toString());
297
- }
298
- // Steps 3-5: Build payment and send with retry loop
299
- // Up to 3 attempts, waiting nextRetryDelayMs (or retryAfter) before each retry.
300
- const MAX_ATTEMPTS = 3;
301
- let lastError = "";
302
- let paymentSignature = null;
303
- // Track relay txids across failed attempts to detect stale dedup.
304
- const seenRelayTxids = new Set();
305
- // Stash first attempt's tx + paymentId for reuse on relay-side conflicts.
306
- let cachedTxHex = null;
307
- let cachedPaymentId = null;
308
- let cachedNonce = null;
309
- let nextRetryDelayMs = 0;
310
- for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
311
- if (attempt > 0 && nextRetryDelayMs > 0) {
312
- console.error(`[send_inbox_message] Retry attempt ${attempt}/${MAX_ATTEMPTS - 1} after ${nextRetryDelayMs}ms`);
313
- await sleep(nextRetryDelayMs);
314
- }
315
- // Step 3: Build sponsored sBTC transfer.
316
- // Reuse the cached transaction on relay-side nonce conflicts (the relay
317
- // deduplicates by paymentId so we avoid consuming additional nonce slots).
318
- let nonce;
319
- let txHex;
320
- let paymentId;
321
- if (cachedTxHex && cachedPaymentId && cachedNonce !== null) {
322
- // Relay-side conflict: resubmit the same tx for dedup
323
- nonce = cachedNonce;
324
- txHex = cachedTxHex;
325
- paymentId = cachedPaymentId;
326
- console.error(`[send_inbox_message] Reusing cached tx (nonce=${nonce}) for relay-side dedup`);
327
- }
328
- else {
329
- // Fresh tx: sender-side conflict or first attempt
330
- nonce = await getNextNonce(account.address);
331
- txHex = await buildSponsoredSbtcTransfer(account.privateKey, account.address, accept.payTo, amount, BigInt(nonce));
332
- paymentId = generatePaymentId();
333
- // Cache for potential reuse on relay-side conflicts
334
- cachedTxHex = txHex;
335
- cachedPaymentId = paymentId;
336
- cachedNonce = nonce;
337
- }
338
- // Step 4: Encode PaymentPayloadV2 with payment-identifier extension.
339
- paymentSignature = encodePaymentPayload({
340
- x402Version: 2,
341
- resource: paymentRequired.resource,
342
- accepted: accept,
343
- payload: { transaction: txHex },
344
- extensions: buildPaymentIdentifierExtension(paymentId),
345
- });
346
- emitPaymentLog("payment.accepted", {
347
- tool: "send_inbox_message",
348
- paymentId,
349
- action: "payment_submitted",
350
- compatShimUsed: false,
351
- });
352
- // Step 5: Send with payment header
353
- const finalRes = await fetch(inboxUrl, {
354
- method: "POST",
355
- headers: {
356
- "Content-Type": "application/json",
357
- [X402_HEADERS.PAYMENT_SIGNATURE]: paymentSignature,
358
- },
359
- body: JSON.stringify(body),
360
- });
361
- const responseData = await finalRes.text();
362
- let parsed;
363
- try {
364
- parsed = JSON.parse(responseData);
365
- }
366
- catch {
367
- parsed = { raw: responseData };
368
- }
369
- if (finalRes.status === 200 || finalRes.status === 201 || finalRes.status === 202) {
370
- // Extract payment response header for txid
371
- const settlement = decodePaymentResponse(finalRes.headers.get(X402_HEADERS.PAYMENT_RESPONSE));
372
- const txid = settlement?.transaction;
373
- // Extract paymentId and paymentStatus from inbox response body.
374
- // The inbox API nests these under "inbox": { paymentId, paymentStatus }.
375
- const inboxData = parsed.inbox;
376
- const inboxPaymentId = (inboxData?.paymentId ?? parsed.paymentId);
377
- const inboxPaymentStatus = (inboxData?.paymentStatus ?? parsed.paymentStatus);
378
- const resolvedPaymentId = inboxPaymentId || paymentId;
379
- const canonicalHints = extractCanonicalPaymentHints({
380
- payload: parsed,
381
- paymentId: resolvedPaymentId,
382
- baseUrl: INBOX_BASE,
383
- });
384
- let compatShimUsed = false;
385
- const canonicalStatus = await resolveCanonicalPaymentStatus({
386
- payload: parsed,
387
- paymentId: resolvedPaymentId,
388
- baseUrl: INBOX_BASE,
389
- fallbackCheckStatusUrl: resolvedPaymentId
390
- ? getInboxPaymentStatusUrl(resolvedPaymentId)
391
- : undefined,
392
- onPoll: ({ paymentId: polledPaymentId, checkStatusUrl, source }) => {
393
- compatShimUsed = emitCanonicalPaymentPollLogs({
394
- tool: "send_inbox_message",
395
- paymentId: polledPaymentId ?? resolvedPaymentId,
396
- checkStatusUrl,
397
- source,
398
- });
399
- },
400
- });
401
- if (canonicalStatus) {
402
- const decision = classifyCanonicalPaymentStatus(canonicalStatus);
403
- emitCanonicalPaymentDecisionLogs({
404
- tool: "send_inbox_message",
405
- status: canonicalStatus,
406
- decision,
407
- compatShimUsed,
408
- });
409
- }
410
- // Advance shared nonce tracker on success.
411
- // When no txid is available (pending settlement), record the paymentId
412
- // so the agent can correlate the nonce with a trackable reference.
413
- // Fall back to the client-generated paymentId if the response body is
414
- // missing/malformed, so success responses always record a non-empty ref.
415
- const nonceRef = txid || `pending:${resolvedPaymentId}`;
416
- await advanceNonceCache(account.address, nonce, nonceRef);
417
- // Build payment info — always include when we have any payment reference.
418
- // This ensures the agent sees payment status even when settlement is pending.
419
- // Prefer canonical poll hints when available; synthesize an inbox-local
420
- // payment-status URL only as an explicit compatibility fallback.
421
- const paymentCheckUrl = canonicalStatus?.checkStatusUrl ??
422
- canonicalHints.checkStatusUrl ??
423
- (resolvedPaymentId ? getInboxPaymentStatusUrl(resolvedPaymentId) : undefined);
424
- return createJsonResponse({
425
- success: true,
426
- message: "Message delivered",
427
- recipient: {
428
- btcAddress: recipientBtcAddress,
429
- stxAddress: recipientStxAddress,
430
- },
431
- contentLength: content.length,
432
- inbox: parsed,
433
- payment: {
434
- ...(txid && {
435
- txid,
436
- explorer: getExplorerTxUrl(txid, NETWORK),
437
- }),
438
- amount: accept.amount + " sats sBTC",
439
- status: canonicalStatus?.status ??
440
- normalizeCallerFacingStatus(inboxPaymentStatus) ??
441
- (txid ? "confirmed" : resolvedPaymentId ? "queued" : "unknown"),
442
- paymentId: resolvedPaymentId,
443
- ...(canonicalStatus?.terminalReason && {
444
- terminalReason: canonicalStatus.terminalReason,
445
- }),
446
- ...(paymentCheckUrl && { checkStatusUrl: paymentCheckUrl }),
447
- },
448
- });
449
- }
450
- // Extract relay txid from payment-response header (forwarded even on failure).
451
- // If we have seen it before, the relay is serving a stale cached result.
452
- const failedTxid = decodePaymentResponse(finalRes.headers.get(X402_HEADERS.PAYMENT_RESPONSE))?.transaction;
453
- if (failedTxid && seenRelayTxids.has(failedTxid)) {
454
- console.error(`[send_inbox_message] Stale dedup: relay returned previously-seen txid ${failedTxid} on attempt ${attempt + 1}`);
455
- }
456
- else if (failedTxid) {
457
- seenRelayTxids.add(failedTxid);
458
- }
459
- const paymentIdForStatus = cachedPaymentId ??
460
- extractPaymentIdFromPaymentSignature(paymentSignature) ??
461
- undefined;
462
- let compatShimUsed = false;
463
- const canonicalStatus = await resolveCanonicalPaymentStatus({
464
- payload: parsed,
465
- paymentId: paymentIdForStatus,
466
- baseUrl: INBOX_BASE,
467
- fallbackCheckStatusUrl: paymentIdForStatus
468
- ? getInboxPaymentStatusUrl(paymentIdForStatus)
469
- : undefined,
470
- onPoll: ({ paymentId: polledPaymentId, checkStatusUrl, source }) => {
471
- compatShimUsed = emitCanonicalPaymentPollLogs({
472
- tool: "send_inbox_message",
473
- paymentId: polledPaymentId ?? paymentIdForStatus,
474
- checkStatusUrl,
475
- source,
476
- });
477
- },
478
- });
479
- if (canonicalStatus) {
480
- const decision = classifyCanonicalPaymentStatus(canonicalStatus);
481
- emitCanonicalPaymentDecisionLogs({
482
- tool: "send_inbox_message",
483
- status: canonicalStatus,
484
- decision,
485
- compatShimUsed,
486
- });
487
- if ((decision.action === "poll_same_payment" || decision.action === "rebuild_sender") &&
488
- attempt < MAX_ATTEMPTS - 1) {
489
- nextRetryDelayMs = decision.action === "poll_same_payment" ? DEFAULT_RETRY_DELAY_MS : 0;
490
- if (decision.action === "rebuild_sender") {
491
- cachedTxHex = null;
492
- cachedPaymentId = null;
493
- cachedNonce = null;
494
- await advanceNonceCache(account.address, nonce);
495
- }
496
- lastError = `${finalRes.status}: ${decision.summary}`;
497
- continue;
498
- }
499
- const errorBase = `Message delivery failed (${finalRes.status}). ${decision.summary}` +
500
- (canonicalStatus.error ? ` Relay detail: ${canonicalStatus.error}` : "");
501
- throw new Error(`${errorBase}\n\nCanonical payment status:\n${formatCanonicalPaymentStatus(canonicalStatus)}`);
502
- }
503
- // Fall back to transport-specific retry hints only when canonical status is unavailable.
504
- const retry = classifyLegacyRetryableError(finalRes.status, parsed);
505
- if (retry.retryable && attempt < MAX_ATTEMPTS - 1) {
506
- console.error(`[send_inbox_message] Retryable error on attempt ${attempt + 1}: status=${finalRes.status} relaySide=${retry.relaySideConflict} delayMs=${retry.delayMs} body=${responseData}`);
507
- nextRetryDelayMs = retry.delayMs;
508
- if (retry.relaySideConflict) {
509
- // Keep cached tx/paymentId — relay will dedup on resubmit
510
- }
511
- else {
512
- // Sender-side conflict: need a fresh tx with new nonce
513
- cachedTxHex = null;
514
- cachedPaymentId = null;
515
- cachedNonce = null;
516
- // Advance nonce cache so the next attempt uses a strictly higher nonce.
517
- await advanceNonceCache(account.address, nonce);
518
- }
519
- lastError = `${finalRes.status}: ${responseData}`;
520
- continue;
521
- }
522
- // Non-retryable or last attempt — build error with txid recovery info
523
- const txid = paymentSignature
524
- ? extractTxidFromPaymentSignature(paymentSignature)
525
- : null;
526
- const errorBase = `Message delivery failed (${finalRes.status}): ${responseData}`;
527
- if (txid) {
528
- // Poll briefly for on-chain status so the error includes actionable info
529
- const confirmation = await pollTransactionConfirmation(txid, NETWORK);
530
- emitPaymentLog("payment.fallback_used", {
531
- tool: "send_inbox_message",
532
- paymentId: paymentIdForStatus,
533
- status: confirmation.status,
534
- action: "txid_recovery",
535
- compatShimUsed: false,
536
- });
537
- throw new Error(`${errorBase}\n\nCanonical payment status was unavailable, so only txid recovery fallback is available. ` +
538
- `Payment transaction may already exist on-chain. ` +
539
- `Transaction recovery info:\n txid: ${confirmation.txid}\n status: ${confirmation.status}\n explorer: ${confirmation.explorer}`);
540
- }
541
- throw new Error(errorBase);
542
- }
543
- // Retries exhausted -- check if any relay txid confirmed on-chain and
544
- // resubmit with the confirmed txid as payment proof.
545
- if (seenRelayTxids.size > 0) {
546
- console.error(`[send_inbox_message] Checking on-chain status of ${seenRelayTxids.size} seen txid(s) before giving up.`);
547
- for (const seenTxid of seenRelayTxids) {
548
- try {
549
- const confirmation = await pollTransactionConfirmation(seenTxid, NETWORK, 5_000);
550
- if (confirmation.status !== "success" && confirmation.status !== "confirmed") {
551
- continue;
552
- }
553
- console.error(`[send_inbox_message] Auto-recovery: txid ${seenTxid} confirmed on-chain. Resubmitting.`);
554
- const result = await submitWithPaymentTxid(recipientBtcAddress, recipientStxAddress, content, seenTxid);
555
- if (result.ok) {
556
- return createJsonResponse({
557
- success: true,
558
- message: result.status === 409
559
- ? "Message already delivered"
560
- : "Message delivered (auto-recovered with confirmed txid)",
561
- recipient: {
562
- btcAddress: recipientBtcAddress,
563
- stxAddress: recipientStxAddress,
564
- },
565
- contentLength: content.length,
566
- payment: {
567
- txid: seenTxid,
568
- amount: accept.amount + " sats sBTC",
569
- explorer: getExplorerTxUrl(seenTxid, NETWORK),
570
- recovered: true,
571
- },
572
- });
573
- }
574
- console.error(`[send_inbox_message] Auto-recovery resubmission failed for txid ${seenTxid}: ${result.status} ${result.body}`);
575
- }
576
- catch {
577
- // Non-fatal: move on to the next txid
578
- }
579
- }
580
- }
581
- // Include all seen txids in the error for diagnostics
582
- const txidSummary = seenRelayTxids.size > 0
583
- ? `\n\nSeen relay txids (all failed or pending):\n${[...seenRelayTxids].map((id) => ` ${id}`).join("\n")}`
584
- : "";
585
- throw new Error(`Message delivery failed after ${MAX_ATTEMPTS} attempts. Last error: ${lastError}${txidSummary}`);
586
- }
587
- catch (error) {
588
- return createErrorResponse(error);
589
- }
36
+ }, async () => {
37
+ return createJsonResponse({
38
+ success: false,
39
+ deprecated: true,
40
+ error: "send_inbox_message has been removed. The sponsored relay path was unstable " +
41
+ "(payments could be accepted but never delivered when the relay's sponsor nonce " +
42
+ "queue wedged).",
43
+ useInstead: "send_inbox_message_direct",
44
+ note: "Call send_inbox_message_direct with the same recipientBtcAddress, recipientStxAddress, " +
45
+ "and content. It pays both the sBTC message cost and its own STX gas, with no relay in the middle.",
46
+ });
590
47
  });
591
48
  }
592
49
  //# sourceMappingURL=inbox.tools.js.map
@@ -39,6 +39,8 @@ import { registerNewsTools } from "./news.tools.js";
39
39
  import { registerIdentityTools } from "./identity.tools.js";
40
40
  import { registerCredentialsTools } from "./credentials.tools.js";
41
41
  import { registerSouldinalsTools } from "./souldinals.tools.js";
42
+ import { registerOrdinalsTools } from "./ordinals.tools.js";
43
+ import { registerChildInscriptionTools } from "./child-inscription.tools.js";
42
44
  import { registerBountyScannerTools } from "./bounty-scanner.tools.js";
43
45
  import { registerRunesTools } from "./runes.tools.js";
44
46
  import { registerInboxTools } from "./inbox.tools.js";
@@ -169,8 +171,12 @@ export function registerAllTools(server) {
169
171
  registerIdentityTools(server);
170
172
  // Credentials (encrypted credential store — list, get, set, delete, unlock)
171
173
  registerCredentialsTools(server);
174
+ // Ordinals (genesis inscriptions — taproot address, estimate fee, inscribe, reveal, lookup)
175
+ registerOrdinalsTools(server);
172
176
  // Souldinals (soul.md child inscriptions — inscribe, reveal, list, load, display traits)
173
177
  registerSouldinalsTools(server);
178
+ // Child inscriptions (parent-child provenance — estimate fee, commit, reveal)
179
+ registerChildInscriptionTools(server);
174
180
  // Bounty board (aibtc.com/api/bounties — list, get, submit, accept, pay, cancel, my-views)
175
181
  registerBountyScannerTools(server);
176
182
  // Runes (Bitcoin-native fungible tokens — list, query, holders, activity, balances)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aibtc/mcp-server",
3
- "version": "1.62.0",
3
+ "version": "1.63.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.62.0 # x-release-please-version
7
+ version: 1.63.0 # x-release-please-version
8
8
  npm: "@aibtc/mcp-server"
9
9
  github: https://github.com/aibtcdev/aibtc-mcp-server
10
10
  ---