@goplausible/algorand-mcp 4.2.7 → 4.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -21,7 +21,8 @@ Algorand is a carbon-negative, pure proof-of-stake Layer 1 blockchain with insta
21
21
  - TEAL compilation and disassembly
22
22
  - Full Algod and Indexer API access
23
23
  - NFDomains (NFD) name service integration
24
- - x402 and AP2 toolins for Algorand
24
+ - x402 HTTP micropayments — automatic discovery and one-call paid requests using the active wallet (USDC/ALGO)
25
+ - AP2 tooling for Algorand
25
26
  - Tinyman AMM integration (pools, swaps, liquidity)
26
27
  - Haystack Router DEX aggregation (best-price swaps across Tinyman, Pact, Folks)
27
28
  - Alpha Arcade prediction market trading (browse markets, orderbooks, limit/market orders, positions, claims)
@@ -319,6 +320,96 @@ The keychain backend is provided by [`@napi-rs/keyring`](https://github.com/nico
319
320
 
320
321
  All credentials are stored under the service name `algorand-mcp`. You can inspect them with your OS keychain app (e.g. Keychain Access on macOS).
321
322
 
323
+ ## x402 HTTP Payments
324
+
325
+ [x402](https://x402.org) is an HTTP-native micropayments protocol. It uses the long-reserved `402 Payment Required` status as a real handshake: when a client requests a paid resource without paying, the server returns 402 with a JSON body listing what it accepts (networks, assets, amounts, the recipient address). The client constructs a payment, attaches it as an HTTP header, retries the same request, and the server returns 200 with the resource. No API keys, no Stripe webhooks, no accounts to manage — payment is part of the request itself.
326
+
327
+ This MCP implements the Algorand flavor of x402, where payments are USDC (or native ALGO) transfers on Algorand. It exposes two tools that collapse the seven-step manual flow (probe → parse → opt-in check → build fee payer → build payment → group → sign → encode → header → retry) into a single tool call.
328
+
329
+ ### Protocol shape (Algorand variant)
330
+
331
+ ```
332
+ Agent (LLM) algorand-mcp Endpoint Facilitator
333
+ ────────── ──────────── ──────── ───────────
334
+ │ │ │ │
335
+ │ make_http_request_ │ │ │
336
+ │ with_x402 { url, ... } │ │ │
337
+ │ ────────────────────────► │ HTTP request │ │
338
+ │ │ ─────────────────────────► │ │
339
+ │ │ 402 PaymentRequired │ │
340
+ │ │ ◄───────────────────────── │ │
341
+ │ │ pick accepts[i] for │ │
342
+ │ │ Algorand network │ │
343
+ │ │ build fee-payer + payment │ │
344
+ │ │ (atomic group of 2) │ │
345
+ │ │ sign payment leg │ │
346
+ │ │ via OS keychain │ │
347
+ │ │ encode unsigned fee-payer │ │
348
+ │ │ base64 PAYMENT-SIGNATURE │ │
349
+ │ │ ─────────────────────────► │ │
350
+ │ │ HTTP request + │ forward + settle │
351
+ │ │ PAYMENT-SIGNATURE │ ───────────────────────► │
352
+ │ │ │ sign fee-payer, │
353
+ │ │ │ submit atomic group │
354
+ │ │ 200 + resource │ ◄─────────────────────── │
355
+ │ │ ◄───────────────────────── │ │
356
+ │ ◄─ { result, paid: {...}}│ │ │
357
+ │ │ │ │
358
+ ```
359
+
360
+ ### What's different from the Coinbase/EVM version
361
+
362
+ 1. **Header name is `PAYMENT-SIGNATURE`**, not `X-PAYMENT`. The header body is base64-encoded JSON with `x402Version`, `scheme`, `network` (a CAIP-2 identifier like `algorand:wGHE2Pw…` for mainnet), a `payload`, and a verbatim copy of the `accepts[]` entry the client chose.
363
+ 2. **Payment is an atomic 2-transaction group.** Index 0 is a fee-payer transaction (sender = facilitator, amount = 0, fee = 2000 µAlgo for the whole group); index 1 is the actual USDC ASA transfer (sender = wallet, fee = 0). The wallet signs only index 1 — the facilitator signs index 0 server-side at settlement. The user's wallet pays only the USDC, not even network fees.
364
+ 3. **Network strings are Algorand CAIP-2.** This MCP recognizes mainnet (`wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=`) and testnet (`SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=`). Endpoints that only accept Base, Solana, or other non-Algorand networks are not satisfiable here and the tool returns a clear error.
365
+
366
+ ### Coinbase Wallet MCP compatibility (x402 surface)
367
+
368
+ The x402 tools — `make_http_request_with_x402` and `x402_discover_payment_requirements` — are intentionally name- and shape-compatible with the Coinbase Wallet MCP's x402 tools. The input parameters (`baseURL`, `path`, `method`, `queryParams`, `body`, `headers`, `correlationId`, `maxAmountPerRequest`, `paymentRequirements`, `preferredNetwork`, `extensions`) are the same. The output envelope (`result`, `_atomicUnitsNote`) is the same.
369
+
370
+ What this means in practice:
371
+ - **Drop-in for Algorand x402.** Agents and MCP apps written against the Coinbase Wallet MCP's x402 tools work against this server without any prompt changes — they just hit Algorand x402 endpoints instead of Base/Solana ones.
372
+ - **Same agent reflexes.** Models trained on tool-call traces from the Coinbase ecosystem use these tools correctly on first call. Nothing to relearn.
373
+ - **Compatibility is scoped to the x402 surface only.** The wallet, account, transaction-building, and DEX tools in this MCP are Algorand-specific and do not mirror Coinbase's wallet API. Only `make_http_request_with_x402` and `x402_discover_payment_requirements` are drop-in compatible.
374
+
375
+ The one parameter that necessarily differs: `preferredNetwork` accepts `mainnet | testnet | localnet` only (Algorand networks), because the wallet only signs Algorand transactions. Coinbase's enum lists `base | base-sepolia | solana | solana-devnet`. Agents that pass one of those values get a clear error indicating no Algorand-payable accepts entry exists.
376
+
377
+ ### Example — paid weather API
378
+
379
+ ```
380
+ # Step 1 (optional): peek at the cost
381
+ x402_discover_payment_requirements {
382
+ "baseURL": "https://example.x402.goplausible.xyz",
383
+ "path": "/weather",
384
+ "method": "GET"
385
+ }
386
+ # returns: { result: { accepts: [{ scheme: "exact", network: "algorand:SGO1...",
387
+ # maxAmountRequired: "100", asset: "10458941",
388
+ # payTo: "AAAA...", extra: { feePayer: "BBBB..." } }] } }
389
+
390
+ # Step 2: pay and fetch in one call
391
+ make_http_request_with_x402 {
392
+ "baseURL": "https://example.x402.goplausible.xyz",
393
+ "path": "/weather",
394
+ "method": "GET",
395
+ "maxAmountPerRequest": 10000,
396
+ "preferredNetwork": "testnet"
397
+ }
398
+ # returns: { result: <weather payload>, paid: { network: "testnet",
399
+ # asset: "10458941",
400
+ # amount: "100", payTo: "AAAA..." },
401
+ # paymentResponse: <decoded X-PAYMENT-RESPONSE> }
402
+ ```
403
+
404
+ The active wallet account must be opted into the target ASA (e.g. USDC) and hold enough balance to cover `maxAmountRequired`. If it isn't opted in, the payment fails at settlement — opt in first with `wallet_optin_asset`.
405
+
406
+ ### Prerequisites
407
+
408
+ - An active wallet account exists (`wallet_get_info` to verify)
409
+ - That account is opted into the payment asset (USDC mainnet ASA `31566704`, testnet ASA `10458941`)
410
+ - The account has enough of the payment asset for `maxAmountRequired`
411
+ - The endpoint's `accepts[]` includes at least one entry with an Algorand network the MCP recognizes
412
+
322
413
  ## Optional Environment Variables
323
414
 
324
415
  Environment variables are only needed for special setups. Pass them via the `env` block in your MCP config.
@@ -368,6 +459,15 @@ See [Secure Wallet](#secure-wallet) for full architecture details.
368
459
  | `wallet_sign_data` | Sign arbitrary hex data with raw Ed25519 (noble, no SDK prefix) |
369
460
  | `wallet_optin_asset` | Opt the active account into an asset (creates, signs, and submits) |
370
461
 
462
+ ### x402 HTTP Payment Tools (2 tools)
463
+
464
+ See [x402 HTTP Payments](#x402-http-payments) for the full protocol explanation.
465
+
466
+ | Tool | Description |
467
+ |---|---|
468
+ | `x402_discover_payment_requirements` | Probe an x402-protected endpoint and return its `accepts[]` array (cost, asset, network, payTo) without paying. Read-only. |
469
+ | `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. |
470
+
371
471
  ### Account Management (8 tools)
372
472
 
373
473
  | Tool | Description |
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ BigInt.prototype.toJSON = function () { return Number(this); };
4
4
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
5
5
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
6
6
  import { CallToolRequestSchema, ErrorCode, ListResourcesRequestSchema, ListToolsRequestSchema, McpError, ReadResourceRequestSchema, PingRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
7
- import { AccountManager, WalletManager, UtilityManager, TransactionManager, AlgodManager, transactionTools, apiManager, handleApiManager, arc26Manager, KnowledgeManager } from './tools/index.js';
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
10
  constructor(name = 'algorand-mcp-server', version = '2.7.5') {
@@ -72,6 +72,8 @@ class AlgorandMcpServer {
72
72
  ...AccountManager.accountTools,
73
73
  // Wallet Tools
74
74
  ...WalletManager.walletTools,
75
+ // x402 Tools
76
+ ...X402Manager.x402Tools,
75
77
  // Utility Tools
76
78
  ...UtilityManager.utilityTools,
77
79
  // Algod Tools
@@ -93,6 +95,10 @@ class AlgorandMcpServer {
93
95
  if (name.startsWith('wallet_')) {
94
96
  return WalletManager.handleTool(name, args);
95
97
  }
98
+ // Handle x402 tools
99
+ if (name === 'make_http_request_with_x402' || name === 'x402_discover_payment_requirements') {
100
+ return X402Manager.handleTool(name, args);
101
+ }
96
102
  // Handle account tools
97
103
  if (name.startsWith('create_account') ||
98
104
  name.startsWith('rekey_account') ||
@@ -1,5 +1,6 @@
1
1
  export { AccountManager } from './accountManager.js';
2
2
  export { WalletManager } from './walletManager.js';
3
+ export { X402Manager } from './x402Manager.js';
3
4
  export { KnowledgeManager } from './knowledgeManager.js';
4
5
  export { arc26Manager } from './arc26Manager.js';
5
6
  export { UtilityManager } from './utilityManager.js';
@@ -1,5 +1,6 @@
1
1
  export { AccountManager } from './accountManager.js';
2
2
  export { WalletManager } from './walletManager.js';
3
+ export { X402Manager } from './x402Manager.js';
3
4
  export { KnowledgeManager } from './knowledgeManager.js';
4
5
  export { arc26Manager } from './arc26Manager.js';
5
6
  export { UtilityManager } from './utilityManager.js';
@@ -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.2.7',
89
+ version: '4.2.8',
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).',
@@ -0,0 +1,25 @@
1
+ export declare class X402Manager {
2
+ static readonly x402Tools: {
3
+ name: string;
4
+ description: string;
5
+ inputSchema: any;
6
+ }[];
7
+ static handleTool(name: string, args: Record<string, unknown>): Promise<{
8
+ content: {
9
+ type: string;
10
+ text: string;
11
+ }[];
12
+ }>;
13
+ private static handleDiscover;
14
+ private static handlePaidRequest;
15
+ private static discover;
16
+ private static selectRequirement;
17
+ private static buildPaymentHeader;
18
+ private static httpRequest;
19
+ private static buildUrl;
20
+ private static requireHttpArgs;
21
+ private static tryParseJson;
22
+ private static decodeBase64Json;
23
+ private static snippet;
24
+ private static textResponse;
25
+ }
@@ -0,0 +1,322 @@
1
+ import algosdk from 'algosdk';
2
+ import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
3
+ import { withCommonParams } from './commonParams.js';
4
+ import { getAlgodClient, extractNetwork } from '../algorand-client.js';
5
+ import { WalletManager } from './walletManager.js';
6
+ // ── CAIP-2 ↔ MCP network mapping ─────────────────────────────────────────────
7
+ const CAIP2_GENESIS_TO_NETWORK = {
8
+ 'wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=': 'mainnet',
9
+ 'SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=': 'testnet',
10
+ };
11
+ const NETWORK_TO_CAIP2 = {
12
+ mainnet: 'algorand:wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=',
13
+ testnet: 'algorand:SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=',
14
+ localnet: 'algorand:localnet',
15
+ };
16
+ function caip2ToNetwork(caip2) {
17
+ if (!caip2.startsWith('algorand:'))
18
+ return null;
19
+ const hash = caip2.slice('algorand:'.length);
20
+ return CAIP2_GENESIS_TO_NETWORK[hash] ?? null;
21
+ }
22
+ const ATOMIC_UNITS_NOTE = 'USDC amounts are expressed in atomic units. 1,000,000 atomic units = $1.00 USDC (USDC has 6 decimals).';
23
+ const HTTP_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'];
24
+ // ── Tool schemas ─────────────────────────────────────────────────────────────
25
+ const x402ToolSchemas = {
26
+ discoverPaymentRequirements: {
27
+ type: 'object',
28
+ properties: {
29
+ baseURL: { type: 'string', description: 'Base URL of the x402-protected endpoint, e.g. https://example.x402.goplausible.xyz' },
30
+ path: { type: 'string', description: 'Path under baseURL, e.g. /weather' },
31
+ method: { type: 'string', enum: HTTP_METHODS, description: 'HTTP method to probe' },
32
+ queryParams: { type: 'object', description: 'Optional query string parameters', additionalProperties: { type: 'string' } },
33
+ body: { description: 'Optional request body. Strings are sent verbatim; objects are JSON-encoded.' },
34
+ },
35
+ required: ['baseURL', 'path', 'method'],
36
+ },
37
+ makeHttpRequestWithX402: {
38
+ type: 'object',
39
+ properties: {
40
+ baseURL: { type: 'string', description: 'Base URL of the x402-protected endpoint' },
41
+ path: { type: 'string', description: 'Path under baseURL' },
42
+ method: { type: 'string', enum: HTTP_METHODS, description: 'HTTP method' },
43
+ queryParams: { type: 'object', description: 'Optional query string parameters', additionalProperties: { type: 'string' } },
44
+ body: { description: 'Optional request body. Strings are sent verbatim; objects are JSON-encoded.' },
45
+ headers: { type: 'object', description: 'Optional additional request headers', additionalProperties: { type: 'string' } },
46
+ correlationId: { type: 'string', description: 'Optional correlation id; forwarded as X-Correlation-ID header' },
47
+ 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: { type: 'array', description: 'Pre-fetched accepts[] array from a prior x402_discover_payment_requirements call. If omitted, this tool discovers internally.' },
49
+ preferredNetwork: { type: 'string', enum: ['mainnet', 'testnet', 'localnet'], description: 'Preferred Algorand network. When multiple accepts entries match, this network wins. If omitted, the active wallet network is used.' },
50
+ extensions: { type: 'object', description: 'Optional pre-fetched PaymentRequired extensions (e.g. Bazaar resource details). Passed through to the response under `extensions`.' },
51
+ },
52
+ required: ['baseURL', 'path', 'method'],
53
+ },
54
+ };
55
+ // ── X402Manager ──────────────────────────────────────────────────────────────
56
+ export class X402Manager {
57
+ // ── Public dispatch ────────────────────────────────────────────────────────
58
+ static async handleTool(name, args) {
59
+ try {
60
+ switch (name) {
61
+ case 'x402_discover_payment_requirements':
62
+ return await X402Manager.handleDiscover(args);
63
+ case 'make_http_request_with_x402':
64
+ return await X402Manager.handlePaidRequest(args);
65
+ default:
66
+ throw new McpError(ErrorCode.MethodNotFound, `Unknown x402 tool: ${name}`);
67
+ }
68
+ }
69
+ catch (error) {
70
+ if (error instanceof McpError)
71
+ throw error;
72
+ throw new McpError(ErrorCode.InternalError, `x402 operation failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
73
+ }
74
+ }
75
+ // ── Tool handlers ──────────────────────────────────────────────────────────
76
+ static async handleDiscover(args) {
77
+ const { baseURL, path, method, queryParams, body } = X402Manager.requireHttpArgs(args);
78
+ const result = await X402Manager.discover({ baseURL, path, method, queryParams, body });
79
+ return X402Manager.textResponse({ result });
80
+ }
81
+ static async handlePaidRequest(args) {
82
+ const { baseURL, path, method, queryParams, body } = X402Manager.requireHttpArgs(args);
83
+ const headers = args.headers ?? {};
84
+ const correlationId = typeof args.correlationId === 'string' ? args.correlationId : undefined;
85
+ const maxAmountPerRequest = typeof args.maxAmountPerRequest === 'number' ? args.maxAmountPerRequest : undefined;
86
+ const preferredNetwork = typeof args.preferredNetwork === 'string' ? args.preferredNetwork : undefined;
87
+ const extensions = args.extensions ?? undefined;
88
+ let accepts = Array.isArray(args.paymentRequirements) ? args.paymentRequirements : undefined;
89
+ // 1. Discover if needed
90
+ if (!accepts || accepts.length === 0) {
91
+ const discovered = await X402Manager.discover({ baseURL, path, method, queryParams, body });
92
+ if (!discovered.x402 || !Array.isArray(discovered.accepts) || discovered.accepts.length === 0) {
93
+ throw new McpError(ErrorCode.InvalidRequest, `Endpoint did not return an x402 payment requirement (status ${discovered.status}). Use a non-x402 HTTP tool, or pass paymentRequirements explicitly.`);
94
+ }
95
+ accepts = discovered.accepts;
96
+ }
97
+ // 2. Select a requirement we can satisfy on Algorand
98
+ const { requirement, mcpNetwork } = X402Manager.selectRequirement(accepts, preferredNetwork, maxAmountPerRequest);
99
+ // 3. Build and sign the payment group
100
+ const paymentHeader = await X402Manager.buildPaymentHeader(requirement, mcpNetwork);
101
+ // 4. Retry with PAYMENT-SIGNATURE
102
+ const paidHeaders = { ...headers, 'PAYMENT-SIGNATURE': paymentHeader };
103
+ if (correlationId)
104
+ paidHeaders['X-Correlation-ID'] = correlationId;
105
+ const response = await X402Manager.httpRequest({ baseURL, path, method, queryParams, body, headers: paidHeaders });
106
+ if (response.status === 402) {
107
+ // Surface the server's rejection rather than retrying.
108
+ throw new McpError(ErrorCode.InvalidRequest, `Payment rejected by server: ${X402Manager.snippet(response.bodyText)}`);
109
+ }
110
+ const result = X402Manager.tryParseJson(response.bodyText) ?? response.bodyText;
111
+ const paymentResponseHeader = response.headers['x-payment-response'] ?? response.headers['X-PAYMENT-RESPONSE'];
112
+ return X402Manager.textResponse({
113
+ result,
114
+ status: response.status,
115
+ paymentResponse: paymentResponseHeader ? X402Manager.decodeBase64Json(paymentResponseHeader) : undefined,
116
+ paid: {
117
+ network: mcpNetwork,
118
+ asset: requirement.asset,
119
+ amount: requirement.maxAmountRequired,
120
+ payTo: requirement.payTo,
121
+ },
122
+ extensions,
123
+ });
124
+ }
125
+ // ── x402 protocol helpers ──────────────────────────────────────────────────
126
+ static async discover(opts) {
127
+ const response = await X402Manager.httpRequest(opts);
128
+ if (response.status !== 402) {
129
+ return { status: response.status, x402: false, rawBody: X402Manager.tryParseJson(response.bodyText) ?? response.bodyText };
130
+ }
131
+ const parsed = X402Manager.tryParseJson(response.bodyText);
132
+ if (!parsed ||
133
+ typeof parsed !== 'object' ||
134
+ typeof parsed.x402Version !== 'number' ||
135
+ !Array.isArray(parsed.accepts)) {
136
+ return { status: 402, x402: false, rawBody: parsed ?? response.bodyText };
137
+ }
138
+ const body = parsed;
139
+ return { status: 402, x402: true, accepts: body.accepts, x402Version: body.x402Version };
140
+ }
141
+ static selectRequirement(accepts, preferredNetwork, maxAmountPerRequest) {
142
+ // Map each accepts entry to its MCP network (or null if non-Algorand).
143
+ const ranked = accepts
144
+ .map(req => ({ req, mcpNetwork: caip2ToNetwork(req.network) }))
145
+ .filter((e) => e.mcpNetwork !== null);
146
+ if (ranked.length === 0) {
147
+ throw new McpError(ErrorCode.InvalidRequest, `No payment requirement is satisfiable on Algorand. Endpoint accepts networks: [${accepts.map(r => r.network).join(', ')}]`);
148
+ }
149
+ const inBudget = (req) => maxAmountPerRequest === undefined || Number(req.maxAmountRequired) <= maxAmountPerRequest;
150
+ const affordable = ranked.filter(e => inBudget(e.req));
151
+ if (affordable.length === 0) {
152
+ throw new McpError(ErrorCode.InvalidRequest, `All Algorand payment requirements exceed maxAmountPerRequest=${maxAmountPerRequest}. Cheapest: ${Math.min(...ranked.map(e => Number(e.req.maxAmountRequired)))}.`);
153
+ }
154
+ if (preferredNetwork) {
155
+ const preferred = affordable.find(e => e.mcpNetwork === preferredNetwork);
156
+ if (preferred)
157
+ return { requirement: preferred.req, mcpNetwork: preferred.mcpNetwork };
158
+ }
159
+ // No preference (or preference not matched): pick the cheapest affordable.
160
+ const chosen = affordable.reduce((a, b) => Number(a.req.maxAmountRequired) <= Number(b.req.maxAmountRequired) ? a : b);
161
+ return { requirement: chosen.req, mcpNetwork: chosen.mcpNetwork };
162
+ }
163
+ static async buildPaymentHeader(requirement, mcpNetwork) {
164
+ const feePayer = requirement.extra?.feePayer;
165
+ if (!feePayer || typeof feePayer !== 'string') {
166
+ throw new McpError(ErrorCode.InvalidRequest, 'Payment requirement is missing `extra.feePayer` (facilitator address).');
167
+ }
168
+ const amount = Number(requirement.maxAmountRequired);
169
+ if (!Number.isFinite(amount) || amount < 0) {
170
+ throw new McpError(ErrorCode.InvalidRequest, `Invalid maxAmountRequired: ${requirement.maxAmountRequired}`);
171
+ }
172
+ const account = await WalletManager.getActiveWalletAccount();
173
+ const sk = await WalletManager.getActiveWalletSecretKey();
174
+ const algodClient = getAlgodClient(mcpNetwork);
175
+ const suggestedParams = await algodClient.getTransactionParams().do();
176
+ // Fee payer covers fees for both txns in the group.
177
+ const feePayerParams = { ...suggestedParams, fee: BigInt(2000), flatFee: true };
178
+ const innerParams = { ...suggestedParams, fee: BigInt(0), flatFee: true };
179
+ const feePayerTxn = algosdk.makePaymentTxnWithSuggestedParamsFromObject({
180
+ sender: feePayer,
181
+ receiver: feePayer,
182
+ amount: 0,
183
+ suggestedParams: feePayerParams,
184
+ });
185
+ const isAlgo = requirement.asset === '0' || requirement.asset === '';
186
+ const paymentTxn = isAlgo
187
+ ? algosdk.makePaymentTxnWithSuggestedParamsFromObject({
188
+ sender: account.address,
189
+ receiver: requirement.payTo,
190
+ amount,
191
+ suggestedParams: innerParams,
192
+ })
193
+ : algosdk.makeAssetTransferTxnWithSuggestedParamsFromObject({
194
+ sender: account.address,
195
+ receiver: requirement.payTo,
196
+ assetIndex: Number(requirement.asset),
197
+ amount,
198
+ suggestedParams: innerParams,
199
+ });
200
+ const grouped = algosdk.assignGroupID([feePayerTxn, paymentTxn]);
201
+ const groupedFeePayer = grouped[0];
202
+ const groupedPayment = grouped[1];
203
+ const signedPayment = algosdk.signTransaction(groupedPayment, sk);
204
+ const unsignedFeePayer = algosdk.encodeUnsignedTransaction(groupedFeePayer);
205
+ const headerBody = {
206
+ x402Version: 2,
207
+ scheme: requirement.scheme,
208
+ network: requirement.network,
209
+ payload: {
210
+ paymentGroup: [
211
+ algosdk.bytesToBase64(unsignedFeePayer),
212
+ algosdk.bytesToBase64(signedPayment.blob),
213
+ ],
214
+ paymentIndex: 1,
215
+ },
216
+ accepted: requirement,
217
+ };
218
+ return Buffer.from(JSON.stringify(headerBody), 'utf8').toString('base64');
219
+ }
220
+ // ── HTTP helpers ───────────────────────────────────────────────────────────
221
+ static async httpRequest(opts) {
222
+ var _a;
223
+ const url = X402Manager.buildUrl(opts.baseURL, opts.path, opts.queryParams);
224
+ const init = { method: opts.method, headers: { ...(opts.headers ?? {}) } };
225
+ if (opts.body !== undefined && opts.method !== 'GET') {
226
+ if (typeof opts.body === 'string') {
227
+ init.body = opts.body;
228
+ }
229
+ else {
230
+ init.body = JSON.stringify(opts.body);
231
+ (_a = init.headers)['content-type'] ?? (_a['content-type'] = 'application/json');
232
+ }
233
+ }
234
+ const response = await fetch(url, init);
235
+ const bodyText = await response.text();
236
+ const headers = {};
237
+ response.headers.forEach((value, key) => { headers[key.toLowerCase()] = value; });
238
+ return { status: response.status, bodyText, headers };
239
+ }
240
+ static buildUrl(baseURL, path, queryParams) {
241
+ const base = baseURL.endsWith('/') ? baseURL.slice(0, -1) : baseURL;
242
+ const tail = path.startsWith('/') ? path : `/${path}`;
243
+ const url = new URL(base + tail);
244
+ if (queryParams) {
245
+ for (const [k, v] of Object.entries(queryParams))
246
+ url.searchParams.set(k, String(v));
247
+ }
248
+ return url.toString();
249
+ }
250
+ // ── Misc helpers ───────────────────────────────────────────────────────────
251
+ static requireHttpArgs(args) {
252
+ const baseURL = typeof args.baseURL === 'string' ? args.baseURL : null;
253
+ const path = typeof args.path === 'string' ? args.path : null;
254
+ const method = typeof args.method === 'string' ? args.method.toUpperCase() : null;
255
+ if (!baseURL)
256
+ throw new McpError(ErrorCode.InvalidParams, 'baseURL (string) is required');
257
+ if (!path)
258
+ throw new McpError(ErrorCode.InvalidParams, 'path (string) is required');
259
+ if (!method || !HTTP_METHODS.includes(method)) {
260
+ throw new McpError(ErrorCode.InvalidParams, `method must be one of: ${HTTP_METHODS.join(', ')}`);
261
+ }
262
+ try {
263
+ new URL(baseURL);
264
+ }
265
+ catch {
266
+ throw new McpError(ErrorCode.InvalidParams, `baseURL is not a valid URL: ${baseURL}`);
267
+ }
268
+ return {
269
+ baseURL,
270
+ path,
271
+ method: method,
272
+ queryParams: args.queryParams,
273
+ body: args.body,
274
+ };
275
+ }
276
+ static tryParseJson(text) {
277
+ try {
278
+ return JSON.parse(text);
279
+ }
280
+ catch {
281
+ return null;
282
+ }
283
+ }
284
+ static decodeBase64Json(b64) {
285
+ try {
286
+ return JSON.parse(Buffer.from(b64, 'base64').toString('utf8'));
287
+ }
288
+ catch {
289
+ return b64;
290
+ }
291
+ }
292
+ static snippet(text, max = 300) {
293
+ return text.length <= max ? text : `${text.slice(0, max)}…`;
294
+ }
295
+ static textResponse(payload) {
296
+ return {
297
+ content: [
298
+ {
299
+ type: 'text',
300
+ text: JSON.stringify({ ...payload, _atomicUnitsNote: ATOMIC_UNITS_NOTE }, null, 2),
301
+ },
302
+ ],
303
+ };
304
+ }
305
+ }
306
+ X402Manager.x402Tools = [
307
+ {
308
+ name: 'x402_discover_payment_requirements',
309
+ description: 'Probe an x402-protected HTTP endpoint and return its payment requirements (the `accepts` array from the 402 response) without paying. Use this to inspect cost and supported networks/assets before calling `make_http_request_with_x402`.',
310
+ inputSchema: withCommonParams(x402ToolSchemas.discoverPaymentRequirements),
311
+ },
312
+ {
313
+ name: 'make_http_request_with_x402',
314
+ 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.',
315
+ inputSchema: withCommonParams(x402ToolSchemas.makeHttpRequestWithX402),
316
+ },
317
+ ];
318
+ // extractNetwork is imported above but the x402 tools do not currently use the
319
+ // generic `network` arg — they derive the working network from the chosen
320
+ // payment requirement's CAIP-2 identifier instead. The import remains for
321
+ // future use (e.g. localnet discovery against a private facilitator).
322
+ void extractNetwork;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goplausible/algorand-mcp",
3
- "version": "4.2.7",
3
+ "version": "4.2.8",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },