@goplausible/algorand-mcp 4.2.7 → 4.2.9

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
@@ -13,7 +13,7 @@ Algorand is a carbon-negative, pure proof-of-stake Layer 1 blockchain with insta
13
13
 
14
14
  ## Features
15
15
 
16
- - Secure wallet management via OS keychain private keys never exposed to agents or LLMs
16
+ - Agent wallet mnemonics stored in a local SQLite database, used by the MCP server to sign on the agent's behalf (mnemonics never returned in tool responses)
17
17
  - Wallet accounts with human-readable nicknames
18
18
  - Account creation, key management, and rekeying
19
19
  - Transaction building, signing, and submission (payments, assets, applications, key registration)
@@ -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)
@@ -267,57 +268,148 @@ If no `network` is provided, tools default to **mainnet**.
267
268
 
268
269
  API responses are automatically paginated. Every tool accepts an optional `itemsPerPage` parameter (default: 10). Pass the `pageToken` from a previous response to fetch the next page.
269
270
 
270
- ## Secure Wallet
271
+ ## Agent Wallet
271
272
 
272
273
  ### Architecture
273
274
 
274
- The wallet system has two layers of storage, each with a distinct security role:
275
+ The agent wallet is a local SQLite database that the MCP server controls on the agent's behalf. The server holds the mnemonics and signs transactions for the agent — the agent never sees the mnemonics in any tool response.
275
276
 
276
- | Layer | What it stores | Where | Encryption |
277
- |---|---|---|---|
278
- | **OS Keychain** | Mnemonics (secret keys) | macOS Keychain / Linux libsecret / Windows Credential Manager | OS-managed, hardware-backed where available |
279
- | **Embedded SQLite** | Account metadata (nicknames, active-account index) | `~/.algorand-mcp/wallet.db` | Plaintext (no secrets) |
277
+ | Layer | What it stores | Where |
278
+ |---|---|---|
279
+ | **SQLite (`wallet.db`)** | Account rows (`address`, `public_key`, `nickname`, `mnemonic`, `created_at`) and the active-account index | `~/.algorand-mcp/wallet.db` (mode `0600`) |
280
280
 
281
- **Private key material never appears in tool responses, MCP config files, environment variables, or logs.** The agent only sees addresses, public keys, and signed transaction blobs.
281
+ **Threat model.** The wallet.db file is the secret. Anyone with read access to it can recover every mnemonic stored in the wallet. The mitigations are filesystem permissions (`0600`, owner-only), keeping the data directory off shared/world-readable volumes, and treating the data directory like any other secret store (snapshot it carefully, restrict backups, encrypt the host disk for at-rest protection). For Docker deployments, mount `~/.algorand-mcp` as a named volume and restrict access to it like you would any secret material.
282
282
 
283
283
  ### How it works
284
284
 
