@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.
|
|
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: {
|
|
49
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
129
|
-
|
|
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
|
-
|
|
132
|
-
if (
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
139
|
-
return { status:
|
|
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
|
|
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 =>
|
|
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) =>
|
|
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
|
|
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
|
|
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;
|