@goplausible/algorand-mcp 4.3.2 → 4.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/dist/index.js +5 -3
- package/dist/tools/utilityManager.js +1 -1
- package/dist/tools/x402Manager.d.ts +9 -0
- package/dist/tools/x402Manager.js +313 -3
- package/dist/types.d.ts +4 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -481,7 +481,7 @@ See [Secure Wallet](#secure-wallet) for full architecture details.
|
|
|
481
481
|
| `wallet_sign_data` | Sign arbitrary hex data with raw Ed25519 (noble, no SDK prefix) |
|
|
482
482
|
| `wallet_optin_asset` | Opt the active account into an asset (creates, signs, and submits) |
|
|
483
483
|
|
|
484
|
-
### x402 HTTP Payment Tools (
|
|
484
|
+
### x402 HTTP Payment Tools (5 tools)
|
|
485
485
|
|
|
486
486
|
See [x402 HTTP Payments](#x402-http-payments) for the full protocol explanation.
|
|
487
487
|
|
|
@@ -489,6 +489,9 @@ See [x402 HTTP Payments](#x402-http-payments) for the full protocol explanation.
|
|
|
489
489
|
|---|---|
|
|
490
490
|
| `x402_discover_payment_requirements` | Probe an x402-protected endpoint and return its `accepts[]` array (cost, asset, network, payTo) without paying. Read-only. |
|
|
491
491
|
| `make_http_request_with_x402` | Call an x402-protected endpoint with automatic USDC/ALGO payment from the active wallet. Discovers internally if `paymentRequirements` is not supplied, builds the atomic fee-payer + payment group, signs, and retries with the `PAYMENT-SIGNATURE` header. |
|
|
492
|
+
| `bazaar_list` | Browse paid API resources cataloged in the Bazaar discovery directory hosted by the configured facilitator (`facilitator.goplausible.xyz` by default). Compact summary by default; `full: true` returns verbatim records. Filters: `network`, `method`, `merchantId`, `limit`, `offset`. |
|
|
493
|
+
| `bazaar_search` | Keyword search over Bazaar resources (URL + description). Server-side: `query`, `network`. Client-side post-filters: `scheme`, `maxUsdPrice`, `asset`, `payTo`, `extensions`, `includeTestnets`. |
|
|
494
|
+
| `bazaar_get_resource_details` | Fetch a single Bazaar resource by its exact `resource` URL. Returns the verbatim record (accepts[], discoveryInfo, popularity counters). |
|
|
492
495
|
|
|
493
496
|
### Account Management (8 tools)
|
|
494
497
|
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import { CallToolRequestSchema, ErrorCode, ListResourcesRequestSchema, ListTools
|
|
|
7
7
|
import { AccountManager, WalletManager, X402Manager, UtilityManager, TransactionManager, AlgodManager, transactionTools, apiManager, handleApiManager, arc26Manager, KnowledgeManager } from './tools/index.js';
|
|
8
8
|
import { ResourceManager } from './resources/index.js';
|
|
9
9
|
class AlgorandMcpServer {
|
|
10
|
-
constructor(name = 'algorand-mcp-server', version = '
|
|
10
|
+
constructor(name = 'algorand-mcp-server', version = '4.4.0') {
|
|
11
11
|
this.name = name;
|
|
12
12
|
this.server = new Server({
|
|
13
13
|
name,
|
|
@@ -95,8 +95,10 @@ class AlgorandMcpServer {
|
|
|
95
95
|
if (name.startsWith('wallet_')) {
|
|
96
96
|
return WalletManager.handleTool(name, args);
|
|
97
97
|
}
|
|
98
|
-
// Handle x402 tools
|
|
99
|
-
if (name === 'make_http_request_with_x402' ||
|
|
98
|
+
// Handle x402 tools (payment) + bazaar_* (discovery directory)
|
|
99
|
+
if (name === 'make_http_request_with_x402' ||
|
|
100
|
+
name === 'x402_discover_payment_requirements' ||
|
|
101
|
+
name.startsWith('bazaar_')) {
|
|
100
102
|
return X402Manager.handleTool(name, args);
|
|
101
103
|
}
|
|
102
104
|
// Handle account tools
|
|
@@ -86,7 +86,7 @@ export class UtilityManager {
|
|
|
86
86
|
type: 'text',
|
|
87
87
|
text: JSON.stringify({
|
|
88
88
|
name: 'Algorand MCP Server',
|
|
89
|
-
version: '4.
|
|
89
|
+
version: '4.4.0',
|
|
90
90
|
builder: 'GoPlausible',
|
|
91
91
|
description: 'A Model Context Protocol (MCP) server providing comprehensive access to the Algorand blockchain. Supports account management, transaction building and signing, smart contract interaction, asset operations, ARC-26 URI generation, and deep integration with Algorand ecosystem services including NFDomains, Tinyman, Vestige, and Ultrade.',
|
|
92
92
|
blockchain: 'Algorand — a carbon-negative, pure proof-of-stake Layer 1 blockchain delivering instant finality, low fees, and advanced smart contract capabilities via AVM (Algorand Virtual Machine).',
|
|
@@ -31,4 +31,13 @@ export declare class X402Manager {
|
|
|
31
31
|
private static decodeBase64Json;
|
|
32
32
|
private static snippet;
|
|
33
33
|
private static textResponse;
|
|
34
|
+
private static handleBazaarList;
|
|
35
|
+
private static handleBazaarSearch;
|
|
36
|
+
private static handleBazaarGetResourceDetails;
|
|
37
|
+
private static bazaarRequest;
|
|
38
|
+
private static summarizeBazaarItem;
|
|
39
|
+
/** USDC has 6 decimals; assume non-USDC assets are USDC-like for ballpark filtering. */
|
|
40
|
+
private static usdPriceOfAccept;
|
|
41
|
+
private static cheapestUsdPrice;
|
|
42
|
+
private static isTestnetNetwork;
|
|
34
43
|
}
|
|
@@ -19,6 +19,31 @@ function caip2ToNetwork(caip2) {
|
|
|
19
19
|
const hash = caip2.slice('algorand:'.length);
|
|
20
20
|
return CAIP2_GENESIS_TO_NETWORK[hash] ?? null;
|
|
21
21
|
}
|
|
22
|
+
// ── Bazaar (discovery) config ────────────────────────────────────────────────
|
|
23
|
+
const BAZAAR_BASE_URL = process.env.BAZAAR_BASE_URL || 'https://facilitator.goplausible.xyz';
|
|
24
|
+
/**
|
|
25
|
+
* Friendly network name → CAIP-2 identifier expected by the Bazaar API.
|
|
26
|
+
* Anything starting with a known chain prefix (algorand:, eip155:, solana:) is
|
|
27
|
+
* passed through unchanged. Anything else is left as-is so the agent can
|
|
28
|
+
* still forward arbitrary upstream-supported network strings.
|
|
29
|
+
*/
|
|
30
|
+
function friendlyToCaip2Network(input) {
|
|
31
|
+
if (!input)
|
|
32
|
+
return input;
|
|
33
|
+
if (input.startsWith('algorand:') || input.startsWith('eip155:') || input.startsWith('solana:')) {
|
|
34
|
+
return input;
|
|
35
|
+
}
|
|
36
|
+
// Algorand friendly aliases.
|
|
37
|
+
if (input === 'algorand-mainnet' || input === 'mainnet')
|
|
38
|
+
return NETWORK_TO_CAIP2.mainnet;
|
|
39
|
+
if (input === 'algorand-testnet' || input === 'testnet')
|
|
40
|
+
return NETWORK_TO_CAIP2.testnet;
|
|
41
|
+
if (input === 'algorand-localnet' || input === 'localnet')
|
|
42
|
+
return NETWORK_TO_CAIP2.localnet;
|
|
43
|
+
// Pass through for non-Algorand short names — the Bazaar may or may not
|
|
44
|
+
// recognize them; if not, it returns an empty items[] which is fine.
|
|
45
|
+
return input;
|
|
46
|
+
}
|
|
22
47
|
const ATOMIC_UNITS_NOTE = 'USDC amounts are expressed in atomic units. 1,000,000 atomic units = $1.00 USDC (USDC has 6 decimals).';
|
|
23
48
|
const HTTP_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'];
|
|
24
49
|
// ── Tool schemas ─────────────────────────────────────────────────────────────
|
|
@@ -45,12 +70,67 @@ const x402ToolSchemas = {
|
|
|
45
70
|
headers: { type: 'object', description: 'Optional additional request headers', additionalProperties: { type: 'string' } },
|
|
46
71
|
correlationId: { type: 'string', description: 'Optional correlation id; forwarded as X-Correlation-ID header' },
|
|
47
72
|
maxAmountPerRequest: { type: 'integer', description: 'Maximum payment in USDC atomic units (1,000,000 = $1.00). If a payment requirement exceeds this, the call is refused.' },
|
|
48
|
-
paymentRequirements: {
|
|
49
|
-
|
|
73
|
+
paymentRequirements: {
|
|
74
|
+
type: 'array',
|
|
75
|
+
description: 'Pre-fetched accepts[] array from a prior x402_discover_payment_requirements call. If omitted, this tool discovers internally. Each element must be an OBJECT with at least { scheme, network, payTo, asset, amount }. Common mistake: passing `preferredNetwork` or `maxAmountPerRequest` as array elements instead of sibling top-level fields.',
|
|
76
|
+
items: {
|
|
77
|
+
type: 'object',
|
|
78
|
+
required: ['scheme', 'network', 'payTo', 'asset'],
|
|
79
|
+
properties: {
|
|
80
|
+
scheme: { type: 'string' },
|
|
81
|
+
network: { type: 'string', description: 'CAIP-2 network identifier, e.g. "algorand:SGO1…" for testnet.' },
|
|
82
|
+
amount: { type: 'string', description: 'x402 V2 canonical amount field (atomic units as decimal string).' },
|
|
83
|
+
maxAmountRequired: { type: 'string', description: 'x402 V1 legacy amount field. Either `amount` or `maxAmountRequired` must be present.' },
|
|
84
|
+
payTo: { type: 'string' },
|
|
85
|
+
asset: { type: 'string' },
|
|
86
|
+
maxTimeoutSeconds: { type: 'integer' },
|
|
87
|
+
extra: { type: 'object' }
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
preferredNetwork: {
|
|
92
|
+
type: 'string',
|
|
93
|
+
enum: ['mainnet', 'testnet', 'localnet'],
|
|
94
|
+
description: 'TOP-LEVEL sibling of paymentRequirements (NOT an array element). Preferred Algorand network when multiple accepts entries match. If omitted, the cheapest affordable Algorand entry is chosen.'
|
|
95
|
+
},
|
|
50
96
|
extensions: { type: 'object', description: 'Optional pre-fetched PaymentRequired extensions (e.g. Bazaar resource details). Passed through to the response under `extensions`.' },
|
|
51
97
|
},
|
|
52
98
|
required: ['baseURL', 'path', 'method'],
|
|
53
99
|
},
|
|
100
|
+
bazaarList: {
|
|
101
|
+
type: 'object',
|
|
102
|
+
properties: {
|
|
103
|
+
network: { type: 'string', description: 'Filter by network. Accepts friendly names ("algorand-mainnet", "algorand-testnet", "algorand-localnet", or bare "mainnet"/"testnet"/"localnet") or raw CAIP-2 strings ("algorand:wGHE2Pw…", "eip155:84532", "solana:EtWT…"). Friendly names are translated to CAIP-2 before the request.' },
|
|
104
|
+
method: { type: 'string', enum: HTTP_METHODS, description: 'Filter by HTTP method.' },
|
|
105
|
+
merchantId: { type: 'string', description: 'Filter by merchant id.' },
|
|
106
|
+
limit: { type: 'integer', description: 'Results per page (default 50, max 100).' },
|
|
107
|
+
offset: { type: 'integer', description: 'Pagination offset.' },
|
|
108
|
+
full: { type: 'boolean', description: 'If true, returns each item verbatim from the facilitator (large — includes full accepts[] and discoveryInfo). If false or omitted, returns a compact summary per item (URL, method, description, popularity counters, and just the Algorand-payable accepts).' },
|
|
109
|
+
},
|
|
110
|
+
required: [],
|
|
111
|
+
},
|
|
112
|
+
bazaarSearch: {
|
|
113
|
+
type: 'object',
|
|
114
|
+
properties: {
|
|
115
|
+
query: { type: 'string', description: 'Keyword to match in resource URL and description (minimum 1 char).', minLength: 1 },
|
|
116
|
+
limit: { type: 'integer', description: 'Max results to return (1–20, default 10).', minimum: 1, maximum: 20 },
|
|
117
|
+
network: { type: 'string', description: 'Filter by network (friendly name or CAIP-2). See bazaar_list for accepted values.' },
|
|
118
|
+
includeTestnets: { type: 'boolean', description: 'If true, include testnet/devnet networks in the results. Default false (mainnet-only).' },
|
|
119
|
+
scheme: { type: 'string', enum: ['exact', 'upto'], description: 'Client-side filter — only return resources whose accepts[] includes this payment scheme.' },
|
|
120
|
+
maxUsdPrice: { type: 'number', description: 'Client-side filter — exclude resources whose cheapest accepts entry exceeds this USD price (computed from amount + asset decimals). Assumes USDC pricing.', exclusiveMinimum: 0 },
|
|
121
|
+
asset: { type: 'string', description: 'Client-side filter — only return resources whose accepts[] includes this asset id.' },
|
|
122
|
+
payTo: { type: 'string', description: 'Client-side filter — only return resources whose accepts[] includes this recipient address.' },
|
|
123
|
+
extensions: { type: 'string', description: 'Client-side filter — only return resources whose discoveryInfo / extensions object contains this key (e.g. "bazaar", "outputSchema").' },
|
|
124
|
+
},
|
|
125
|
+
required: ['query'],
|
|
126
|
+
},
|
|
127
|
+
bazaarGetResourceDetails: {
|
|
128
|
+
type: 'object',
|
|
129
|
+
properties: {
|
|
130
|
+
resource: { type: 'string', description: 'Exact resource URL to fetch details for (must match the resourceUrl as registered in the Bazaar).' },
|
|
131
|
+
},
|
|
132
|
+
required: ['resource'],
|
|
133
|
+
},
|
|
54
134
|
};
|
|
55
135
|
// ── X402Manager ──────────────────────────────────────────────────────────────
|
|
56
136
|
export class X402Manager {
|
|
@@ -62,6 +142,12 @@ export class X402Manager {
|
|
|
62
142
|
return await X402Manager.handleDiscover(args);
|
|
63
143
|
case 'make_http_request_with_x402':
|
|
64
144
|
return await X402Manager.handlePaidRequest(args);
|
|
145
|
+
case 'bazaar_list':
|
|
146
|
+
return await X402Manager.handleBazaarList(args);
|
|
147
|
+
case 'bazaar_search':
|
|
148
|
+
return await X402Manager.handleBazaarSearch(args);
|
|
149
|
+
case 'bazaar_get_resource_details':
|
|
150
|
+
return await X402Manager.handleBazaarGetResourceDetails(args);
|
|
65
151
|
default:
|
|
66
152
|
throw new McpError(ErrorCode.MethodNotFound, `Unknown x402 tool: ${name}`);
|
|
67
153
|
}
|
|
@@ -86,7 +172,35 @@ export class X402Manager {
|
|
|
86
172
|
const preferredNetwork = typeof args.preferredNetwork === 'string' ? args.preferredNetwork : undefined;
|
|
87
173
|
const extensions = args.extensions ?? undefined;
|
|
88
174
|
let accepts = Array.isArray(args.paymentRequirements) ? args.paymentRequirements : undefined;
|
|
89
|
-
//
|
|
175
|
+
// 1a. Defensive validation: every paymentRequirements entry must be an
|
|
176
|
+
// object with at least { network, payTo, asset } and an amount field
|
|
177
|
+
// (either x402 V2 `amount` or V1 `maxAmountRequired`). LLMs frequently
|
|
178
|
+
// malform JSON by appending sibling field names (e.g. "preferredNetwork")
|
|
179
|
+
// as bare strings inside this array — catch that here with an actionable
|
|
180
|
+
// message rather than crashing later in selectRequirement.
|
|
181
|
+
if (accepts) {
|
|
182
|
+
for (let i = 0; i < accepts.length; i++) {
|
|
183
|
+
const entry = accepts[i];
|
|
184
|
+
const isObject = !!entry && typeof entry === 'object' && !Array.isArray(entry);
|
|
185
|
+
if (!isObject) {
|
|
186
|
+
throw new McpError(ErrorCode.InvalidParams, `paymentRequirements[${i}] must be an OBJECT (got ${entry === null ? 'null' : Array.isArray(entry) ? 'array' : typeof entry}: ${JSON.stringify(entry)}). Tip: pass the accepts[] array verbatim from x402_discover_payment_requirements. Other arguments like preferredNetwork and maxAmountPerRequest are TOP-LEVEL siblings of paymentRequirements, not array members.`);
|
|
187
|
+
}
|
|
188
|
+
const e = entry;
|
|
189
|
+
const missing = [];
|
|
190
|
+
if (typeof e.network !== 'string')
|
|
191
|
+
missing.push('network');
|
|
192
|
+
if (typeof e.payTo !== 'string')
|
|
193
|
+
missing.push('payTo');
|
|
194
|
+
if (typeof e.asset !== 'string')
|
|
195
|
+
missing.push('asset');
|
|
196
|
+
if (typeof e.amount !== 'string' && typeof e.maxAmountRequired !== 'string')
|
|
197
|
+
missing.push('amount (or maxAmountRequired)');
|
|
198
|
+
if (missing.length > 0) {
|
|
199
|
+
throw new McpError(ErrorCode.InvalidParams, `paymentRequirements[${i}] is missing required string field(s): ${missing.join(', ')}. Expected shape: { scheme, network, payTo, asset, amount, extra: { feePayer, ... } }. Pass the accepts[] entry verbatim from x402_discover_payment_requirements.`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// 1b. Discover if needed
|
|
90
204
|
if (!accepts || accepts.length === 0) {
|
|
91
205
|
const discovered = await X402Manager.discover({ baseURL, path, method, queryParams, body });
|
|
92
206
|
if (!discovered.x402 || !Array.isArray(discovered.accepts) || discovered.accepts.length === 0) {
|
|
@@ -331,6 +445,187 @@ export class X402Manager {
|
|
|
331
445
|
],
|
|
332
446
|
};
|
|
333
447
|
}
|
|
448
|
+
// ── Bazaar (discovery) handlers ────────────────────────────────────────────
|
|
449
|
+
static async handleBazaarList(args) {
|
|
450
|
+
const network = typeof args.network === 'string' ? friendlyToCaip2Network(args.network) : undefined;
|
|
451
|
+
const method = typeof args.method === 'string' ? args.method.toUpperCase() : undefined;
|
|
452
|
+
const merchantId = typeof args.merchantId === 'string' ? args.merchantId : undefined;
|
|
453
|
+
const limit = typeof args.limit === 'number' ? args.limit : undefined;
|
|
454
|
+
const offset = typeof args.offset === 'number' ? args.offset : undefined;
|
|
455
|
+
const full = args.full === true;
|
|
456
|
+
const params = {};
|
|
457
|
+
if (network)
|
|
458
|
+
params.network = network;
|
|
459
|
+
if (method)
|
|
460
|
+
params.method = method;
|
|
461
|
+
if (merchantId)
|
|
462
|
+
params.merchantId = merchantId;
|
|
463
|
+
if (limit !== undefined)
|
|
464
|
+
params.limit = String(limit);
|
|
465
|
+
if (offset !== undefined)
|
|
466
|
+
params.offset = String(offset);
|
|
467
|
+
const body = await X402Manager.bazaarRequest('/discovery/resources', params);
|
|
468
|
+
const items = Array.isArray(body?.items) ? body.items : [];
|
|
469
|
+
const pagination = body?.pagination;
|
|
470
|
+
return X402Manager.textResponse({
|
|
471
|
+
source: BAZAAR_BASE_URL,
|
|
472
|
+
pagination,
|
|
473
|
+
count: items.length,
|
|
474
|
+
items: full ? items : items.map((it) => X402Manager.summarizeBazaarItem(it)),
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
static async handleBazaarSearch(args) {
|
|
478
|
+
const query = typeof args.query === 'string' ? args.query : '';
|
|
479
|
+
if (!query || query.length < 1) {
|
|
480
|
+
throw new McpError(ErrorCode.InvalidParams, '`query` is required and must be a non-empty string.');
|
|
481
|
+
}
|
|
482
|
+
const limit = typeof args.limit === 'number' ? args.limit : 10;
|
|
483
|
+
if (limit < 1 || limit > 20) {
|
|
484
|
+
throw new McpError(ErrorCode.InvalidParams, '`limit` must be between 1 and 20.');
|
|
485
|
+
}
|
|
486
|
+
const network = typeof args.network === 'string' ? friendlyToCaip2Network(args.network) : undefined;
|
|
487
|
+
const includeTestnets = args.includeTestnets === true;
|
|
488
|
+
const scheme = typeof args.scheme === 'string' ? args.scheme : undefined;
|
|
489
|
+
const maxUsdPrice = typeof args.maxUsdPrice === 'number' ? args.maxUsdPrice : undefined;
|
|
490
|
+
const assetFilter = typeof args.asset === 'string' ? args.asset : undefined;
|
|
491
|
+
const payToFilter = typeof args.payTo === 'string' ? args.payTo : undefined;
|
|
492
|
+
const extensionsFilter = typeof args.extensions === 'string' ? args.extensions : undefined;
|
|
493
|
+
// Server-side params we can forward.
|
|
494
|
+
const params = { search: query, limit: String(Math.min(100, Math.max(limit * 5, 50))) };
|
|
495
|
+
if (network)
|
|
496
|
+
params.network = network;
|
|
497
|
+
const body = await X402Manager.bazaarRequest('/discovery/resources', params);
|
|
498
|
+
let items = Array.isArray(body?.items) ? body.items : [];
|
|
499
|
+
// Client-side filters not supported by the server.
|
|
500
|
+
if (!includeTestnets) {
|
|
501
|
+
items = items.filter((it) => Array.isArray(it?.accepts) && it.accepts.some((a) => !X402Manager.isTestnetNetwork(a?.network)));
|
|
502
|
+
}
|
|
503
|
+
if (scheme) {
|
|
504
|
+
items = items.filter((it) => Array.isArray(it?.accepts) && it.accepts.some((a) => a?.scheme === scheme));
|
|
505
|
+
}
|
|
506
|
+
if (assetFilter) {
|
|
507
|
+
items = items.filter((it) => Array.isArray(it?.accepts) && it.accepts.some((a) => String(a?.asset) === assetFilter));
|
|
508
|
+
}
|
|
509
|
+
if (payToFilter) {
|
|
510
|
+
items = items.filter((it) => Array.isArray(it?.accepts) && it.accepts.some((a) => a?.payTo === payToFilter));
|
|
511
|
+
}
|
|
512
|
+
if (maxUsdPrice !== undefined) {
|
|
513
|
+
items = items.filter((it) => {
|
|
514
|
+
const cheapest = X402Manager.cheapestUsdPrice(it?.accepts);
|
|
515
|
+
return cheapest !== null && cheapest <= maxUsdPrice;
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
if (extensionsFilter) {
|
|
519
|
+
items = items.filter((it) => {
|
|
520
|
+
const info = it?.discoveryInfo;
|
|
521
|
+
if (extensionsFilter === 'bazaar')
|
|
522
|
+
return !!info;
|
|
523
|
+
if (info && typeof info === 'object' && extensionsFilter in info)
|
|
524
|
+
return true;
|
|
525
|
+
return false;
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
items = items.slice(0, limit);
|
|
529
|
+
return X402Manager.textResponse({
|
|
530
|
+
source: BAZAAR_BASE_URL,
|
|
531
|
+
query,
|
|
532
|
+
filtersApplied: { network, includeTestnets, scheme, maxUsdPrice, asset: assetFilter, payTo: payToFilter, extensions: extensionsFilter },
|
|
533
|
+
count: items.length,
|
|
534
|
+
items: items.map((it) => X402Manager.summarizeBazaarItem(it)),
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
static async handleBazaarGetResourceDetails(args) {
|
|
538
|
+
const resource = typeof args.resource === 'string' ? args.resource : '';
|
|
539
|
+
if (!resource) {
|
|
540
|
+
throw new McpError(ErrorCode.InvalidParams, '`resource` is required (the exact resourceUrl as registered in the Bazaar).');
|
|
541
|
+
}
|
|
542
|
+
try {
|
|
543
|
+
new URL(resource);
|
|
544
|
+
}
|
|
545
|
+
catch {
|
|
546
|
+
throw new McpError(ErrorCode.InvalidParams, `\`resource\` is not a valid URL: ${resource}`);
|
|
547
|
+
}
|
|
548
|
+
// The facilitator API has no GET-by-id endpoint; we search by URL substring
|
|
549
|
+
// and exact-match on resourceUrl client-side.
|
|
550
|
+
const body = await X402Manager.bazaarRequest('/discovery/resources', { search: resource, limit: '100' });
|
|
551
|
+
const items = Array.isArray(body?.items) ? body.items : [];
|
|
552
|
+
const exact = items.find((it) => it?.resourceUrl === resource);
|
|
553
|
+
if (!exact) {
|
|
554
|
+
throw new McpError(ErrorCode.InvalidRequest, `No Bazaar resource found with resourceUrl=${resource}. The facilitator returned ${items.length} partial match(es) for the substring search.`);
|
|
555
|
+
}
|
|
556
|
+
return X402Manager.textResponse({ source: BAZAAR_BASE_URL, resource: exact });
|
|
557
|
+
}
|
|
558
|
+
// ── Bazaar helpers ─────────────────────────────────────────────────────────
|
|
559
|
+
static async bazaarRequest(path, query) {
|
|
560
|
+
const base = BAZAAR_BASE_URL.endsWith('/') ? BAZAAR_BASE_URL.slice(0, -1) : BAZAAR_BASE_URL;
|
|
561
|
+
const tail = path.startsWith('/') ? path : `/${path}`;
|
|
562
|
+
const url = new URL(base + tail);
|
|
563
|
+
for (const [k, v] of Object.entries(query))
|
|
564
|
+
url.searchParams.set(k, v);
|
|
565
|
+
const response = await fetch(url.toString(), { method: 'GET', headers: { accept: 'application/json' } });
|
|
566
|
+
const text = await response.text();
|
|
567
|
+
if (!response.ok) {
|
|
568
|
+
throw new McpError(ErrorCode.InternalError, `Bazaar request failed (${response.status} ${response.statusText}) for ${url.pathname}${url.search}: ${X402Manager.snippet(text)}`);
|
|
569
|
+
}
|
|
570
|
+
const parsed = X402Manager.tryParseJson(text);
|
|
571
|
+
if (parsed === null) {
|
|
572
|
+
throw new McpError(ErrorCode.InternalError, `Bazaar returned non-JSON body: ${X402Manager.snippet(text)}`);
|
|
573
|
+
}
|
|
574
|
+
return parsed;
|
|
575
|
+
}
|
|
576
|
+
static summarizeBazaarItem(item) {
|
|
577
|
+
const accepts = Array.isArray(item?.accepts) ? item.accepts : [];
|
|
578
|
+
const algorandAccepts = accepts
|
|
579
|
+
.filter(a => typeof a?.network === 'string' && a.network.startsWith('algorand:'))
|
|
580
|
+
.map(a => ({
|
|
581
|
+
network: a.network,
|
|
582
|
+
mcpNetwork: caip2ToNetwork(a.network) ?? null,
|
|
583
|
+
scheme: a.scheme,
|
|
584
|
+
amount: a.amount ?? a.maxAmountRequired,
|
|
585
|
+
asset: a.asset,
|
|
586
|
+
payTo: a.payTo,
|
|
587
|
+
usdPrice: X402Manager.usdPriceOfAccept(a),
|
|
588
|
+
}));
|
|
589
|
+
return {
|
|
590
|
+
resourceUrl: item?.resourceUrl,
|
|
591
|
+
method: item?.method,
|
|
592
|
+
description: item?.description,
|
|
593
|
+
merchantId: item?.merchantId,
|
|
594
|
+
algorandPayable: algorandAccepts.length > 0,
|
|
595
|
+
algorandAccepts,
|
|
596
|
+
totalAcceptedNetworks: accepts.length,
|
|
597
|
+
otherNetworks: accepts.filter(a => !algorandAccepts.find(ao => ao.network === a.network)).map(a => a?.network).filter(Boolean),
|
|
598
|
+
hasDiscoveryInfo: !!item?.discoveryInfo,
|
|
599
|
+
popularity: { verifyCount: item?.verifyCount ?? 0, settleCount: item?.settleCount ?? 0 },
|
|
600
|
+
firstSeen: item?.firstSeen,
|
|
601
|
+
lastSeen: item?.lastSeen,
|
|
602
|
+
id: item?.id,
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
/** USDC has 6 decimals; assume non-USDC assets are USDC-like for ballpark filtering. */
|
|
606
|
+
static usdPriceOfAccept(accept) {
|
|
607
|
+
const amountStr = accept?.amount ?? accept?.maxAmountRequired;
|
|
608
|
+
const decimals = typeof accept?.extra?.decimals === 'number' ? accept.extra.decimals : 6;
|
|
609
|
+
const n = Number(amountStr);
|
|
610
|
+
if (!Number.isFinite(n))
|
|
611
|
+
return null;
|
|
612
|
+
return n / Math.pow(10, decimals);
|
|
613
|
+
}
|
|
614
|
+
static cheapestUsdPrice(accepts) {
|
|
615
|
+
if (!Array.isArray(accepts) || accepts.length === 0)
|
|
616
|
+
return null;
|
|
617
|
+
const prices = accepts.map(a => X402Manager.usdPriceOfAccept(a)).filter((p) => p !== null);
|
|
618
|
+
if (prices.length === 0)
|
|
619
|
+
return null;
|
|
620
|
+
return Math.min(...prices);
|
|
621
|
+
}
|
|
622
|
+
static isTestnetNetwork(caip2) {
|
|
623
|
+
if (typeof caip2 !== 'string')
|
|
624
|
+
return false;
|
|
625
|
+
if (caip2 === NETWORK_TO_CAIP2.testnet)
|
|
626
|
+
return true;
|
|
627
|
+
return /sepolia|devnet|testnet/i.test(caip2);
|
|
628
|
+
}
|
|
334
629
|
}
|
|
335
630
|
X402Manager.x402Tools = [
|
|
336
631
|
{
|
|
@@ -343,6 +638,21 @@ X402Manager.x402Tools = [
|
|
|
343
638
|
description: 'Call an x402-protected HTTP endpoint with automatic USDC/ALGO payment from the active wallet. If `paymentRequirements` is not supplied, this tool runs discovery internally (one extra request). Builds an atomic 2-transaction group (facilitator fee-payer + wallet payment), signs the payment leg with the wallet, and resends with the `PAYMENT-SIGNATURE` header.',
|
|
344
639
|
inputSchema: withCommonParams(x402ToolSchemas.makeHttpRequestWithX402),
|
|
345
640
|
},
|
|
641
|
+
{
|
|
642
|
+
name: 'bazaar_list',
|
|
643
|
+
description: 'Browse paid API resources cataloged in the Bazaar (the discovery directory hosted by the configured facilitator). Returns a compact summary per resource by default; pass `full: true` for the verbatim facilitator response. Supports filtering by `network`, `method`, `merchantId`, plus standard `limit`/`offset` pagination.',
|
|
644
|
+
inputSchema: withCommonParams(x402ToolSchemas.bazaarList),
|
|
645
|
+
},
|
|
646
|
+
{
|
|
647
|
+
name: 'bazaar_search',
|
|
648
|
+
description: 'Search Bazaar paid API resources by keyword. Hits the same /discovery/resources endpoint as bazaar_list but with the `search` query forwarded to the facilitator. Additional filters (`scheme`, `maxUsdPrice`, `asset`, `payTo`, `extensions`, `includeTestnets`) are applied client-side after the response. Defaults to mainnet-only.',
|
|
649
|
+
inputSchema: withCommonParams(x402ToolSchemas.bazaarSearch),
|
|
650
|
+
},
|
|
651
|
+
{
|
|
652
|
+
name: 'bazaar_get_resource_details',
|
|
653
|
+
description: 'Fetch full details for a single Bazaar resource by its exact `resource` URL. Internally calls /discovery/resources?search=<url> and exact-matches against resourceUrl. Returns the verbatim resource record (accepts[], discoveryInfo / extensions, popularity counters) or an error if no match is found.',
|
|
654
|
+
inputSchema: withCommonParams(x402ToolSchemas.bazaarGetResourceDetails),
|
|
655
|
+
},
|
|
346
656
|
];
|
|
347
657
|
// extractNetwork is imported above but the x402 tools do not currently use the
|
|
348
658
|
// generic `network` arg — they derive the working network from the chosen
|
package/dist/types.d.ts
CHANGED
|
@@ -47,16 +47,16 @@ export declare const AccountDetailsSchema: z.ZodObject<{
|
|
|
47
47
|
}>, "many">;
|
|
48
48
|
authAddress: z.ZodOptional<z.ZodString>;
|
|
49
49
|
}, "strip", z.ZodTypeAny, {
|
|
50
|
-
address: string;
|
|
51
50
|
amount: number | bigint;
|
|
51
|
+
address: string;
|
|
52
52
|
assets: {
|
|
53
53
|
assetId: number | bigint;
|
|
54
54
|
amount: number | bigint;
|
|
55
55
|
}[];
|
|
56
56
|
authAddress?: string | undefined;
|
|
57
57
|
}, {
|
|
58
|
-
address: string;
|
|
59
58
|
amount: number | bigint;
|
|
59
|
+
address: string;
|
|
60
60
|
assets: {
|
|
61
61
|
assetId: number | bigint;
|
|
62
62
|
amount: number | bigint;
|
|
@@ -74,16 +74,16 @@ export declare const TransactionInfoSchema: z.ZodObject<{
|
|
|
74
74
|
timestamp: z.ZodString;
|
|
75
75
|
}, "strip", z.ZodTypeAny, {
|
|
76
76
|
sender: string;
|
|
77
|
-
type: string;
|
|
78
77
|
id: string;
|
|
78
|
+
type: string;
|
|
79
79
|
timestamp: string;
|
|
80
80
|
assetId?: number | bigint | undefined;
|
|
81
81
|
amount?: number | bigint | undefined;
|
|
82
82
|
receiver?: string | undefined;
|
|
83
83
|
}, {
|
|
84
84
|
sender: string;
|
|
85
|
-
type: string;
|
|
86
85
|
id: string;
|
|
86
|
+
type: string;
|
|
87
87
|
timestamp: string;
|
|
88
88
|
assetId?: number | bigint | undefined;
|
|
89
89
|
amount?: number | bigint | undefined;
|