285
285
  ```
286
- Agent (LLM) MCP Server Storage
287
- ────────── ────────── ───────
288
- │ │
289
- │ wallet_add_account │
290
- │ { nickname: "main" } │
291
- │ ──────────────────────────► │ generate keypair
292
- │ │ store mnemonic ──────────► OS Keychain (encrypted)
293
- │ │ store metadata ──────────► │ SQLite (nickname)
294
- │ ◄─ { address, publicKey }
295
-
296
- wallet_sign_transaction │
297
- { transaction: {...} }
298
- ──────────────────────────► │ retrieve mnemonic ◄────── OS Keychain
299
- sign in memory │
300
- ◄─ { txID, blob } (key discarded)
301
- │ │
302
- ```
303
-
304
- 1. **Account creation** (`wallet_add_account`) — Generates a keypair, stores the mnemonic in the OS keychain, and stores the nickname in SQLite. Returns **only** address and public key.
286
+ Agent (LLM) MCP Server Storage
287
+ ────────── ────────── ───────
288
+ │ │
289
+ │ wallet_add_account │
290
+ │ { nickname: "main" } │
291
+ │ ──────────────────────────► │ generate keypair
292
+ │ │ INSERT (address, public_key,
293
+ │ │ nickname, mnemonic) ──►│ wallet.db
294
+ │ ◄─ { address, publicKey,
295
+ nickname, index }
296
+ │ │
297
+ wallet_sign_transaction
298
+ { transaction: {...} }
299
+ ──────────────────────────► SELECT mnemonic FROM accounts ◄─│
300
+ WHERE address=<active>
301
+ │ │ sign in memory
302
+ │ ◄─ { txID, blob } │ (key discarded after sign) │
303
+ │ │ │
304
+ ```
305
+
306
+ 1. **Account creation** (`wallet_add_account`) — Generates a keypair and inserts a row containing the mnemonic into `accounts`. Returns address, public key, nickname, and index. The mnemonic is never returned.
305
307
  2. **Active account** — One account is active at a time. `wallet_switch_account` changes it by nickname or index. All signing and query tools operate on the active account.
306
- 3. **Transaction signing** (`wallet_sign_transaction`) — Retrieves the key from the keychain, signs in memory, discards the key. Returns only the signed blob.
308
+ 3. **Transaction signing** (`wallet_sign_transaction`) — Reads the mnemonic from the DB, signs in memory, returns only the signed blob.
307
309
  4. **Data signing** (`wallet_sign_data`) — Signs arbitrary hex data using raw Ed25519 via the [`@noble/curves`](https://github.com/paulmillr/noble-curves) library (no Algorand SDK prefix). Useful for off-chain authentication.
308
310
  5. **Asset opt-in** (`wallet_optin_asset`) — Creates, signs, and submits an opt-in transaction for the active account in one step.
309
311
 
310
- ### Platform keychain support
312
+ ### Backward compatibility (silent migration from OS keychain)
311
313
 
312
- The keychain backend is provided by [`@napi-rs/keyring`](https://github.com/nicolo-ribaudo/napi-keyring) (Rust-based, prebuilt binaries):
314
+ Older installs of this MCP stored mnemonics in the OS keychain (`@napi-rs/keyring`). On first startup after upgrading, the server runs a one-shot, silent migration:
313
315
 
314
- | Platform | Backend |
315
- |---|---|
316
- | macOS | Keychain Services (the system Keychain app) |
317
- | Linux | libsecret (GNOME Keyring) or KWallet |
318
- | Windows | Windows Credential Manager |
316
+ - For every `accounts` row whose `mnemonic` column is `NULL` or empty, it attempts to read the mnemonic from the OS keychain under the service name `algorand-mcp` keyed by the address.
317
+ - If found, the mnemonic is copied into the DB column.
318
+ - The original keychain entry is left in place as a redundant backup; nothing is deleted.
319
+
320
+ After this completes, the DB is the sole source of truth. The keychain is consulted only as a fallback if the DB still has a `NULL` mnemonic for an address (e.g., the keychain was unavailable during startup and became available later). All new accounts created after the upgrade are written directly to the DB and never touch the keychain.
321
+
322
+ No user action is required. No prompts, no env vars, no migration logs.
323
+
324
+ ## x402 HTTP Payments
325
+
326
+ [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.
327
+
328
+ 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.
329
+
330
+ ### Protocol shape (Algorand variant)
331
+
332
+ ```
333
+ Agent (LLM) algorand-mcp Endpoint Facilitator
334
+ ────────── ──────────── ──────── ───────────
335
+ │ │ │ │
336
+ │ make_http_request_ │ │ │
337
+ │ with_x402 { url, ... } │ │ │
338
+ │ ────────────────────────► │ HTTP request │ │
339
+ │ │ ─────────────────────────► │ │
340
+ │ │ 402 PaymentRequired │ │
341
+ │ │ ◄───────────────────────── │ │
342
+ │ │ pick accepts[i] for │ │
343
+ │ │ Algorand network │ │
344
+ │ │ build fee-payer + payment │ │
345
+ │ │ (atomic group of 2) │ │
346
+ │ │ sign payment leg │ │
347
+ │ │ via agent wallet DB │ │
348
+ │ │ encode unsigned fee-payer │ │
349
+ │ │ base64 PAYMENT-SIGNATURE │ │
350
+ │ │ ─────────────────────────► │ │
351
+ │ │ HTTP request + │ forward + settle │
352
+ │ │ PAYMENT-SIGNATURE │ ───────────────────────► │
353
+ │ │ │ sign fee-payer, │
354
+ │ │ │ submit atomic group │
355
+ │ │ 200 + resource │ ◄─────────────────────── │
356
+ │ │ ◄───────────────────────── │ │
357
+ │ ◄─ { result, paid: {...}}│ │ │
358
+ │ │ │ │
359
+ ```
360
+
361
+ ### What's different from the Coinbase/EVM version
362
+
363
+ 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.
364
+ 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.
365
+ 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.
366
+
367
+ ### Coinbase Wallet MCP compatibility (x402 surface)
368
+
369
+ 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.
319
370
 
320
- 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).
371
+ What this means in practice:
372
+ - **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.
373
+ - **Same agent reflexes.** Models trained on tool-call traces from the Coinbase ecosystem use these tools correctly on first call. Nothing to relearn.
374
+ - **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.
375
+
376
+ 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.
377
+
378
+ ### Example — paid weather API
379
+
380
+ ```
381
+ # Step 1 (optional): peek at the cost
382
+ x402_discover_payment_requirements {
383
+ "baseURL": "https://example.x402.goplausible.xyz",
384
+ "path": "/weather",
385
+ "method": "GET"
386
+ }
387
+ # returns: { result: { accepts: [{ scheme: "exact", network: "algorand:SGO1...",
388
+ # maxAmountRequired: "100", asset: "10458941",
389
+ # payTo: "AAAA...", extra: { feePayer: "BBBB..." } }] } }
390
+
391
+ # Step 2: pay and fetch in one call
392
+ make_http_request_with_x402 {
393
+ "baseURL": "https://example.x402.goplausible.xyz",
394
+ "path": "/weather",
395
+ "method": "GET",
396
+ "maxAmountPerRequest": 10000,
397
+ "preferredNetwork": "testnet"
398
+ }
399
+ # returns: { result: <weather payload>, paid: { network: "testnet",
400
+ # asset: "10458941",
401
+ # amount: "100", payTo: "AAAA..." },
402
+ # paymentResponse: <decoded X-PAYMENT-RESPONSE> }
403
+ ```
404
+
405
+ 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`.
406
+
407
+ ### Prerequisites
408
+
409
+ - An active wallet account exists (`wallet_get_info` to verify)
410
+ - That account is opted into the payment asset (USDC mainnet ASA `31566704`, testnet ASA `10458941`)
411
+ - The account has enough of the payment asset for `maxAmountRequired`
412
+ - The endpoint's `accepts[]` includes at least one entry with an Algorand network the MCP recognizes
321
413
 
