@aibtc/mcp-server 1.64.1 → 1.65.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/index.js +18 -0
- package/dist/services/x402.service.d.ts +72 -7
- package/dist/services/x402.service.js +142 -14
- package/dist/tools/bounty-hint.d.ts +37 -0
- package/dist/tools/bounty-hint.js +121 -0
- package/dist/tools/dual-stacking.tools.js +97 -33
- package/dist/tools/endpoint.tools.js +30 -7
- package/package.json +1 -1
- package/skill/SKILL.md +1 -1
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import { createRequire } from "module";
|
|
|
7
7
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
8
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
9
9
|
import { registerAllTools } from "./tools/index.js";
|
|
10
|
+
import { installBountyHint } from "./tools/bounty-hint.js";
|
|
10
11
|
import { NETWORK, API_URL } from "./config/index.js";
|
|
11
12
|
import { redactSensitive } from "./utils/redact.js";
|
|
12
13
|
import { initializeStorage } from "./utils/storage.js";
|
|
@@ -253,9 +254,26 @@ else {
|
|
|
253
254
|
const server = new McpServer({
|
|
254
255
|
name: "aibtc-mcp-server",
|
|
255
256
|
version: packageJson.version,
|
|
257
|
+
}, {
|
|
258
|
+
instructions: [
|
|
259
|
+
"This server lets an agent earn and move real Bitcoin/sBTC on Stacks.",
|
|
260
|
+
"",
|
|
261
|
+
"Hunt bounties. The most direct way to earn here is the sBTC bounty board:",
|
|
262
|
+
"call bounty_list to see open bounties, bounty_get for the task detail, do",
|
|
263
|
+
"the work, then bounty_submit your deliverable. Accepted submissions are paid",
|
|
264
|
+
"in sBTC. Check periodically for new open bounties.",
|
|
265
|
+
"",
|
|
266
|
+
"For the full menu of ways to put assets to work (stacking, DeFi yield,",
|
|
267
|
+
"trading, paid endpoints), call earning_opportunities. Submitting to a bounty",
|
|
268
|
+
"requires a Registered (L1+) on-chain identity — see identity_register.",
|
|
269
|
+
].join("\n"),
|
|
256
270
|
});
|
|
271
|
+
// Append a bounty-board hint to spend/onboarding-tool output.
|
|
272
|
+
// Must wrap registerTool before registration; restore it after.
|
|
273
|
+
const restoreBountyHint = installBountyHint(server);
|
|
257
274
|
// Register all tools from the modular registry
|
|
258
275
|
registerAllTools(server);
|
|
276
|
+
restoreBountyHint();
|
|
259
277
|
async function main() {
|
|
260
278
|
await initializeStorage();
|
|
261
279
|
const transport = new StdioServerTransport();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type AxiosInstance } from "axios";
|
|
2
|
+
import { type PaymentRequirementsV2 } from "../utils/x402-protocol.js";
|
|
2
3
|
import { NETWORK, API_URL, type Network } from "../config/networks.js";
|
|
3
4
|
import type { Account } from "../transactions/builder.js";
|
|
4
5
|
/**
|
|
@@ -34,6 +35,12 @@ export interface CreateApiClientOptions {
|
|
|
34
35
|
*/
|
|
35
36
|
onBeforePayment?: (requirements: PaymentRequirements) => Promise<void>;
|
|
36
37
|
toolName?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Which asset to pay with when the endpoint offers several — an asset
|
|
40
|
+
* identifier ("SM3V....sbtc-token") or a symbol ("sBTC", "STX", "USDCx").
|
|
41
|
+
* Unset means the first entry this client can pay for.
|
|
42
|
+
*/
|
|
43
|
+
asset?: string;
|
|
37
44
|
}
|
|
38
45
|
/**
|
|
39
46
|
* Create an API client with x402 payment interceptor.
|
|
@@ -64,9 +71,13 @@ export type ProbeResultPaymentRequired = {
|
|
|
64
71
|
type: 'payment_required';
|
|
65
72
|
amount: string;
|
|
66
73
|
asset: string;
|
|
74
|
+
/** Display symbol for `asset` (e.g. "USDCx"), when derivable. */
|
|
75
|
+
symbol?: string;
|
|
67
76
|
recipient: string;
|
|
68
77
|
network: string;
|
|
69
78
|
endpoint: string;
|
|
79
|
+
/** Every asset the endpoint accepts, so the caller can pick another. */
|
|
80
|
+
accepts?: PaymentOptionSummary[];
|
|
70
81
|
resource?: {
|
|
71
82
|
url: string;
|
|
72
83
|
description?: string;
|
|
@@ -76,18 +87,70 @@ export type ProbeResultPaymentRequired = {
|
|
|
76
87
|
};
|
|
77
88
|
export type ProbeResult = ProbeResultFree | ProbeResultPaymentRequired;
|
|
78
89
|
/**
|
|
79
|
-
*
|
|
90
|
+
* Token rails this client can actually build and sign a payment for.
|
|
91
|
+
* Anything else is `unsupported` — see detectTokenType.
|
|
92
|
+
*/
|
|
93
|
+
export type PaymentTokenType = 'STX' | 'sBTC' | 'unsupported';
|
|
94
|
+
/**
|
|
95
|
+
* Detect token type from asset identifier.
|
|
96
|
+
*
|
|
97
|
+
* STX must be identified POSITIVELY, never as a fallback. This function used
|
|
98
|
+
* to return 'STX' for any unrecognized asset, which meant a USDCx-denominated
|
|
99
|
+
* 402 (asset `SP120...usdcx`, amount `29000000`) was routed to the interceptor's
|
|
100
|
+
* native-STX branch and signed as a 29 STX transfer — real funds, wrong asset,
|
|
101
|
+
* and the endpoint never credits it. Unknown assets are now `unsupported` so
|
|
102
|
+
* the payment is refused instead of silently mis-sent (#613).
|
|
103
|
+
*
|
|
80
104
|
* @param asset - Full contract identifier or token name
|
|
81
|
-
* @returns 'STX' for native STX, 'sBTC' for sBTC token
|
|
82
105
|
*/
|
|
83
|
-
export declare function detectTokenType(asset: string):
|
|
106
|
+
export declare function detectTokenType(asset: string): PaymentTokenType;
|
|
84
107
|
/**
|
|
85
|
-
*
|
|
86
|
-
*
|
|
108
|
+
* Best-effort display symbol for an asset. Servers advertise a friendly name in
|
|
109
|
+
* `extra.tokenType` (arc0btc sends "USDCx"); otherwise fall back to the contract
|
|
110
|
+
* name, then the raw identifier.
|
|
111
|
+
*/
|
|
112
|
+
export declare function resolveAssetSymbol(asset: string, extra?: Record<string, unknown>): string;
|
|
113
|
+
/**
|
|
114
|
+
* Format payment amount into human-readable string with token symbol.
|
|
115
|
+
*
|
|
116
|
+
* Only STX and sBTC have known decimal scales here. For any other asset the
|
|
117
|
+
* amount is rendered in raw base units with an explicit marker rather than
|
|
118
|
+
* being run through the STX formatter — dividing a USDCx amount by 1e6 and
|
|
119
|
+
* labelling it "STX" is how `29000000` USDCx base units got reported as
|
|
120
|
+
* "29 STX" (#613).
|
|
121
|
+
*
|
|
122
|
+
* @param amount - Raw amount string (microSTX, satoshis, or token base units)
|
|
87
123
|
* @param asset - Token asset identifier
|
|
88
|
-
* @
|
|
124
|
+
* @param extra - Scheme-specific `extra` block from the accepts[] entry
|
|
125
|
+
* @returns Formatted string like "0.000001 sBTC", "0.001 STX", or "29000000 USDCx (base units)"
|
|
89
126
|
*/
|
|
90
|
-
export declare function formatPaymentAmount(amount: string, asset: string): string;
|
|
127
|
+
export declare function formatPaymentAmount(amount: string, asset: string, extra?: Record<string, unknown>): string;
|
|
128
|
+
/** One payment option, summarised for callers choosing between them. */
|
|
129
|
+
export interface PaymentOptionSummary {
|
|
130
|
+
asset: string;
|
|
131
|
+
symbol: string;
|
|
132
|
+
amount: string;
|
|
133
|
+
formatted: string;
|
|
134
|
+
payTo: string;
|
|
135
|
+
/** False when this client cannot build a transaction for the asset. */
|
|
136
|
+
payable: boolean;
|
|
137
|
+
}
|
|
138
|
+
export declare function summarizePaymentOptions(accepts: PaymentRequirementsV2[]): PaymentOptionSummary[];
|
|
139
|
+
/**
|
|
140
|
+
* Choose which entry of `accepts[]` to pay.
|
|
141
|
+
*
|
|
142
|
+
* Previously this was hardcoded to the first Stacks-network entry, so on a
|
|
143
|
+
* multi-asset endpoint the caller got whatever the server happened to list
|
|
144
|
+
* first — USDCx on arc0btc — with no way to ask for anything else (#613).
|
|
145
|
+
*
|
|
146
|
+
* - `preferredAsset` set: that asset or an explicit error naming what IS on
|
|
147
|
+
* offer. Never silently substitutes a different asset than the one asked for.
|
|
148
|
+
* - unset: the first entry this client can actually pay for, in the server's
|
|
149
|
+
* own preference order. Deliberately NOT balance-aware — auto-selecting by
|
|
150
|
+
* wallet contents would let a manifest reorder change which asset leaves the
|
|
151
|
+
* wallet without the caller ever naming it.
|
|
152
|
+
*/
|
|
153
|
+
export declare function selectPaymentOption(accepts: PaymentRequirementsV2[], preferredAsset?: string): PaymentRequirementsV2;
|
|
91
154
|
/**
|
|
92
155
|
* Probe an endpoint without payment interceptor
|
|
93
156
|
* Returns either free response data or payment requirements
|
|
@@ -97,6 +160,8 @@ export declare function probeEndpoint(options: {
|
|
|
97
160
|
url: string;
|
|
98
161
|
params?: Record<string, string>;
|
|
99
162
|
data?: Record<string, unknown>;
|
|
163
|
+
/** Asset to quote (identifier or symbol). Defaults to the first payable entry. */
|
|
164
|
+
asset?: string;
|
|
100
165
|
}): Promise<ProbeResult>;
|
|
101
166
|
/**
|
|
102
167
|
* Generate a stable deduplication key for a request
|
|
@@ -473,12 +473,18 @@ export async function createApiClient(baseUrl, options) {
|
|
|
473
473
|
if (!paymentRequired || !paymentRequired.accepts || paymentRequired.accepts.length === 0) {
|
|
474
474
|
return Promise.reject(new Error("Invalid x402 402 response: missing or empty payment-required header"));
|
|
475
475
|
}
|
|
476
|
-
// Select
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
476
|
+
// Select the payment option: the caller's requested asset, else the
|
|
477
|
+
// first entry this client can actually sign for. Throws with the list
|
|
478
|
+
// of accepted assets rather than falling back to a different one.
|
|
479
|
+
let selectedOption;
|
|
480
|
+
try {
|
|
481
|
+
selectedOption = selectPaymentOption(paymentRequired.accepts, options?.asset);
|
|
481
482
|
}
|
|
483
|
+
catch (selectionError) {
|
|
484
|
+
return Promise.reject(selectionError);
|
|
485
|
+
}
|
|
486
|
+
// Guaranteed non-'unsupported' by selectPaymentOption, so the branch
|
|
487
|
+
// below can never route a third-party token into the native-STX path.
|
|
482
488
|
const tokenType = detectTokenType(selectedOption.asset);
|
|
483
489
|
const amount = BigInt(selectedOption.amount);
|
|
484
490
|
const isSbtc = tokenType === "sBTC";
|
|
@@ -624,9 +630,16 @@ export async function getAccount() {
|
|
|
624
630
|
return mnemonicToAccount(mnemonic, NETWORK);
|
|
625
631
|
}
|
|
626
632
|
/**
|
|
627
|
-
* Detect token type from asset identifier
|
|
633
|
+
* Detect token type from asset identifier.
|
|
634
|
+
*
|
|
635
|
+
* STX must be identified POSITIVELY, never as a fallback. This function used
|
|
636
|
+
* to return 'STX' for any unrecognized asset, which meant a USDCx-denominated
|
|
637
|
+
* 402 (asset `SP120...usdcx`, amount `29000000`) was routed to the interceptor's
|
|
638
|
+
* native-STX branch and signed as a 29 STX transfer — real funds, wrong asset,
|
|
639
|
+
* and the endpoint never credits it. Unknown assets are now `unsupported` so
|
|
640
|
+
* the payment is refused instead of silently mis-sent (#613).
|
|
641
|
+
*
|
|
628
642
|
* @param asset - Full contract identifier or token name
|
|
629
|
-
* @returns 'STX' for native STX, 'sBTC' for sBTC token
|
|
630
643
|
*/
|
|
631
644
|
export function detectTokenType(asset) {
|
|
632
645
|
const assetLower = asset.trim().toLowerCase();
|
|
@@ -636,27 +649,133 @@ export function detectTokenType(asset) {
|
|
|
636
649
|
if (assetLower === 'sbtc' || assetLower.endsWith('::token-sbtc') || assetLower.endsWith('.sbtc-token')) {
|
|
637
650
|
return 'sBTC';
|
|
638
651
|
}
|
|
639
|
-
|
|
652
|
+
// Native STX: the bare symbol, or a CAIP-19-style native reference
|
|
653
|
+
// ("stacks:1/native" / ".../slip44:5773") used by some manifests.
|
|
654
|
+
if (assetLower === 'stx' || /(^|\/)(native|slip44:5773)$/.test(assetLower)) {
|
|
655
|
+
return 'STX';
|
|
656
|
+
}
|
|
657
|
+
return 'unsupported';
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Best-effort display symbol for an asset. Servers advertise a friendly name in
|
|
661
|
+
* `extra.tokenType` (arc0btc sends "USDCx"); otherwise fall back to the contract
|
|
662
|
+
* name, then the raw identifier.
|
|
663
|
+
*/
|
|
664
|
+
export function resolveAssetSymbol(asset, extra) {
|
|
665
|
+
const advertised = extra?.tokenType;
|
|
666
|
+
if (typeof advertised === 'string' && advertised.trim()) {
|
|
667
|
+
return advertised.trim();
|
|
668
|
+
}
|
|
669
|
+
const tokenType = detectTokenType(asset);
|
|
670
|
+
if (tokenType !== 'unsupported') {
|
|
671
|
+
return tokenType;
|
|
672
|
+
}
|
|
673
|
+
// "SP120....usdcx" -> "usdcx"; strip any "::asset-name" suffix first.
|
|
674
|
+
const withoutAssetName = asset.split('::')[0];
|
|
675
|
+
const contractName = withoutAssetName.split('.')[1];
|
|
676
|
+
return contractName || asset;
|
|
640
677
|
}
|
|
641
678
|
/**
|
|
642
|
-
* Format payment amount into human-readable string with token symbol
|
|
643
|
-
*
|
|
679
|
+
* Format payment amount into human-readable string with token symbol.
|
|
680
|
+
*
|
|
681
|
+
* Only STX and sBTC have known decimal scales here. For any other asset the
|
|
682
|
+
* amount is rendered in raw base units with an explicit marker rather than
|
|
683
|
+
* being run through the STX formatter — dividing a USDCx amount by 1e6 and
|
|
684
|
+
* labelling it "STX" is how `29000000` USDCx base units got reported as
|
|
685
|
+
* "29 STX" (#613).
|
|
686
|
+
*
|
|
687
|
+
* @param amount - Raw amount string (microSTX, satoshis, or token base units)
|
|
644
688
|
* @param asset - Token asset identifier
|
|
645
|
-
* @
|
|
689
|
+
* @param extra - Scheme-specific `extra` block from the accepts[] entry
|
|
690
|
+
* @returns Formatted string like "0.000001 sBTC", "0.001 STX", or "29000000 USDCx (base units)"
|
|
646
691
|
*/
|
|
647
|
-
export function formatPaymentAmount(amount, asset) {
|
|
692
|
+
export function formatPaymentAmount(amount, asset, extra) {
|
|
648
693
|
const tokenType = detectTokenType(asset);
|
|
649
694
|
if (tokenType === 'sBTC') {
|
|
650
695
|
return formatSbtc(amount);
|
|
651
696
|
}
|
|
697
|
+
if (tokenType === 'unsupported') {
|
|
698
|
+
// Decimals are unknown, so do not invent a scaled figure.
|
|
699
|
+
return `${amount} ${resolveAssetSymbol(asset, extra)} (base units)`;
|
|
700
|
+
}
|
|
652
701
|
return formatStx(amount);
|
|
653
702
|
}
|
|
703
|
+
/**
|
|
704
|
+
* Does `requested` name this accepts[] entry? Matches on the full asset
|
|
705
|
+
* identifier or on the display symbol, both case-insensitively, so callers can
|
|
706
|
+
* say "sBTC", "usdcx", or the full "SM3V....sbtc-token".
|
|
707
|
+
*/
|
|
708
|
+
function assetMatches(option, requested) {
|
|
709
|
+
const want = requested.trim().toLowerCase();
|
|
710
|
+
if (!want)
|
|
711
|
+
return false;
|
|
712
|
+
if (option.asset.trim().toLowerCase() === want)
|
|
713
|
+
return true;
|
|
714
|
+
if (resolveAssetSymbol(option.asset, option.extra).toLowerCase() === want)
|
|
715
|
+
return true;
|
|
716
|
+
// "sbtc" should also match the full sbtc-token contract id, and "stx" the
|
|
717
|
+
// native entry, without the caller needing the exact string.
|
|
718
|
+
const tokenType = detectTokenType(option.asset);
|
|
719
|
+
return tokenType !== 'unsupported' && tokenType.toLowerCase() === want;
|
|
720
|
+
}
|
|
721
|
+
export function summarizePaymentOptions(accepts) {
|
|
722
|
+
return accepts.map((opt) => ({
|
|
723
|
+
asset: opt.asset,
|
|
724
|
+
symbol: resolveAssetSymbol(opt.asset, opt.extra),
|
|
725
|
+
amount: opt.amount,
|
|
726
|
+
formatted: formatPaymentAmount(opt.amount, opt.asset, opt.extra),
|
|
727
|
+
payTo: opt.payTo,
|
|
728
|
+
payable: detectTokenType(opt.asset) !== 'unsupported',
|
|
729
|
+
}));
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Choose which entry of `accepts[]` to pay.
|
|
733
|
+
*
|
|
734
|
+
* Previously this was hardcoded to the first Stacks-network entry, so on a
|
|
735
|
+
* multi-asset endpoint the caller got whatever the server happened to list
|
|
736
|
+
* first — USDCx on arc0btc — with no way to ask for anything else (#613).
|
|
737
|
+
*
|
|
738
|
+
* - `preferredAsset` set: that asset or an explicit error naming what IS on
|
|
739
|
+
* offer. Never silently substitutes a different asset than the one asked for.
|
|
740
|
+
* - unset: the first entry this client can actually pay for, in the server's
|
|
741
|
+
* own preference order. Deliberately NOT balance-aware — auto-selecting by
|
|
742
|
+
* wallet contents would let a manifest reorder change which asset leaves the
|
|
743
|
+
* wallet without the caller ever naming it.
|
|
744
|
+
*/
|
|
745
|
+
export function selectPaymentOption(accepts, preferredAsset) {
|
|
746
|
+
const stacksOptions = accepts.filter((opt) => opt.network?.startsWith('stacks:'));
|
|
747
|
+
if (stacksOptions.length === 0) {
|
|
748
|
+
const networks = accepts.map((a) => a.network).join(', ') || 'none';
|
|
749
|
+
throw new Error(`No compatible Stacks payment option found. Available networks: ${networks}`);
|
|
750
|
+
}
|
|
751
|
+
const describe = (opts) => opts
|
|
752
|
+
.map((o) => `${resolveAssetSymbol(o.asset, o.extra)} (${o.asset})`)
|
|
753
|
+
.join(', ');
|
|
754
|
+
if (preferredAsset) {
|
|
755
|
+
const match = stacksOptions.find((opt) => assetMatches(opt, preferredAsset));
|
|
756
|
+
if (!match) {
|
|
757
|
+
throw new Error(`Endpoint does not accept "${preferredAsset}". Accepted assets: ${describe(stacksOptions)}.`);
|
|
758
|
+
}
|
|
759
|
+
if (detectTokenType(match.asset) === 'unsupported') {
|
|
760
|
+
throw new Error(`Payment in ${resolveAssetSymbol(match.asset, match.extra)} (${match.asset}) is not supported — ` +
|
|
761
|
+
`this client can only sign STX and sBTC payments. ` +
|
|
762
|
+
`Accepted assets: ${describe(stacksOptions)}.`);
|
|
763
|
+
}
|
|
764
|
+
return match;
|
|
765
|
+
}
|
|
766
|
+
const payable = stacksOptions.find((opt) => detectTokenType(opt.asset) !== 'unsupported');
|
|
767
|
+
if (!payable) {
|
|
768
|
+
throw new Error(`No payable asset offered: this client can only sign STX and sBTC payments, ` +
|
|
769
|
+
`but the endpoint accepts ${describe(stacksOptions)}.`);
|
|
770
|
+
}
|
|
771
|
+
return payable;
|
|
772
|
+
}
|
|
654
773
|
/**
|
|
655
774
|
* Probe an endpoint without payment interceptor
|
|
656
775
|
* Returns either free response data or payment requirements
|
|
657
776
|
*/
|
|
658
777
|
export async function probeEndpoint(options) {
|
|
659
|
-
const { method, url, params, data } = options;
|
|
778
|
+
const { method, url, params, data, asset: preferredAsset } = options;
|
|
660
779
|
const axiosInstance = createBaseAxiosInstance();
|
|
661
780
|
try {
|
|
662
781
|
const response = await axiosInstance.request({ method, url, params, data });
|
|
@@ -675,18 +794,22 @@ export async function probeEndpoint(options) {
|
|
|
675
794
|
const paymentRequired = decodePaymentRequired(headerValue);
|
|
676
795
|
// If v2 header is successfully parsed, use it
|
|
677
796
|
if (paymentRequired?.accepts?.length) {
|
|
678
|
-
|
|
797
|
+
// Pick the requested asset, or the first one we can actually pay —
|
|
798
|
+
// NOT blindly accepts[0], which quoted USDCx to sBTC-only wallets (#613).
|
|
799
|
+
const acceptedPayment = selectPaymentOption(paymentRequired.accepts, preferredAsset);
|
|
679
800
|
// Convert CAIP-2 network identifier to human-readable format
|
|
680
801
|
const network = getNetworkFromStacksChainId(acceptedPayment.network) ?? NETWORK;
|
|
681
802
|
return {
|
|
682
803
|
type: 'payment_required',
|
|
683
804
|
amount: acceptedPayment.amount,
|
|
684
805
|
asset: acceptedPayment.asset,
|
|
806
|
+
symbol: resolveAssetSymbol(acceptedPayment.asset, acceptedPayment.extra),
|
|
685
807
|
recipient: acceptedPayment.payTo,
|
|
686
808
|
network,
|
|
687
809
|
endpoint: url,
|
|
688
810
|
resource: paymentRequired.resource,
|
|
689
811
|
maxTimeoutSeconds: acceptedPayment.maxTimeoutSeconds,
|
|
812
|
+
accepts: summarizePaymentOptions(paymentRequired.accepts),
|
|
690
813
|
};
|
|
691
814
|
}
|
|
692
815
|
// Fall back to v1 body parsing
|
|
@@ -750,6 +873,11 @@ export function recordTransaction(key, txid) {
|
|
|
750
873
|
export async function checkSufficientBalance(account, amount, asset, sponsored = false) {
|
|
751
874
|
const tokenType = detectTokenType(asset);
|
|
752
875
|
const requiredAmount = BigInt(amount);
|
|
876
|
+
if (tokenType === 'unsupported') {
|
|
877
|
+
// No balance rail for third-party tokens. Refuse rather than falling
|
|
878
|
+
// through to the STX check, which would validate the wrong asset (#613).
|
|
879
|
+
throw new Error(`Cannot validate balance for asset ${asset}: this client only supports STX and sBTC payments.`);
|
|
880
|
+
}
|
|
753
881
|
if (tokenType === 'sBTC') {
|
|
754
882
|
const sbtcService = getSbtcService(account.network);
|
|
755
883
|
const balanceInfo = await sbtcService.getBalance(account.address);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounty hint injector.
|
|
3
|
+
*
|
|
4
|
+
* Appends a one-line pointer to the sBTC bounty board on the output of two tool
|
|
5
|
+
* groups:
|
|
6
|
+
* - SPEND_TOOLS: money-moving tools (transfers, contract calls, swaps, protocol
|
|
7
|
+
* deposits). When an agent has just moved value, remind it the board pays sBTC.
|
|
8
|
+
* - ONBOARDING_TOOLS: identity registration and wallet creation/import. When an
|
|
9
|
+
* agent has just set itself up, point it at the fastest way to start earning.
|
|
10
|
+
*
|
|
11
|
+
* Deliberately NOT applied to read/query tools an agent calls constantly
|
|
12
|
+
* (balances, status, lists) — that would be noise. Only these two groups get it.
|
|
13
|
+
*
|
|
14
|
+
* The hint is a separate text content block, static (no network call), and only
|
|
15
|
+
* added to successful results — never to errors, and never touching the tool's
|
|
16
|
+
* own JSON payload.
|
|
17
|
+
*/
|
|
18
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
19
|
+
/**
|
|
20
|
+
* Tools that move or spend value. A hint is appended to their successful output.
|
|
21
|
+
*/
|
|
22
|
+
export declare const SPEND_TOOLS: ReadonlySet<string>;
|
|
23
|
+
/**
|
|
24
|
+
* Onboarding tools. A hint is appended right after an agent registers an identity
|
|
25
|
+
* or creates/imports a wallet — the moment it's deciding what to do next.
|
|
26
|
+
*/
|
|
27
|
+
export declare const ONBOARDING_TOOLS: ReadonlySet<string>;
|
|
28
|
+
/**
|
|
29
|
+
* Monkey-patch `server.registerTool` so that any tool in SPEND_TOOLS or
|
|
30
|
+
* ONBOARDING_TOOLS has the relevant bounty hint appended to its (successful)
|
|
31
|
+
* output. Must be called BEFORE registering the tools. No-op for every other tool.
|
|
32
|
+
*
|
|
33
|
+
* Returns a cleanup function that restores the original method, mirroring
|
|
34
|
+
* `withSkillMeta` in tools/index.ts — call it once registration is done.
|
|
35
|
+
*/
|
|
36
|
+
export declare function installBountyHint(server: McpServer): () => void;
|
|
37
|
+
//# sourceMappingURL=bounty-hint.d.ts.map
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tools that move or spend value. A hint is appended to their successful output.
|
|
3
|
+
*/
|
|
4
|
+
export const SPEND_TOOLS = new Set([
|
|
5
|
+
// Core transfers / contract / swap chokepoints
|
|
6
|
+
"transfer_stx",
|
|
7
|
+
"transfer_btc",
|
|
8
|
+
"transfer_token",
|
|
9
|
+
"transfer_nft",
|
|
10
|
+
"transfer_rune",
|
|
11
|
+
"sbtc_transfer",
|
|
12
|
+
"call_contract",
|
|
13
|
+
"deploy_contract",
|
|
14
|
+
"bitflow_swap",
|
|
15
|
+
"alex_swap",
|
|
16
|
+
"lightning_pay_invoice",
|
|
17
|
+
// Stacking / PoX
|
|
18
|
+
"stack_stx",
|
|
19
|
+
"extend_stacking",
|
|
20
|
+
"dual_stacking_enroll",
|
|
21
|
+
// Zest lending
|
|
22
|
+
"zest_supply",
|
|
23
|
+
"zest_borrow",
|
|
24
|
+
"zest_repay",
|
|
25
|
+
// Pillar smart wallet (browser-handoff)
|
|
26
|
+
"pillar_supply",
|
|
27
|
+
"pillar_boost",
|
|
28
|
+
"pillar_unwind",
|
|
29
|
+
"pillar_send",
|
|
30
|
+
"pillar_fund",
|
|
31
|
+
"pillar_auto_compound",
|
|
32
|
+
// Pillar smart wallet (agent-signed direct)
|
|
33
|
+
"pillar_direct_supply",
|
|
34
|
+
"pillar_direct_boost",
|
|
35
|
+
"pillar_direct_unwind",
|
|
36
|
+
"pillar_direct_send",
|
|
37
|
+
"pillar_direct_stack_stx",
|
|
38
|
+
// JingSwap batch auction
|
|
39
|
+
"jingswap_deposit_stx",
|
|
40
|
+
"jingswap_deposit_sbtc",
|
|
41
|
+
// Styx BTC→sBTC
|
|
42
|
+
"styx_deposit",
|
|
43
|
+
// StackSpot lottery
|
|
44
|
+
"stackspot_join_pot",
|
|
45
|
+
"stackspot_start_pot",
|
|
46
|
+
// sBTC peg
|
|
47
|
+
"sbtc_deposit",
|
|
48
|
+
"sbtc_withdraw",
|
|
49
|
+
"sbtc_initiate_withdrawal",
|
|
50
|
+
// Inbox (direct sBTC send)
|
|
51
|
+
"send_inbox_message_direct",
|
|
52
|
+
]);
|
|
53
|
+
/**
|
|
54
|
+
* Onboarding tools. A hint is appended right after an agent registers an identity
|
|
55
|
+
* or creates/imports a wallet — the moment it's deciding what to do next.
|
|
56
|
+
*/
|
|
57
|
+
export const ONBOARDING_TOOLS = new Set([
|
|
58
|
+
"identity_register",
|
|
59
|
+
"wallet_create",
|
|
60
|
+
"wallet_import",
|
|
61
|
+
]);
|
|
62
|
+
const SPEND_HINT = "💰 Tip: the AIBTC bounty board pays sBTC for completed tasks — " +
|
|
63
|
+
"call bounty_list to see open bounties you could earn from.";
|
|
64
|
+
const ONBOARDING_HINT = "💰 You're set up. The fastest way to start earning sBTC is the AIBTC bounty " +
|
|
65
|
+
"board — call bounty_list to see open bounties, or earning_opportunities for " +
|
|
66
|
+
"the full menu of ways to put your assets to work.";
|
|
67
|
+
/** Resolve the hint text for a tool, or null if the tool gets no hint. */
|
|
68
|
+
function hintFor(name) {
|
|
69
|
+
if (SPEND_TOOLS.has(name))
|
|
70
|
+
return SPEND_HINT;
|
|
71
|
+
if (ONBOARDING_TOOLS.has(name))
|
|
72
|
+
return ONBOARDING_HINT;
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
function appendHint(result, hint) {
|
|
76
|
+
// Never annotate an error result — the agent should see the failure cleanly.
|
|
77
|
+
if (!result || result.isError)
|
|
78
|
+
return result;
|
|
79
|
+
const content = Array.isArray(result.content) ? result.content : [];
|
|
80
|
+
return {
|
|
81
|
+
...result,
|
|
82
|
+
content: [...content, { type: "text", text: hint }],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Monkey-patch `server.registerTool` so that any tool in SPEND_TOOLS or
|
|
87
|
+
* ONBOARDING_TOOLS has the relevant bounty hint appended to its (successful)
|
|
88
|
+
* output. Must be called BEFORE registering the tools. No-op for every other tool.
|
|
89
|
+
*
|
|
90
|
+
* Returns a cleanup function that restores the original method, mirroring
|
|
91
|
+
* `withSkillMeta` in tools/index.ts — call it once registration is done.
|
|
92
|
+
*/
|
|
93
|
+
export function installBountyHint(server) {
|
|
94
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
95
|
+
const original = server.registerTool;
|
|
96
|
+
const hasOwn = Object.prototype.hasOwnProperty.call(server, "registerTool");
|
|
97
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
98
|
+
server.registerTool = function (name, config, cb) {
|
|
99
|
+
const hint = hintFor(name);
|
|
100
|
+
if (!hint) {
|
|
101
|
+
return original.call(server, name, config, cb);
|
|
102
|
+
}
|
|
103
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
104
|
+
const wrapped = async (...args) => {
|
|
105
|
+
const result = await cb(...args);
|
|
106
|
+
return appendHint(result, hint);
|
|
107
|
+
};
|
|
108
|
+
return original.call(server, name, config, wrapped);
|
|
109
|
+
};
|
|
110
|
+
return () => {
|
|
111
|
+
if (hasOwn) {
|
|
112
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
113
|
+
server.registerTool = original;
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
117
|
+
delete server.registerTool;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=bounty-hint.js.map
|
|
@@ -11,9 +11,63 @@ import { createJsonResponse, createErrorResponse } from "../utils/index.js";
|
|
|
11
11
|
const DUAL_STACKING_ADDRESS = "SP1HFCRKEJ8BYW4D0E3FAWHFDX8A25PPAA83HWWZ9";
|
|
12
12
|
const DUAL_STACKING_CONTRACT = "dual-stacking-v2_0_4";
|
|
13
13
|
const DUAL_STACKING_CONTRACT_ID = `${DUAL_STACKING_ADDRESS}.${DUAL_STACKING_CONTRACT}`;
|
|
14
|
+
/** APR fields are fixed-point, scaled by 1e6 (500000 => 0.5%). */
|
|
15
|
+
const APR_DIVISOR = 1_000_000;
|
|
14
16
|
// ---------------------------------------------------------------------------
|
|
15
17
|
// Helpers
|
|
16
18
|
// ---------------------------------------------------------------------------
|
|
19
|
+
/**
|
|
20
|
+
* Unwrap a `cvToJSON` tuple into its field map.
|
|
21
|
+
*
|
|
22
|
+
* cvToJSON renders a tuple as `{ type: "(tuple ...)", value: { FIELD: { type, value } } }`
|
|
23
|
+
* — the fields sit one level down under `.value`, keyed by their on-wire Clarity
|
|
24
|
+
* name. Reading them off the top-level object silently yields `undefined` for
|
|
25
|
+
* every field, which is how #611 turned real APR/cycle data into zeros.
|
|
26
|
+
*/
|
|
27
|
+
function tupleFields(raw) {
|
|
28
|
+
if (!raw || typeof raw !== "object")
|
|
29
|
+
return null;
|
|
30
|
+
const inner = raw.value;
|
|
31
|
+
if (!inner || typeof inner !== "object")
|
|
32
|
+
return null;
|
|
33
|
+
return inner;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Read a uint field out of a tuple field map. Returns null and records the key
|
|
37
|
+
* in `missing` when absent or non-numeric, so a decode miss surfaces as a
|
|
38
|
+
* warning instead of a plausible-looking zero.
|
|
39
|
+
*/
|
|
40
|
+
function tupleUint(fields, key, missing) {
|
|
41
|
+
const raw = fields?.[key]?.value;
|
|
42
|
+
if (raw === undefined || raw === null) {
|
|
43
|
+
missing.push(key);
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
const num = Number(raw);
|
|
47
|
+
if (!Number.isFinite(num)) {
|
|
48
|
+
missing.push(key);
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
return num;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Unwrap a `cvToJSON` optional. `(some u123)` renders as
|
|
55
|
+
* `{ type: "(optional uint)", value: { type: "uint", value: "123" } }` and
|
|
56
|
+
* `none` as a null/undefined `.value`, so the payload needs one extra hop.
|
|
57
|
+
*/
|
|
58
|
+
function optionalUint(raw) {
|
|
59
|
+
if (!raw || typeof raw !== "object")
|
|
60
|
+
return null;
|
|
61
|
+
const inner = raw.value;
|
|
62
|
+
if (inner === undefined || inner === null)
|
|
63
|
+
return null;
|
|
64
|
+
// `some` wraps the payload in another {type, value} envelope.
|
|
65
|
+
const scalar = typeof inner === "object" ? inner.value : inner;
|
|
66
|
+
if (scalar === undefined || scalar === null)
|
|
67
|
+
return null;
|
|
68
|
+
const num = Number(scalar);
|
|
69
|
+
return Number.isFinite(num) ? num : null;
|
|
70
|
+
}
|
|
17
71
|
/**
|
|
18
72
|
* Call a read-only function on the dual-stacking contract and return a JSON-friendly value.
|
|
19
73
|
*/
|
|
@@ -91,41 +145,53 @@ Note: Dual Stacking is only available on mainnet.`,
|
|
|
91
145
|
return null;
|
|
92
146
|
});
|
|
93
147
|
const [enrolledThisCycleRaw, enrolledNextCycleRaw, minimumAmountRaw, aprDataRaw, cycleOverviewRaw,] = values;
|
|
94
|
-
// Parse APR data —
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
const
|
|
104
|
-
const
|
|
148
|
+
// Parse APR data — tuple {MAX_APR: uint, MIN_APR: uint, MULTIPLIER: uint}.
|
|
149
|
+
// Keys are the uppercase on-wire Clarity names, not the camelCase output
|
|
150
|
+
// names. A field that fails to decode stays null and is named in
|
|
151
|
+
// `warnings` — a zero here reads as "0% APR, don't enroll" (#611).
|
|
152
|
+
let apr = null;
|
|
153
|
+
if (aprDataRaw) {
|
|
154
|
+
const missing = [];
|
|
155
|
+
const fields = tupleFields(aprDataRaw);
|
|
156
|
+
const minApr = tupleUint(fields, "MIN_APR", missing);
|
|
157
|
+
const maxApr = tupleUint(fields, "MAX_APR", missing);
|
|
158
|
+
const multiplier = tupleUint(fields, "MULTIPLIER", missing);
|
|
159
|
+
if (missing.length > 0) {
|
|
160
|
+
warnings.push(`get-apr-data: could not decode tuple field(s): ${missing.join(", ")}`);
|
|
161
|
+
}
|
|
105
162
|
apr = {
|
|
106
|
-
minApr:
|
|
107
|
-
maxApr:
|
|
163
|
+
minApr: minApr === null ? null : minApr / APR_DIVISOR,
|
|
164
|
+
maxApr: maxApr === null ? null : maxApr / APR_DIVISOR,
|
|
165
|
+
multiplier,
|
|
108
166
|
unit: "%",
|
|
109
|
-
note:
|
|
167
|
+
note: `Multiplier up to ${multiplier ?? 10}x with stacked STX`,
|
|
110
168
|
};
|
|
111
169
|
}
|
|
112
|
-
// Parse cycle overview —
|
|
113
|
-
let cycleOverview =
|
|
114
|
-
if (cycleOverviewRaw
|
|
115
|
-
const
|
|
170
|
+
// Parse cycle overview — tuple {cycle-id, snapshot-index, snapshots-per-cycle}
|
|
171
|
+
let cycleOverview = null;
|
|
172
|
+
if (cycleOverviewRaw) {
|
|
173
|
+
const missing = [];
|
|
174
|
+
const fields = tupleFields(cycleOverviewRaw);
|
|
116
175
|
cycleOverview = {
|
|
117
|
-
currentCycleId:
|
|
118
|
-
snapshotIndex:
|
|
119
|
-
snapshotsPerCycle:
|
|
120
|
-
? Number(co["snapshots-per-cycle"].value)
|
|
121
|
-
: 0,
|
|
176
|
+
currentCycleId: tupleUint(fields, "cycle-id", missing),
|
|
177
|
+
snapshotIndex: tupleUint(fields, "snapshot-index", missing),
|
|
178
|
+
snapshotsPerCycle: tupleUint(fields, "snapshots-per-cycle", missing),
|
|
122
179
|
};
|
|
180
|
+
if (missing.length > 0) {
|
|
181
|
+
warnings.push(`current-overview-data: could not decode tuple field(s): ${missing.join(", ")}`);
|
|
182
|
+
}
|
|
123
183
|
}
|
|
124
|
-
// Parse minimum enrollment amount
|
|
125
|
-
let minimumEnrollmentSats =
|
|
184
|
+
// Parse minimum enrollment amount (plain uint — one {type, value} level)
|
|
185
|
+
let minimumEnrollmentSats = null;
|
|
126
186
|
if (minimumAmountRaw !== null && minimumAmountRaw !== undefined) {
|
|
127
|
-
const raw = minimumAmountRaw;
|
|
128
|
-
|
|
187
|
+
const raw = minimumAmountRaw.value;
|
|
188
|
+
const num = Number(raw);
|
|
189
|
+
if (raw === undefined || raw === null || !Number.isFinite(num)) {
|
|
190
|
+
warnings.push("get-minimum-enrollment-amount: could not decode uint result");
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
minimumEnrollmentSats = num;
|
|
194
|
+
}
|
|
129
195
|
}
|
|
130
196
|
// Parse boolean enrollment flags. A failed read stays null (unknown)
|
|
131
197
|
// rather than collapsing to a misleading `false`.
|
|
@@ -192,12 +258,10 @@ Note: Dual Stacking is only available on mainnet.`,
|
|
|
192
258
|
uintCV(BigInt(rollbackValue)),
|
|
193
259
|
principalCV(resolvedAddress),
|
|
194
260
|
]);
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
200
|
-
const rewardBtc = rewardSats / 100_000_000;
|
|
261
|
+
// Contract returns `(optional uint)` — `none` means no reward recorded
|
|
262
|
+
// for this cycle/address, which is distinct from a zero reward.
|
|
263
|
+
const rewardSats = optionalUint(rewardRaw);
|
|
264
|
+
const rewardBtc = rewardSats === null ? null : rewardSats / 100_000_000;
|
|
201
265
|
return createJsonResponse({
|
|
202
266
|
address: resolvedAddress,
|
|
203
267
|
cycle,
|
|
@@ -60,6 +60,9 @@ function buildCallWith(options) {
|
|
|
60
60
|
callWith.params = options.params;
|
|
61
61
|
if (options.data && Object.keys(options.data).length > 0)
|
|
62
62
|
callWith.data = options.data;
|
|
63
|
+
// Echo the asset so executing the quote pays in the asset that was quoted.
|
|
64
|
+
if (options.asset)
|
|
65
|
+
callWith.asset = options.asset;
|
|
63
66
|
return callWith;
|
|
64
67
|
}
|
|
65
68
|
/**
|
|
@@ -98,18 +101,29 @@ function formatProbeResponse(result, method, fullUrl, callWithOptions, messagePr
|
|
|
98
101
|
response: result.data,
|
|
99
102
|
});
|
|
100
103
|
}
|
|
104
|
+
// Derive the message from the same asset the machine-readable block reports.
|
|
105
|
+
// These used to disagree — the text said "29 STX" while payment.asset was
|
|
106
|
+
// USDCx — because the formatter treated every unknown token as STX (#613).
|
|
101
107
|
const formattedCost = formatPaymentAmount(result.amount, result.asset);
|
|
102
108
|
const prefix = messagePrefix ?? 'No payment made. ';
|
|
109
|
+
const alternatives = (result.accepts ?? []).filter((o) => o.asset !== result.asset);
|
|
110
|
+
const altNote = alternatives.length > 0
|
|
111
|
+
? ` This endpoint also accepts ${alternatives
|
|
112
|
+
.map((o) => `${o.formatted}${o.payable ? '' : ' [not payable by this client]'}`)
|
|
113
|
+
.join(', ')} — pass the \`asset\` parameter to pay with one of those instead.`
|
|
114
|
+
: '';
|
|
103
115
|
return createJsonResponse({
|
|
104
116
|
type: 'payment_required',
|
|
105
117
|
endpoint: `${method} ${fullUrl}`,
|
|
106
|
-
message: `${prefix}This endpoint costs ${formattedCost}. To execute and pay, call execute_x402_endpoint with autoApprove: true and the parameters shown in callWith below
|
|
118
|
+
message: `${prefix}This endpoint costs ${formattedCost}. To execute and pay, call execute_x402_endpoint with autoApprove: true and the parameters shown in callWith below.${altNote}`,
|
|
107
119
|
payment: {
|
|
108
120
|
amount: result.amount,
|
|
109
121
|
asset: result.asset,
|
|
122
|
+
...(result.symbol && { symbol: result.symbol }),
|
|
110
123
|
recipient: result.recipient,
|
|
111
124
|
network: result.network,
|
|
112
125
|
},
|
|
126
|
+
...(result.accepts && result.accepts.length > 0 && { accepts: result.accepts }),
|
|
113
127
|
callWith: buildCallWith(callWithOptions),
|
|
114
128
|
});
|
|
115
129
|
}
|
|
@@ -249,16 +263,20 @@ For aibtc.com inbox messages, use send_inbox_message_direct instead — it signs
|
|
|
249
263
|
.optional()
|
|
250
264
|
.default(false)
|
|
251
265
|
.describe("Skip cost probe and execute immediately. When false (default), probes first and returns cost info for paid endpoints. When true, executes atomically like before. Free endpoints always execute transparently."),
|
|
266
|
+
asset: z
|
|
267
|
+
.string()
|
|
268
|
+
.optional()
|
|
269
|
+
.describe("Which asset to pay with when the endpoint accepts several. Accepts a symbol (\"sBTC\", \"STX\") or a full contract identifier. Defaults to the first asset this client can pay. Only STX and sBTC can be signed; requesting any other asset returns an error listing what is accepted."),
|
|
252
270
|
},
|
|
253
|
-
}, async ({ method, url, path, apiUrl, params, data, autoApprove }) => {
|
|
271
|
+
}, async ({ method, url, path, apiUrl, params, data, autoApprove, asset }) => {
|
|
254
272
|
let fullUrl = "";
|
|
255
273
|
try {
|
|
256
274
|
const parsed = parseEndpointUrl({ url, path, apiUrl, params });
|
|
257
275
|
fullUrl = parsed.fullUrl;
|
|
258
276
|
params = parsed.params;
|
|
259
277
|
if (!autoApprove) {
|
|
260
|
-
const probeResult = await probeEndpoint({ method, url: fullUrl, params, data });
|
|
261
|
-
return formatProbeResponse(probeResult, method, fullUrl, { method, url, path, apiUrl, params, data });
|
|
278
|
+
const probeResult = await probeEndpoint({ method, url: fullUrl, params, data, asset });
|
|
279
|
+
return formatProbeResponse(probeResult, method, fullUrl, { method, url, path, apiUrl, params, data, asset });
|
|
262
280
|
}
|
|
263
281
|
// autoApprove=true: check dedup cache before any network request, then execute
|
|
264
282
|
// with a single request. Balance validation happens inside the onBeforePayment
|
|
@@ -275,6 +293,7 @@ For aibtc.com inbox messages, use send_inbox_message_direct instead — it signs
|
|
|
275
293
|
}
|
|
276
294
|
const api = await createApiClient(parsed.baseUrl, {
|
|
277
295
|
toolName: "execute_x402_endpoint",
|
|
296
|
+
asset,
|
|
278
297
|
onBeforePayment: async (requirements) => {
|
|
279
298
|
// Non-sponsored: sender pays its own gas, so validate STX for the
|
|
280
299
|
// fee too (sponsored=false is the default, passed explicitly here).
|
|
@@ -444,15 +463,19 @@ Supported sources:
|
|
|
444
463
|
.record(z.string(), z.unknown())
|
|
445
464
|
.optional()
|
|
446
465
|
.describe("Request body for POST/PUT requests"),
|
|
466
|
+
asset: z
|
|
467
|
+
.string()
|
|
468
|
+
.optional()
|
|
469
|
+
.describe("Quote the cost in this asset when the endpoint accepts several. Accepts a symbol (\"sBTC\", \"STX\") or a full contract identifier. Defaults to the first asset this client can pay; the full accepts[] list is always returned."),
|
|
447
470
|
},
|
|
448
|
-
}, async ({ method, url, path, apiUrl, params, data }) => {
|
|
471
|
+
}, async ({ method, url, path, apiUrl, params, data, asset }) => {
|
|
449
472
|
let fullUrl = "";
|
|
450
473
|
try {
|
|
451
474
|
const parsed = parseEndpointUrl({ url, path, apiUrl, params });
|
|
452
475
|
fullUrl = parsed.fullUrl;
|
|
453
476
|
params = parsed.params;
|
|
454
|
-
const result = await probeEndpoint({ method, url: fullUrl, params, data });
|
|
455
|
-
return formatProbeResponse(result, method, fullUrl, { method, url, path, apiUrl, params, data });
|
|
477
|
+
const result = await probeEndpoint({ method, url: fullUrl, params, data, asset });
|
|
478
|
+
return formatProbeResponse(result, method, fullUrl, { method, url, path, apiUrl, params, data, asset });
|
|
456
479
|
}
|
|
457
480
|
catch (error) {
|
|
458
481
|
return formatEndpointError(error, fullUrl || "unknown");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aibtc/mcp-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.65.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.65.0 # x-release-please-version
|
|
8
8
|
npm: "@aibtc/mcp-server"
|
|
9
9
|
github: https://github.com/aibtcdev/aibtc-mcp-server
|
|
10
10
|
---
|