mpp32-mcp-server 1.2.0 → 1.2.2

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/CHANGELOG.md CHANGED
@@ -4,6 +4,47 @@ All notable changes to `mpp32-mcp-server` are documented here. The format
4
4
  follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the
5
5
  project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [1.2.2] - 2026-05-11
8
+
9
+ ### Fixed
10
+
11
+ * **x402 v2 envelope (the whole third-party catalog).** Every third-party
12
+ x402 provider we tested — Venice (`api.venice.ai`), Exa (`api.exa.ai`),
13
+ Firecrawl, OpenAI's x402 gateway, etc. — ships the **v2** challenge
14
+ shape: `{x402Version: 2, accepts: [{scheme, network, asset, payTo,
15
+ amount, ...}, ...]}`. 1.2.0 and 1.2.1 only understood **v1**
16
+ (`{scheme, network, asset, payTo, maxAmountRequired, ...}` at the top
17
+ level), so every third-party call failed with "missing network field"
18
+ before we ever signed anything. The signer now:
19
+ - Detects v1 vs v2 by the presence of `accepts: [...]`.
20
+ - Picks the first `accepts[]` entry it can pay (prefers EVM when
21
+ `MPP32_PRIVATE_KEY` is set, falls back to SVM when only the Solana
22
+ key is set, falls back to the first entry so the per-network error
23
+ surfaces precisely).
24
+ - Maps v2's `amount` to v1's `maxAmountRequired` internally, so the
25
+ same signing code handles both.
26
+ - Echoes the challenge's `x402Version` back in the outgoing envelope
27
+ so servers that key off it (Venice's facilitator does) accept the
28
+ response. v1 challenges still get v1 back; v2 challenges get v2.
29
+ This unblocks paid access to the entire ~4,500-entry federated
30
+ catalog, which is overwhelmingly v2.
31
+
32
+ ## [1.2.1] - 2026-05-11
33
+
34
+ ### Fixed
35
+
36
+ * **"Server disconnected" on startup under Claude Desktop's bundled Node.**
37
+ 1.2.0 imported `@solana/kit` (and `@solana-program/*`) at the top of the
38
+ payment-signer module. Those packages declare `engines.node: ">=20.18.0"`
39
+ and use Node-20-only WebCrypto Ed25519 APIs at load time. Claude Desktop
40
+ ships a bundled Node that on many installs is still 18.x, so the import
41
+ threw before the MCP server could answer the initialize handshake — the
42
+ process exited and Claude Desktop reported only "Server disconnected"
43
+ with no further diagnostics. All Solana and EVM crypto deps are now
44
+ loaded lazily inside the signer functions. The server boots on any Node
45
+ that supports MCP; only a `solana:*` payment attempt fails on too-old
46
+ Node, and now with a clear actionable error.
47
+
7
48
  ## [1.2.0] - 2026-05-11
8
49
 
9
50
  This release makes x402 payments actually work end-to-end. Prior versions
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { z } from "zod";
5
5
  import { signX402Payment } from "./x402-signers.js";
6
- const SERVER_VERSION = "1.2.0";
6
+ const SERVER_VERSION = "1.2.2";
7
7
  // ── Env loading: trim and sanitize aggressively ─────────────────────────────
8
8
  // Copy-paste from Claude Desktop / Cursor / Windsurf JSON config UIs frequently
9
9
  // adds trailing \n, \r, NBSP, BOM, or wraps the value in literal quotes. Any
@@ -25,8 +25,8 @@ export interface X402PaymentEnvelope {
25
25
  }
26
26
  export declare function isSvmNetwork(network: string): boolean;
27
27
  export declare function isEvmNetwork(network: string): boolean;
28
- export declare function signX402PaymentSvm(requirements: X402PaymentRequirements, rawKey: string, rpcUrlOverride?: string): Promise<string>;
29
- export declare function signX402PaymentEvm(requirements: X402PaymentRequirements, rawKey: string): Promise<string>;
28
+ export declare function signX402PaymentSvm(requirements: X402PaymentRequirements, rawKey: string, rpcUrlOverride?: string, echoedVersion?: number): Promise<string>;
29
+ export declare function signX402PaymentEvm(requirements: X402PaymentRequirements, rawKey: string, echoedVersion?: number): Promise<string>;
30
30
  export interface SignX402Args {
31
31
  paymentRequiredHeader: string;
32
32
  solanaKey?: string;
@@ -17,12 +17,50 @@
17
17
  // In both cases the outer envelope is
18
18
  // { x402Version: 1, scheme: "exact", network, payload: <scheme payload> }
19
19
  // base64-encoded into the `X-Payment` HTTP header.
20
- import { address, createKeyPairSignerFromBytes, createSolanaRpc, createTransactionMessage, setTransactionMessageFeePayer, setTransactionMessageLifetimeUsingBlockhash, appendTransactionMessageInstructions, partiallySignTransactionMessageWithSigners, getBase64EncodedWireTransaction, pipe, } from "@solana/kit";
21
- import { getTransferCheckedInstruction, findAssociatedTokenPda, TOKEN_PROGRAM_ADDRESS, } from "@solana-program/token";
22
- import { getSetComputeUnitLimitInstruction, getSetComputeUnitPriceInstruction, } from "@solana-program/compute-budget";
23
- import { privateKeyToAccount } from "viem/accounts";
24
- import bs58 from "bs58";
25
- import nacl from "tweetnacl";
20
+ async function loadSvmDeps() {
21
+ const [kit, tokenProgram, computeBudgetProgram, bs58Mod, naclMod] = await Promise.all([
22
+ import("@solana/kit"),
23
+ import("@solana-program/token"),
24
+ import("@solana-program/compute-budget"),
25
+ import("bs58"),
26
+ import("tweetnacl"),
27
+ ]).catch((err) => {
28
+ const msg = err instanceof Error ? err.message : String(err);
29
+ throw new Error(`Could not load Solana signing libraries: ${msg}. ` +
30
+ `Solana x402 payments require Node 20.18 or newer (Claude Desktop's bundled Node may be older). ` +
31
+ `Upgrade Node, or run mpp32-mcp-server under a system Node 20+ via your MCP config's "command".`);
32
+ });
33
+ return {
34
+ address: kit.address,
35
+ createKeyPairSignerFromBytes: kit.createKeyPairSignerFromBytes,
36
+ createSolanaRpc: kit.createSolanaRpc,
37
+ createTransactionMessage: kit.createTransactionMessage,
38
+ setTransactionMessageFeePayer: kit.setTransactionMessageFeePayer,
39
+ setTransactionMessageLifetimeUsingBlockhash: kit.setTransactionMessageLifetimeUsingBlockhash,
40
+ appendTransactionMessageInstructions: kit.appendTransactionMessageInstructions,
41
+ partiallySignTransactionMessageWithSigners: kit.partiallySignTransactionMessageWithSigners,
42
+ getBase64EncodedWireTransaction: kit.getBase64EncodedWireTransaction,
43
+ pipe: kit.pipe,
44
+ getTransferCheckedInstruction: tokenProgram.getTransferCheckedInstruction,
45
+ findAssociatedTokenPda: tokenProgram.findAssociatedTokenPda,
46
+ TOKEN_PROGRAM_ADDRESS: tokenProgram.TOKEN_PROGRAM_ADDRESS,
47
+ getSetComputeUnitLimitInstruction: computeBudgetProgram.getSetComputeUnitLimitInstruction,
48
+ getSetComputeUnitPriceInstruction: computeBudgetProgram.getSetComputeUnitPriceInstruction,
49
+ bs58: bs58Mod.default ?? bs58Mod,
50
+ nacl: naclMod.default ?? naclMod,
51
+ };
52
+ }
53
+ async function loadEvmDeps() {
54
+ try {
55
+ const viemAccounts = await import("viem/accounts");
56
+ return { privateKeyToAccount: viemAccounts.privateKeyToAccount };
57
+ }
58
+ catch (err) {
59
+ const msg = err instanceof Error ? err.message : String(err);
60
+ throw new Error(`Could not load EVM signing libraries: ${msg}. ` +
61
+ `Base / Ethereum x402 payments require Node 18 or newer. Upgrade Node and retry.`);
62
+ }
63
+ }
26
64
  // ── Network classification ──────────────────────────────────────────────────
27
65
  const SOLANA_MAINNET = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
28
66
  const SOLANA_DEVNET = "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1";
@@ -46,8 +84,7 @@ function chainSpecFor(network) {
46
84
  }
47
85
  throw new Error(`Unsupported EVM network "${network}". x402 EVM payments currently support: base, base-sepolia, ethereum.`);
48
86
  }
49
- // ── Key decoding ────────────────────────────────────────────────────────────
50
- function decodeSolanaSecret(raw) {
87
+ function decodeSolanaSecret(raw, deps) {
51
88
  if (raw.startsWith("[")) {
52
89
  const arr = JSON.parse(raw);
53
90
  if (!Array.isArray(arr))
@@ -57,24 +94,24 @@ function decodeSolanaSecret(raw) {
57
94
  if (/^[0-9a-fA-F]+$/.test(raw) && raw.length % 2 === 0) {
58
95
  return new Uint8Array(Buffer.from(raw, "hex"));
59
96
  }
60
- return bs58.decode(raw);
97
+ return deps.bs58.decode(raw);
61
98
  }
62
- async function buildSolanaSigner(rawKey) {
63
- let bytes = decodeSolanaSecret(rawKey);
99
+ async function buildSolanaSigner(rawKey, deps) {
100
+ let bytes = decodeSolanaSecret(rawKey, deps);
64
101
  if (bytes.length === 32) {
65
102
  // 32-byte seed — kit's createKeyPairSignerFromBytes wants the 64-byte
66
103
  // expanded key. Derive via tweetnacl.
67
- const kp = nacl.sign.keyPair.fromSeed(bytes);
104
+ const kp = deps.nacl.sign.keyPair.fromSeed(bytes);
68
105
  bytes = kp.secretKey;
69
106
  }
70
107
  else if (bytes.length !== 64) {
71
108
  throw new Error(`Solana private key must be a 32-byte seed or a 64-byte expanded key; got ${bytes.length} bytes.`);
72
109
  }
73
- return await createKeyPairSignerFromBytes(bytes);
110
+ return await deps.createKeyPairSignerFromBytes(bytes);
74
111
  }
75
112
  // ── SVM signer ──────────────────────────────────────────────────────────────
76
113
  const DEFAULT_SOLANA_RPC = "https://api.mainnet-beta.solana.com";
77
- export async function signX402PaymentSvm(requirements, rawKey, rpcUrlOverride) {
114
+ export async function signX402PaymentSvm(requirements, rawKey, rpcUrlOverride, echoedVersion = 1) {
78
115
  if (requirements.scheme !== "exact") {
79
116
  throw new Error(`SVM x402 scheme "${requirements.scheme}" not implemented; only "exact" is supported.`);
80
117
  }
@@ -86,33 +123,34 @@ export async function signX402PaymentSvm(requirements, rawKey, rpcUrlOverride) {
86
123
  const amount = BigInt(requirements.maxAmountRequired);
87
124
  if (amount <= 0n)
88
125
  throw new Error(`Invalid maxAmountRequired: ${requirements.maxAmountRequired}`);
89
- const signer = await buildSolanaSigner(rawKey);
126
+ const deps = await loadSvmDeps();
127
+ const signer = await buildSolanaSigner(rawKey, deps);
90
128
  const payerAddress = signer.address;
91
- const mintAddress = address(requirements.asset);
92
- const recipientAddress = address(requirements.payTo);
93
- const feePayerAddress = address(requirements.extra.feePayer);
129
+ const mintAddress = deps.address(requirements.asset);
130
+ const recipientAddress = deps.address(requirements.payTo);
131
+ const feePayerAddress = deps.address(requirements.extra.feePayer);
94
132
  // Derive both sides' associated token accounts (classic SPL Token program).
95
133
  const [sourceAtaTuple, destinationAtaTuple] = await Promise.all([
96
- findAssociatedTokenPda({
134
+ deps.findAssociatedTokenPda({
97
135
  owner: payerAddress,
98
136
  mint: mintAddress,
99
- tokenProgram: TOKEN_PROGRAM_ADDRESS,
137
+ tokenProgram: deps.TOKEN_PROGRAM_ADDRESS,
100
138
  }),
101
- findAssociatedTokenPda({
139
+ deps.findAssociatedTokenPda({
102
140
  owner: recipientAddress,
103
141
  mint: mintAddress,
104
- tokenProgram: TOKEN_PROGRAM_ADDRESS,
142
+ tokenProgram: deps.TOKEN_PROGRAM_ADDRESS,
105
143
  }),
106
144
  ]);
107
145
  const sourceAta = sourceAtaTuple[0];
108
146
  const destinationAta = destinationAtaTuple[0];
109
147
  const rpcUrl = rpcUrlOverride && rpcUrlOverride.length > 0 ? rpcUrlOverride : DEFAULT_SOLANA_RPC;
110
- const rpc = createSolanaRpc(rpcUrl);
148
+ const rpc = deps.createSolanaRpc(rpcUrl);
111
149
  const { value: latestBlockhash } = await rpc.getLatestBlockhash({ commitment: "confirmed" }).send();
112
150
  const instructions = [
113
- getSetComputeUnitLimitInstruction({ units: 150_000 }),
114
- getSetComputeUnitPriceInstruction({ microLamports: 1000n }),
115
- getTransferCheckedInstruction({
151
+ deps.getSetComputeUnitLimitInstruction({ units: 150_000 }),
152
+ deps.getSetComputeUnitPriceInstruction({ microLamports: 1000n }),
153
+ deps.getTransferCheckedInstruction({
116
154
  source: sourceAta,
117
155
  mint: mintAddress,
118
156
  destination: destinationAta,
@@ -121,13 +159,13 @@ export async function signX402PaymentSvm(requirements, rawKey, rpcUrlOverride) {
121
159
  decimals,
122
160
  }),
123
161
  ];
124
- const message = pipe(createTransactionMessage({ version: 0 }), (m) => setTransactionMessageFeePayer(feePayerAddress, m), (m) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m), (m) => appendTransactionMessageInstructions(instructions, m));
162
+ const message = deps.pipe(deps.createTransactionMessage({ version: 0 }), (m) => deps.setTransactionMessageFeePayer(feePayerAddress, m), (m) => deps.setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m), (m) => deps.appendTransactionMessageInstructions(instructions, m));
125
163
  // Partially sign — fills the payer's signature slot, leaves the fee payer's
126
164
  // slot empty for the facilitator to fill in at /settle time.
127
- const partiallySigned = await partiallySignTransactionMessageWithSigners(message);
128
- const base64Tx = getBase64EncodedWireTransaction(partiallySigned);
165
+ const partiallySigned = await deps.partiallySignTransactionMessageWithSigners(message);
166
+ const base64Tx = deps.getBase64EncodedWireTransaction(partiallySigned);
129
167
  const envelope = {
130
- x402Version: 1,
168
+ x402Version: echoedVersion,
131
169
  scheme: "exact",
132
170
  network: requirements.network,
133
171
  payload: { transaction: base64Tx },
@@ -141,7 +179,7 @@ function randomHex32() {
141
179
  buf[i] = Math.floor(Math.random() * 256);
142
180
  return ("0x" + buf.toString("hex"));
143
181
  }
144
- export async function signX402PaymentEvm(requirements, rawKey) {
182
+ export async function signX402PaymentEvm(requirements, rawKey, echoedVersion = 1) {
145
183
  if (requirements.scheme !== "exact") {
146
184
  throw new Error(`EVM x402 scheme "${requirements.scheme}" not implemented; only "exact" is supported.`);
147
185
  }
@@ -163,6 +201,7 @@ export async function signX402PaymentEvm(requirements, rawKey) {
163
201
  if (!/^0x[0-9a-fA-F]{64}$/.test(keyHex)) {
164
202
  throw new Error("MPP32_PRIVATE_KEY must be a 64-character hex EVM private key (0x-prefixed or bare).");
165
203
  }
204
+ const { privateKeyToAccount } = await loadEvmDeps();
166
205
  const account = privateKeyToAccount(keyHex);
167
206
  const now = Math.floor(Date.now() / 1000);
168
207
  const validAfter = BigInt(0);
@@ -199,7 +238,7 @@ export async function signX402PaymentEvm(requirements, rawKey) {
199
238
  message: messageObj,
200
239
  });
201
240
  const envelope = {
202
- x402Version: 1,
241
+ x402Version: echoedVersion,
203
242
  scheme: "exact",
204
243
  network: requirements.network,
205
244
  payload: {
@@ -216,29 +255,93 @@ export async function signX402PaymentEvm(requirements, rawKey) {
216
255
  };
217
256
  return Buffer.from(JSON.stringify(envelope)).toString("base64");
218
257
  }
258
+ function isV2Challenge(decoded) {
259
+ return (!!decoded &&
260
+ typeof decoded === "object" &&
261
+ Array.isArray(decoded.accepts));
262
+ }
263
+ // Normalize a v2 `accepts[i]` to our internal requirements shape. v2 uses
264
+ // `amount`, v1 uses `maxAmountRequired` — we map both into the same field so
265
+ // the downstream signers don't have to know which version produced the input.
266
+ function normalizeRequirements(raw) {
267
+ const amount = raw.maxAmountRequired ?? raw.amount ?? "";
268
+ return {
269
+ scheme: String(raw.scheme ?? "exact"),
270
+ network: String(raw.network ?? ""),
271
+ maxAmountRequired: amount,
272
+ resource: String(raw.resource ?? ""),
273
+ description: raw.description,
274
+ mimeType: raw.mimeType,
275
+ payTo: String(raw.payTo ?? ""),
276
+ maxTimeoutSeconds: raw.maxTimeoutSeconds,
277
+ asset: String(raw.asset ?? ""),
278
+ outputSchema: raw.outputSchema,
279
+ extra: raw.extra,
280
+ };
281
+ }
282
+ // Pick the first entry in `accepts` we can actually sign. Preference order:
283
+ // 1. EVM entries when an EVM key is available (Base is the dominant chain).
284
+ // 2. SVM entries when a Solana key is available.
285
+ // If no entry matches the keys we hold, fall back to the first entry and let
286
+ // the per-network signer throw a precise "you need MPP32_X_PRIVATE_KEY" error.
287
+ function pickRequirements(accepts, haveSvm, haveEvm) {
288
+ if (accepts.length === 0) {
289
+ throw new Error("x402 v2 challenge has empty `accepts` array — nothing to pay.");
290
+ }
291
+ if (haveEvm) {
292
+ const evm = accepts.find((a) => isEvmNetwork(a.network));
293
+ if (evm)
294
+ return evm;
295
+ }
296
+ if (haveSvm) {
297
+ const svm = accepts.find((a) => isSvmNetwork(a.network));
298
+ if (svm)
299
+ return svm;
300
+ }
301
+ return accepts[0];
302
+ }
219
303
  export async function signX402Payment(args) {
220
- let requirements;
304
+ let decoded;
221
305
  try {
222
306
  const json = Buffer.from(args.paymentRequiredHeader, "base64").toString("utf-8");
223
- requirements = JSON.parse(json);
307
+ decoded = JSON.parse(json);
224
308
  }
225
309
  catch (err) {
226
310
  throw new Error(`Could not decode Payment-Required header as base64 JSON: ${err instanceof Error ? err.message : String(err)}`);
227
311
  }
312
+ // Resolve v1 vs v2. v2's `x402Version` field tells the server which envelope
313
+ // shape to expect back — we mirror whichever the challenge used.
314
+ let requirements;
315
+ let echoedVersion;
316
+ if (isV2Challenge(decoded)) {
317
+ const accepts = decoded.accepts
318
+ .filter((a) => !!a && typeof a === "object")
319
+ .map(normalizeRequirements);
320
+ requirements = pickRequirements(accepts, !!args.solanaKey, !!args.evmKey);
321
+ echoedVersion = decoded.x402Version || 2;
322
+ }
323
+ else if (decoded && typeof decoded === "object") {
324
+ requirements = normalizeRequirements(decoded);
325
+ echoedVersion = decoded.x402Version || 1;
326
+ }
327
+ else {
328
+ throw new Error("Decoded Payment-Required is not a JSON object.");
329
+ }
228
330
  if (!requirements.network)
229
331
  throw new Error("x402 payment requirements missing 'network'");
230
332
  if (!requirements.asset)
231
333
  throw new Error("x402 payment requirements missing 'asset'");
232
334
  if (!requirements.payTo)
233
335
  throw new Error("x402 payment requirements missing 'payTo'");
234
- if (!requirements.maxAmountRequired)
235
- throw new Error("x402 payment requirements missing 'maxAmountRequired'");
336
+ if (!requirements.maxAmountRequired) {
337
+ throw new Error("x402 payment requirements missing 'maxAmountRequired'/'amount'");
338
+ }
236
339
  if (isSvmNetwork(requirements.network)) {
237
340
  if (!args.solanaKey) {
238
341
  throw new Error(`Provider requires SVM payment on ${requirements.network}, but MPP32_SOLANA_PRIVATE_KEY is not configured. ` +
239
342
  `Set it in your MCP config to enable USDC-on-Solana payments.`);
240
343
  }
241
- const header = await signX402PaymentSvm(requirements, args.solanaKey, args.solanaRpcUrl);
344
+ const header = await signX402PaymentSvm(requirements, args.solanaKey, args.solanaRpcUrl, echoedVersion);
242
345
  return {
243
346
  xPaymentHeader: header,
244
347
  network: requirements.network,
@@ -251,7 +354,7 @@ export async function signX402Payment(args) {
251
354
  throw new Error(`Provider requires EVM payment on ${requirements.network}, but MPP32_PRIVATE_KEY is not configured. ` +
252
355
  `Set it in your MCP config to enable USDC-on-Base payments.`);
253
356
  }
254
- const header = await signX402PaymentEvm(requirements, args.evmKey);
357
+ const header = await signX402PaymentEvm(requirements, args.evmKey, echoedVersion);
255
358
  return {
256
359
  xPaymentHeader: header,
257
360
  network: requirements.network,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mpp32-mcp-server",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "mcpName": "io.github.MPP32/mpp32-mcp-server",
5
5
  "description": "Payment layer for AI agents. One MCP, five protocols, thousands of paid APIs your agent can call.",
6
6
  "type": "module",