322
414
  ## Optional Environment Variables
323
415
 
@@ -361,13 +453,22 @@ See [Secure Wallet](#secure-wallet) for full architecture details.
361
453
  | `wallet_remove_account` | Remove an account from the wallet by nickname or index |
362
454
  | `wallet_list_accounts` | List all accounts with nicknames and addresses |
363
455
  | `wallet_switch_account` | Switch the active account by nickname or index |
364
- | `wallet_get_info` | Get info for the **active account this MCP server owns** (keychain-backed): address, public key, balance, opted-in counts. For arbitrary on-chain accounts use `api_algod_get_account_info`. |
456
+ | `wallet_get_info` | Get info for the **active account this MCP server owns** (DB-backed): address, public key, balance, opted-in counts. For arbitrary on-chain accounts use `api_algod_get_account_info`. |
365
457
  | `wallet_get_assets` | Get all ASA holdings for the **active account this MCP server owns**. For arbitrary on-chain accounts use `api_algod_get_account_info` or `api_algod_get_account_asset_info`. |
366
458
  | `wallet_sign_transaction` | Sign a single transaction with the active account |
367
459
  | `wallet_sign_transaction_group` | Sign a group of transactions with the active account (auto-assigns group ID) |
368
460
  | `wallet_sign_data` | Sign arbitrary hex data with raw Ed25519 (noble, no SDK prefix) |
369
461
  | `wallet_optin_asset` | Opt the active account into an asset (creates, signs, and submits) |
370
462
 
463
+ ### x402 HTTP Payment Tools (2 tools)
464
+
465
+ See [x402 HTTP Payments](#x402-http-payments) for the full protocol explanation.
466
+
467
+ | Tool | Description |
468
+ |---|---|
469
+ | `x402_discover_payment_requirements` | Probe an x402-protected endpoint and return its `accepts[]` array (cost, asset, network, payTo) without paying. Read-only. |
470
+ | `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. |
471
+
371
472
  ### Account Management (8 tools)
372
473
 
373
474
  | Tool | Description |
@@ -587,7 +688,7 @@ algorand-mcp/
587
688
  │ │ └── wallet/ # Wallet resources
588
689
  │ ├── tools/ # MCP Tools
589
690
  │ │ ├── commonParams.ts # Network + pagination schema fragments
590
- │ │ ├── walletManager.ts # Secure wallet (keychain + SQLite)
691
+ │ │ ├── walletManager.ts # Agent wallet (SQLite-backed)
591
692
  │ │ ├── accountManager.ts # Account operations
592
693
  │ │ ├── utilityManager.ts # Utility functions
593
694
  │ │ ├── algodManager.ts # TEAL compile, simulate, submit
@@ -867,7 +968,7 @@ describeIf(testConfig.isCategoryEnabled('your-category'))('Your Tools (E2E)', ()
867
968
 
868
969
  - [algosdk](https://github.com/algorand/js-algorand-sdk) v3 — Algorand JavaScript SDK
869
970
  - [@modelcontextprotocol/sdk](https://github.com/modelcontextprotocol/typescript-sdk) — MCP TypeScript SDK
870
- - [@napi-rs/keyring](https://github.com/nicolo-ribaudo/napi-keyring) — Native OS keychain access (macOS Keychain, Linux libsecret, Windows Credential Manager)
971
+ - [@napi-rs/keyring](https://github.com/nicolo-ribaudo/napi-keyring) — Native OS keychain access (macOS Keychain, Linux libsecret, Windows Credential Manager). Used as a backward-compatibility read fallback for accounts created by pre-DB-migration installs; new mnemonics are written only to `wallet.db`.
871
972
  - [sql.js](https://github.com/sql-js/sql.js) — Embedded SQLite (WASM) for wallet metadata persistence
872
973
  - [@noble/curves](https://github.com/paulmillr/noble-curves) — Pure JS Ed25519 for raw data signing (`wallet_sign_data`)
873
974
  - [@tinymanorg/tinyman-js-sdk](https://github.com/tinymanorg/tinyman-js-sdk) — Tinyman AMM SDK
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.9",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },