@goplausible/algorand-mcp 4.3.1 → 4.3.3

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.
@@ -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.3.1',
89
+ version: '4.3.2',
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).',
@@ -12,6 +12,15 @@ export declare class X402Manager {
12
12
  }>;
13
13
  private static handleDiscover;
14
14
  private static handlePaidRequest;
15
+ /**
16
+ * Returns the amount field from a PaymentRequirement, supporting both x402
17
+ * spec versions:
18
+ * - V2 (current): `amount`
19
+ * - V1 (legacy): `maxAmountRequired`
20
+ * Prefers V2 if both are set (V2 deployments may include both for compat).
21
+ */
22
+ private static getRequirementAmount;
23
+ private static isValidPaymentRequiredResponse;
15
24
  private static discover;
16
25
  private static selectRequirement;
17
26
  private static buildPaymentHeader;
@@ -45,8 +45,29 @@ const x402ToolSchemas = {
45
45
  headers: { type: 'object', description: 'Optional additional request headers', additionalProperties: { type: 'string' } },
46
46
  correlationId: { type: 'string', description: 'Optional correlation id; forwarded as X-Correlation-ID header' },
47
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.' },
48
+ paymentRequirements: {
49
+ type: 'array',
50
+ 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.',
51
+ items: {
52
+ type: 'object',
53
+ required: ['scheme', 'network', 'payTo', 'asset'],
54
+ properties: {
55
+ scheme: { type: 'string' },
56
+ network: { type: 'string', description: 'CAIP-2 network identifier, e.g. "algorand:SGO1…" for testnet.' },
57
+ amount: { type: 'string', description: 'x402 V2 canonical amount field (atomic units as decimal string).' },
58
+ maxAmountRequired: { type: 'string', description: 'x402 V1 legacy amount field. Either `amount` or `maxAmountRequired` must be present.' },
59
+ payTo: { type: 'string' },
60
+ asset: { type: 'string' },
61
+ maxTimeoutSeconds: { type: 'integer' },
62
+ extra: { type: 'object' }
63
+ }
64
+ }
65
+ },
66
+ preferredNetwork: {
67
+ type: 'string',
68
+ enum: ['mainnet', 'testnet', 'localnet'],
69
+ 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.'
70
+ },
50
71
  extensions: { type: 'object', description: 'Optional pre-fetched PaymentRequired extensions (e.g. Bazaar resource details). Passed through to the response under `extensions`.' },
51
72
  },
52
73
  required: ['baseURL', 'path', 'method'],
@@ -86,7 +107,35 @@ export class X402Manager {
86
107
  const preferredNetwork = typeof args.preferredNetwork === 'string' ? args.preferredNetwork : undefined;
87
108
  const extensions = args.extensions ?? undefined;
88
109
  let accepts = Array.isArray(args.paymentRequirements) ? args.paymentRequirements : undefined;
89
- // 1. Discover if needed
110
+ // 1a. Defensive validation: every paymentRequirements entry must be an
111
+ // object with at least { network, payTo, asset } and an amount field
112
+ // (either x402 V2 `amount` or V1 `maxAmountRequired`). LLMs frequently
113
+ // malform JSON by appending sibling field names (e.g. "preferredNetwork")
114
+ // as bare strings inside this array — catch that here with an actionable
115
+ // message rather than crashing later in selectRequirement.
116
+ if (accepts) {
117
+ for (let i = 0; i < accepts.length; i++) {
118
+ const entry = accepts[i];
119
+ const isObject = !!entry && typeof entry === 'object' && !Array.isArray(entry);
120
+ if (!isObject) {
121
+ 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.`);
122
+ }
123
+ const e = entry;
124
+ const missing = [];
125
+ if (typeof e.network !== 'string')
126
+ missing.push('network');
127
+ if (typeof e.payTo !== 'string')
128
+ missing.push('payTo');
129
+ if (typeof e.asset !== 'string')
130
+ missing.push('asset');
131
+ if (typeof e.amount !== 'string' && typeof e.maxAmountRequired !== 'string')
132
+ missing.push('amount (or maxAmountRequired)');
133
+ if (missing.length > 0) {
134
+ 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.`);
135
+ }
136
+ }
137
+ }
138
+ // 1b. Discover if needed
90
139
  if (!accepts || accepts.length === 0) {
91
140
  const discovered = await X402Manager.discover({ baseURL, path, method, queryParams, body });
92
141
  if (!discovered.x402 || !Array.isArray(discovered.accepts) || discovered.accepts.length === 0) {
@@ -108,7 +157,10 @@ export class X402Manager {
108
157
  throw new McpError(ErrorCode.InvalidRequest, `Payment rejected by server: ${X402Manager.snippet(response.bodyText)}`);
109
158
  }
110
159
  const result = X402Manager.tryParseJson(response.bodyText) ?? response.bodyText;
111
- const paymentResponseHeader = response.headers['x-payment-response'] ?? response.headers['X-PAYMENT-RESPONSE'];
160
+ // Algorand x402 V2 uses lowercase `payment-response`; older deployments
161
+ // may use Coinbase's `x-payment-response`. Accept either.
162
+ const paymentResponseHeader = response.headers['payment-response']
163
+ ?? response.headers['x-payment-response'];
112
164
  return X402Manager.textResponse({
113
165
  result,
114
166
  status: response.status,
@@ -116,27 +168,51 @@ export class X402Manager {
116
168
  paid: {
117
169
  network: mcpNetwork,
118
170
  asset: requirement.asset,
119
- amount: requirement.maxAmountRequired,
171
+ amount: X402Manager.getRequirementAmount(requirement),
120
172
  payTo: requirement.payTo,
121
173
  },
122
174
  extensions,
123
175
  });
124
176
  }
125
177
  // ── x402 protocol helpers ──────────────────────────────────────────────────
178
+ /**
179
+ * Returns the amount field from a PaymentRequirement, supporting both x402
180
+ * spec versions:
181
+ * - V2 (current): `amount`
182
+ * - V1 (legacy): `maxAmountRequired`
183
+ * Prefers V2 if both are set (V2 deployments may include both for compat).
184
+ */
185
+ static getRequirementAmount(req) {
186
+ return req.amount ?? req.maxAmountRequired;
187
+ }
188
+ static isValidPaymentRequiredResponse(v) {
189
+ return !!v
190
+ && typeof v === 'object'
191
+ && typeof v.x402Version === 'number'
192
+ && Array.isArray(v.accepts);
193
+ }
126
194
  static async discover(opts) {
127
195
  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 };
196
+ // 1. Canonical Algorand x402 V2 transport: requirements arrive in the
197
+ // `payment-required` HTTP header as base64-encoded JSON. The body is
198
+ // usually empty (`{}`). Check this first.
199
+ const headerPayload = response.headers['payment-required'];
200
+ if (headerPayload) {
201
+ const decoded = X402Manager.decodeBase64Json(headerPayload);
202
+ if (X402Manager.isValidPaymentRequiredResponse(decoded)) {
203
+ return { status: response.status, x402: true, accepts: decoded.accepts, x402Version: decoded.x402Version };
204
+ }
130
205
  }
131
- const parsed = X402Manager.tryParseJson(response.bodyText);
132
- if (!parsed ||
133
- typeof parsed !== 'object' ||
134
- typeof parsed.x402Version !== 'number' ||
135
- !Array.isArray(parsed.accepts)) {
206
+ // 2. Fallback transport (some x402 servers put requirements in the 402 body).
207
+ if (response.status === 402) {
208
+ const parsed = X402Manager.tryParseJson(response.bodyText);
209
+ if (X402Manager.isValidPaymentRequiredResponse(parsed)) {
210
+ return { status: 402, x402: true, accepts: parsed.accepts, x402Version: parsed.x402Version };
211
+ }
136
212
  return { status: 402, x402: false, rawBody: parsed ?? response.bodyText };
137
213
  }
138
- const body = parsed;
139
- return { status: 402, x402: true, accepts: body.accepts, x402Version: body.x402Version };
214
+ // 3. Not an x402-protected response at all.
215
+ return { status: response.status, x402: false, rawBody: X402Manager.tryParseJson(response.bodyText) ?? response.bodyText };
140
216
  }
141
217
  static selectRequirement(accepts, preferredNetwork, maxAmountPerRequest) {
142
218
  // Map each accepts entry to its MCP network (or null if non-Algorand).
@@ -146,10 +222,11 @@ export class X402Manager {
146
222
  if (ranked.length === 0) {
147
223
  throw new McpError(ErrorCode.InvalidRequest, `No payment requirement is satisfiable on Algorand. Endpoint accepts networks: [${accepts.map(r => r.network).join(', ')}]`);
148
224
  }
149
- const inBudget = (req) => maxAmountPerRequest === undefined || Number(req.maxAmountRequired) <= maxAmountPerRequest;
225
+ const amountOf = (req) => Number(X402Manager.getRequirementAmount(req));
226
+ const inBudget = (req) => maxAmountPerRequest === undefined || amountOf(req) <= maxAmountPerRequest;
150
227
  const affordable = ranked.filter(e => inBudget(e.req));
151
228
  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)))}.`);
229
+ throw new McpError(ErrorCode.InvalidRequest, `All Algorand payment requirements exceed maxAmountPerRequest=${maxAmountPerRequest}. Cheapest: ${Math.min(...ranked.map(e => amountOf(e.req)))}.`);
153
230
  }
154
231
  if (preferredNetwork) {
155
232
  const preferred = affordable.find(e => e.mcpNetwork === preferredNetwork);
@@ -157,7 +234,7 @@ export class X402Manager {
157
234
  return { requirement: preferred.req, mcpNetwork: preferred.mcpNetwork };
158
235
  }
159
236
  // 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);
237
+ const chosen = affordable.reduce((a, b) => amountOf(a.req) <= amountOf(b.req) ? a : b);
161
238
  return { requirement: chosen.req, mcpNetwork: chosen.mcpNetwork };
162
239
  }
163
240
  static async buildPaymentHeader(requirement, mcpNetwork) {
@@ -165,9 +242,10 @@ export class X402Manager {
165
242
  if (!feePayer || typeof feePayer !== 'string') {
166
243
  throw new McpError(ErrorCode.InvalidRequest, 'Payment requirement is missing `extra.feePayer` (facilitator address).');
167
244
  }
168
- const amount = Number(requirement.maxAmountRequired);
245
+ const amountStr = X402Manager.getRequirementAmount(requirement);
246
+ const amount = Number(amountStr);
169
247
  if (!Number.isFinite(amount) || amount < 0) {
170
- throw new McpError(ErrorCode.InvalidRequest, `Invalid maxAmountRequired: ${requirement.maxAmountRequired}`);
248
+ throw new McpError(ErrorCode.InvalidRequest, `Invalid payment amount: ${amountStr ?? '(missing `amount` / `maxAmountRequired` field)'}`);
171
249
  }
172
250
  const account = await WalletManager.getActiveWalletAccount();
173
251
  const sk = await WalletManager.getActiveWalletSecretKey();
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goplausible/algorand-mcp",
3
- "version": "4.3.1",
3
+ "version": "4.3.3",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },