mpp32-mcp-server 1.1.4 → 1.2.1

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,76 @@ 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.1] - 2026-05-11
8
+
9
+ ### Fixed
10
+
11
+ * **"Server disconnected" on startup under Claude Desktop's bundled Node.**
12
+ 1.2.0 imported `@solana/kit` (and `@solana-program/*`) at the top of the
13
+ payment-signer module. Those packages declare `engines.node: ">=20.18.0"`
14
+ and use Node-20-only WebCrypto Ed25519 APIs at load time. Claude Desktop
15
+ ships a bundled Node that on many installs is still 18.x, so the import
16
+ threw before the MCP server could answer the initialize handshake — the
17
+ process exited and Claude Desktop reported only "Server disconnected"
18
+ with no further diagnostics. All Solana and EVM crypto deps are now
19
+ loaded lazily inside the signer functions. The server boots on any Node
20
+ that supports MCP; only a `solana:*` payment attempt fails on too-old
21
+ Node, and now with a clear actionable error.
22
+
23
+ ## [1.2.0] - 2026-05-11
24
+
25
+ This release makes x402 payments actually work end-to-end. Prior versions
26
+ shipped a non-spec-compliant signing path that the official x402.org
27
+ facilitator rejected with HTTP 400 on every paid call, so no settlement ever
28
+ occurred. The signing path has been rewritten from scratch against the
29
+ [Coinbase x402 reference implementation](https://github.com/coinbase/x402).
30
+
31
+ ### Fixed (the headline)
32
+
33
+ * **Real Solana x402 payments.** When a server returns a `Payment-Required`
34
+ challenge on a `solana:*` network, the MCP client now builds a real Solana
35
+ `VersionedTransaction` with the three instructions the `exact` SVM scheme
36
+ requires — `SetComputeUnitLimit`, `SetComputeUnitPrice`, and SPL-Token
37
+ `TransferChecked` between the payer's and recipient's Associated Token
38
+ Accounts. The transaction is partially signed by the payer (the fee-payer
39
+ slot is left empty for the facilitator to fill at `/settle` time, per spec)
40
+ and base64-encoded into the `payload.transaction` field. The official
41
+ `x402.org/facilitator` now accepts and settles these payments.
42
+
43
+ * **Real EVM x402 payments on Base.** For challenges with `network: "base"`
44
+ or `network: "base-sepolia"` (and the `eip155:*` aliases), the client now
45
+ signs an EIP-3009 `transferWithAuthorization` typed-data message using
46
+ `viem` and the EVM private key in `MPP32_PRIVATE_KEY`. This unblocks the
47
+ ~85% of the federated catalog (~3,900 of 4,581 entries) that lives on
48
+ Base — Exa Search, Firecrawl, OpenAI's x402 gateway, Anthropic's,
49
+ Alchemy RPC, CoinGecko Pro, Nansen, Cloudflare Workers AI, and the rest.
50
+
51
+ ### Added
52
+
53
+ * **`path` argument to `call_mpp32_endpoint`.** Many curated catalog
54
+ entries store only an upstream base URL (e.g. `https://api.exa.ai`). Pass
55
+ the upstream path (e.g. `/search`) via the new `path` parameter to hit a
56
+ real endpoint instead of `POST /` (which returned 404). The agent server
57
+ forwards the path and appends it safely to the catalog base URL.
58
+
59
+ * **`MPP32_SOLANA_RPC_URL` env var.** Override the Solana RPC used to fetch
60
+ recent blockhashes when building x402 transactions. Defaults to
61
+ `https://api.mainnet-beta.solana.com`. Set this if you hit public-endpoint
62
+ rate limits.
63
+
64
+ * **`@solana/kit`, `@solana-program/token`, `@solana-program/compute-budget`,
65
+ `viem` as real dependencies.** Tree-shakeable, no `rpc-websockets`
66
+ transitive dependency, and no ESM/CJS landmines on Node 20+. `viem` was
67
+ previously an optional peer; it is now required because the EVM x402
68
+ signer cannot work without it.
69
+
70
+ ### Migration
71
+
72
+ * No config changes required if you only use the Solana intelligence oracle
73
+ (it still uses `MPP32_SOLANA_PRIVATE_KEY`). To pay for Base-network
74
+ services like Exa Search, set `MPP32_PRIVATE_KEY` to your EVM private key
75
+ and ensure that wallet holds USDC on Base.
76
+
7
77
  ## [1.1.4] - 2026-05-11
8
78
 
9
79
  ### Added
package/dist/index.js CHANGED
@@ -2,7 +2,8 @@
2
2
  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
- const SERVER_VERSION = "1.1.4";
5
+ import { signX402Payment } from "./x402-signers.js";
6
+ const SERVER_VERSION = "1.2.1";
6
7
  // ── Env loading: trim and sanitize aggressively ─────────────────────────────
7
8
  // Copy-paste from Claude Desktop / Cursor / Windsurf JSON config UIs frequently
8
9
  // adds trailing \n, \r, NBSP, BOM, or wraps the value in literal quotes. Any
@@ -342,14 +343,18 @@ server.tool("list_mpp32_services", "Browse the MPP32 federated catalog of 4,500+
342
343
  }
343
344
  });
344
345
  // ── Tool 2: call_mpp32_endpoint ─────────────────────────────────────────────
345
- server.tool("call_mpp32_endpoint", "Call any HTTP-callable service in the MPP32 federated catalog. Free services return immediately. Paid services return a 402 challenge that this tool will sign and retry automatically when a payment key (MPP32_SOLANA_PRIVATE_KEY for x402/USDC, MPP32_PRIVATE_KEY for Tempo/pathUSD) is configured. Set MPP32_AGENT_KEY for dashboard tracking. Use `list_mpp32_services` first to find a slug. Listing-only entries (npx-installable MCP servers, x402 Bazaar non-mirrored items) cannot be called through this tool — install them directly per the catalog instructions.", {
346
+ server.tool("call_mpp32_endpoint", "Call any HTTP-callable service in the MPP32 federated catalog. Free services return immediately. Paid services return a 402 challenge that this tool will sign and retry automatically when a payment key (MPP32_SOLANA_PRIVATE_KEY for x402-on-Solana, MPP32_PRIVATE_KEY for x402-on-Base/Ethereum and Tempo pathUSD) is configured. Set MPP32_AGENT_KEY for dashboard tracking. Use `list_mpp32_services` first to find a slug. Many catalog entries store only the upstream BASE URL (e.g. `https://api.exa.ai`) — pass the upstream path (e.g. `/search`) via the `path` argument when calling those. Listing-only entries (npx-installable MCP servers, etc.) cannot be called through this tool.", {
346
347
  slug: z
347
348
  .string()
348
- .describe("Service slug from `list_mpp32_services` (e.g. 'mpp32-intelligence')."),
349
+ .describe("Service slug from `list_mpp32_services` (e.g. 'curated:exa', 'mpp32-intelligence')."),
349
350
  method: z
350
351
  .enum(["GET", "POST", "PUT", "DELETE"])
351
352
  .default("POST")
352
353
  .describe("HTTP method."),
354
+ path: z
355
+ .string()
356
+ .optional()
357
+ .describe("Upstream path appended to the service's base URL (e.g. '/search' for Exa, '/v1/chat/completions' for OpenAI). Leave empty for catalog entries that already store a full path, or for native MPP32 services. Always begins with '/'."),
353
358
  body: z
354
359
  .union([z.string(), z.record(z.unknown())])
355
360
  .optional()
@@ -358,7 +363,7 @@ server.tool("call_mpp32_endpoint", "Call any HTTP-callable service in the MPP32
358
363
  .record(z.string())
359
364
  .optional()
360
365
  .describe("URL query parameters as key-value pairs."),
361
- }, async ({ slug, method, body, query }) => {
366
+ }, async ({ slug, method, path, body, query }) => {
362
367
  // Normalize body to an object so it can be JSON.stringified by the upstream call
363
368
  let parsedBody = body;
364
369
  if (typeof body === "string") {
@@ -370,10 +375,10 @@ server.tool("call_mpp32_endpoint", "Call any HTTP-callable service in the MPP32
370
375
  }
371
376
  }
372
377
  if (AGENT_KEY) {
373
- return await callViaAgentExecute(slug, method, parsedBody, query);
378
+ return await callViaAgentExecute(slug, method, parsedBody, query, path);
374
379
  }
375
380
  // Legacy path — only works for native services with payment keys
376
- return await callViaLegacyProxy(slug, method, parsedBody, query);
381
+ return await callViaLegacyProxy(slug, method, parsedBody, query, path);
377
382
  });
378
383
  // ── Tool 3: get_solana_token_intelligence ───────────────────────────────────
379
384
  server.tool("get_solana_token_intelligence", "Get real-time Solana token intelligence from the MPP32 Intelligence Oracle. Returns alpha score (0-100), rug risk assessment, whale activity, smart money signals, 24h pump probability, projected ROI ranges, and aggregated DexScreener/Jupiter/CoinGecko market data. Costs $0.008 per query, paid automatically via x402 (USDC on Solana) or Tempo (pathUSD on Eth L2). M32 token holders receive up to 40% discount once their wallet is signature-verified. Set MPP32_AGENT_KEY in config to attribute calls to your dashboard.", {
@@ -393,7 +398,7 @@ server.tool("get_solana_token_intelligence", "Get real-time Solana token intelli
393
398
  return await legacyIntelligenceCall(token, walletAddress);
394
399
  });
395
400
  // ── Core: agent/execute path with 402 sign-and-retry ────────────────────────
396
- async function callViaAgentExecute(service, method, body, query) {
401
+ async function callViaAgentExecute(service, method, body, query, path) {
397
402
  try {
398
403
  const execUrl = new URL("/api/agent/execute", API_URL).toString();
399
404
  const reqBody = JSON.stringify({
@@ -401,6 +406,7 @@ async function callViaAgentExecute(service, method, body, query) {
401
406
  method,
402
407
  ...(body !== undefined ? { body } : {}),
403
408
  ...(query ? { query } : {}),
409
+ ...(path ? { path } : {}),
404
410
  });
405
411
  // Round 1: no payment headers
406
412
  const firstRes = await fetchWithTimeout(execUrl, {
@@ -452,11 +458,18 @@ function detectPaymentRequired(resp) {
452
458
  async function signAndRetry(execUrl, reqBody, challenge) {
453
459
  const paymentHeaders = {};
454
460
  let usedProtocol = "";
455
- // Prefer x402 if Solana key present and server offered Payment-Required
456
- if (challenge.paymentRequired && SOLANA_PRIVATE_KEY) {
461
+ // Prefer x402 if a payment-required challenge is present AND we hold a key
462
+ // for *either* the SVM or EVM side. The signer module inspects the
463
+ // challenge's `network` field and routes to the right signer; we just need
464
+ // to pass it whichever keys we have.
465
+ if (challenge.paymentRequired && (SOLANA_PRIVATE_KEY || PRIVATE_KEY)) {
457
466
  try {
458
- paymentHeaders["X-Payment"] = await completeX402Payment(challenge.paymentRequired, SOLANA_PRIVATE_KEY);
459
- usedProtocol = "USDC (x402)";
467
+ const completed = await completeX402Payment(challenge.paymentRequired, {
468
+ solana: SOLANA_PRIVATE_KEY,
469
+ evm: PRIVATE_KEY,
470
+ });
471
+ paymentHeaders["X-Payment"] = completed.xPaymentHeader;
472
+ usedProtocol = completed.protocolUsed === "x402-evm" ? "USDC (x402, Base)" : "USDC (x402, Solana)";
460
473
  }
461
474
  catch (err) {
462
475
  // Fall through to Tempo if available
@@ -683,9 +696,12 @@ function paymentFailedMessage(challenge, proto, err) {
683
696
  };
684
697
  }
685
698
  // ── Legacy path (no MPP32_AGENT_KEY) ────────────────────────────────────────
686
- async function callViaLegacyProxy(slug, method, body, query) {
699
+ async function callViaLegacyProxy(slug, method, body, query, path) {
687
700
  try {
688
701
  // Without an agent key, only native /api/proxy/<slug> is reachable.
702
+ // Native services do not need a `path` argument; if one is passed, we
703
+ // ignore it here. (The agent-execute path forwards it for external entries.)
704
+ void path;
689
705
  // We fetch /info first to detect that the slug exists as a native service.
690
706
  const infoUrl = new URL(`/api/proxy/${encodeURIComponent(slug)}/info`, API_URL).toString();
691
707
  const infoRes = await fetchWithTimeout(infoUrl);
@@ -760,10 +776,14 @@ async function callViaLegacyProxy(slug, method, body, query) {
760
776
  }
761
777
  const paymentHeaders = {};
762
778
  let usedProtocol = "";
763
- if (paymentRequired && SOLANA_PRIVATE_KEY) {
779
+ if (paymentRequired && (SOLANA_PRIVATE_KEY || PRIVATE_KEY)) {
764
780
  try {
765
- paymentHeaders["X-Payment"] = await completeX402Payment(paymentRequired, SOLANA_PRIVATE_KEY);
766
- usedProtocol = "USDC (x402)";
781
+ const completed = await completeX402Payment(paymentRequired, {
782
+ solana: SOLANA_PRIVATE_KEY,
783
+ evm: PRIVATE_KEY,
784
+ });
785
+ paymentHeaders["X-Payment"] = completed.xPaymentHeader;
786
+ usedProtocol = completed.protocolUsed === "x402-evm" ? "USDC (x402, Base)" : "USDC (x402, Solana)";
767
787
  }
768
788
  catch (err) {
769
789
  if (wwwAuth && PRIVATE_KEY) {
@@ -901,10 +921,14 @@ async function legacyIntelligenceCall(token, walletAddress) {
901
921
  const paymentRequired = res.headers.get("payment-required") ?? undefined;
902
922
  const paymentHeaders = {};
903
923
  let usedProtocol = "";
904
- if (paymentRequired && SOLANA_PRIVATE_KEY) {
924
+ if (paymentRequired && (SOLANA_PRIVATE_KEY || PRIVATE_KEY)) {
905
925
  try {
906
- paymentHeaders["X-Payment"] = await completeX402Payment(paymentRequired, SOLANA_PRIVATE_KEY);
907
- usedProtocol = "USDC (x402)";
926
+ const completed = await completeX402Payment(paymentRequired, {
927
+ solana: SOLANA_PRIVATE_KEY,
928
+ evm: PRIVATE_KEY,
929
+ });
930
+ paymentHeaders["X-Payment"] = completed.xPaymentHeader;
931
+ usedProtocol = completed.protocolUsed === "x402-evm" ? "USDC (x402, Base)" : "USDC (x402, Solana)";
908
932
  }
909
933
  catch (x402Err) {
910
934
  if (wwwAuth && PRIVATE_KEY) {
@@ -925,7 +949,7 @@ async function legacyIntelligenceCall(token, walletAddress) {
925
949
  else {
926
950
  return {
927
951
  content: [
928
- { type: "text", text: `x402 payment failed: ${x402Err instanceof Error ? x402Err.message : String(x402Err)}. Check Solana wallet balance.` },
952
+ { type: "text", text: `x402 payment failed: ${x402Err instanceof Error ? x402Err.message : String(x402Err)}. Check that the wallet for the challenge network has sufficient USDC balance.` },
929
953
  ],
930
954
  };
931
955
  }
@@ -1026,89 +1050,25 @@ async function completeTempoPayment(challengeParams, privateKey) {
1026
1050
  throw new Error(`Tempo payment failed: ${payErr instanceof Error ? payErr.message : String(payErr)}`);
1027
1051
  }
1028
1052
  }
1029
- async function completeX402Payment(paymentRequiredHeader, solanaPrivateKey) {
1030
- let requirements;
1031
- try {
1032
- requirements = JSON.parse(Buffer.from(paymentRequiredHeader, "base64").toString("utf-8"));
1033
- }
1034
- catch {
1035
- throw new Error("Could not decode Payment-Required header");
1036
- }
1037
- // We sign x402 challenges with raw Ed25519 (Solana keys are Ed25519). No
1038
- // @solana/web3.js needed — it pulls in rpc-websockets which has a
1039
- // CJS/ESM uuid incompat on Node 20+ that breaks every paid call.
1040
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1041
- let tweetnacl;
1042
- try {
1043
- const pkg = "tweetnacl";
1044
- tweetnacl = await import(pkg);
1045
- }
1046
- catch (err) {
1047
- throw new Error(`x402 signing requires tweetnacl, which ships with mpp32-mcp-server. ` +
1048
- `If you're seeing this on a clean npx install, upgrade to mpp32-mcp-server@latest. ` +
1049
- `Underlying error: ${err instanceof Error ? err.message : String(err)}`);
1050
- }
1051
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1052
- let bs58;
1053
- try {
1054
- const pkg = "bs58";
1055
- bs58 = await import(pkg);
1056
- }
1057
- catch (err) {
1058
- throw new Error(`x402 signing requires bs58, which ships with mpp32-mcp-server. ` +
1059
- `Upgrade to mpp32-mcp-server@latest. ` +
1060
- `Underlying error: ${err instanceof Error ? err.message : String(err)}`);
1061
- }
1062
- const bs58Decode = bs58.default?.decode ?? bs58.decode;
1063
- const bs58Encode = bs58.default?.encode ?? bs58.encode;
1064
- const naclSign = tweetnacl.default?.sign ?? tweetnacl.sign;
1065
- let rawKey;
1066
- try {
1067
- if (solanaPrivateKey.startsWith("[")) {
1068
- rawKey = new Uint8Array(JSON.parse(solanaPrivateKey));
1069
- }
1070
- else if (/^[0-9a-fA-F]+$/.test(solanaPrivateKey) && solanaPrivateKey.length % 2 === 0) {
1071
- rawKey = new Uint8Array(Buffer.from(solanaPrivateKey, "hex"));
1072
- }
1073
- else {
1074
- rawKey = bs58Decode(solanaPrivateKey);
1075
- }
1076
- }
1077
- catch (err) {
1078
- throw new Error(`Could not decode Solana private key: ${err instanceof Error ? err.message : String(err)}`);
1079
- }
1080
- let secretKey;
1081
- let publicKey;
1082
- if (rawKey.length === 64) {
1083
- secretKey = rawKey;
1084
- publicKey = rawKey.slice(32);
1085
- }
1086
- else if (rawKey.length === 32) {
1087
- const kp = naclSign.keyPair.fromSeed(rawKey);
1088
- secretKey = kp.secretKey;
1089
- publicKey = kp.publicKey;
1090
- }
1091
- else {
1092
- throw new Error(`Solana private key must be a 32-byte seed or 64-byte expanded key; got ${rawKey.length} bytes.`);
1093
- }
1094
- const payload = {
1095
- x402Version: 1,
1096
- scheme: requirements.scheme ?? "exact",
1097
- network: requirements.network ?? "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
1098
- payload: {
1099
- signature: "",
1100
- from: bs58Encode(publicKey),
1101
- amount: requirements.maxAmountRequired,
1102
- asset: requirements.asset,
1103
- payTo: requirements.payTo,
1104
- nonce: Date.now().toString(),
1105
- },
1053
+ // Build a real, x402-spec-compliant payment payload from the server's
1054
+ // Payment-Required challenge. For Solana-family networks, this produces a
1055
+ // base64 partially-signed VersionedTransaction (3 instructions, fee-payer
1056
+ // slot reserved for the facilitator). For Base/Base-Sepolia/Ethereum, it
1057
+ // produces an EIP-3009 transferWithAuthorization signature. Returns the
1058
+ // envelope ready to drop into the `X-Payment` HTTP header.
1059
+ async function completeX402Payment(paymentRequiredHeader, keys) {
1060
+ const solanaRpcUrl = readEnv("MPP32_SOLANA_RPC_URL");
1061
+ const result = await signX402Payment({
1062
+ paymentRequiredHeader,
1063
+ solanaKey: keys.solana,
1064
+ evmKey: keys.evm,
1065
+ solanaRpcUrl,
1066
+ });
1067
+ return {
1068
+ xPaymentHeader: result.xPaymentHeader,
1069
+ network: result.network,
1070
+ protocolUsed: result.protocolUsed,
1106
1071
  };
1107
- const message = JSON.stringify(payload.payload);
1108
- const messageBytes = new TextEncoder().encode(message);
1109
- const signed = naclSign.detached(messageBytes, secretKey);
1110
- payload.payload.signature = Buffer.from(signed).toString("base64");
1111
- return Buffer.from(JSON.stringify(payload)).toString("base64");
1112
1072
  }
1113
1073
  // ── Start ───────────────────────────────────────────────────────────────────
1114
1074
  async function main() {
@@ -0,0 +1,42 @@
1
+ export interface X402PaymentRequirements {
2
+ scheme: string;
3
+ network: string;
4
+ maxAmountRequired: string;
5
+ resource: string;
6
+ description?: string;
7
+ mimeType?: string;
8
+ payTo: string;
9
+ maxTimeoutSeconds?: number;
10
+ asset: string;
11
+ outputSchema?: unknown;
12
+ extra?: {
13
+ feePayer?: string;
14
+ name?: string;
15
+ version?: string;
16
+ decimals?: number;
17
+ [k: string]: unknown;
18
+ };
19
+ }
20
+ export interface X402PaymentEnvelope {
21
+ x402Version: number;
22
+ scheme: string;
23
+ network: string;
24
+ payload: unknown;
25
+ }
26
+ export declare function isSvmNetwork(network: string): boolean;
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>;
30
+ export interface SignX402Args {
31
+ paymentRequiredHeader: string;
32
+ solanaKey?: string;
33
+ evmKey?: string;
34
+ solanaRpcUrl?: string;
35
+ }
36
+ export interface SignX402Result {
37
+ xPaymentHeader: string;
38
+ network: string;
39
+ scheme: string;
40
+ protocolUsed: "x402-svm" | "x402-evm";
41
+ }
42
+ export declare function signX402Payment(args: SignX402Args): Promise<SignX402Result>;
@@ -0,0 +1,306 @@
1
+ // x402 protocol-compliant payment signers.
2
+ //
3
+ // Two schemes are implemented end-to-end here. Both follow the official
4
+ // `exact` scheme from https://x402.org and the reference implementation at
5
+ // https://github.com/coinbase/x402.
6
+ //
7
+ // • SVM (Solana): build a 3-instruction Solana VersionedTransaction
8
+ // (SetComputeUnitLimit, SetComputeUnitPrice, SPL-Token TransferChecked
9
+ // between Associated Token Accounts), set the facilitator-advertised
10
+ // fee payer, partially sign with the payer's Ed25519 keypair, and
11
+ // base64-encode the wire transaction. The fee-payer signature slot is
12
+ // left empty — the facilitator fills it during /settle.
13
+ // • EVM (Base / Base-Sepolia): sign an EIP-3009 `transferWithAuthorization`
14
+ // typed data message with the payer's secp256k1 key using viem. The
15
+ // resulting signature plus authorization parameters form the payload.
16
+ //
17
+ // In both cases the outer envelope is
18
+ // { x402Version: 1, scheme: "exact", network, payload: <scheme payload> }
19
+ // base64-encoded into the `X-Payment` HTTP header.
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
+ }
64
+ // ── Network classification ──────────────────────────────────────────────────
65
+ const SOLANA_MAINNET = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
66
+ const SOLANA_DEVNET = "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1";
67
+ export function isSvmNetwork(network) {
68
+ return network.startsWith("solana") || network === "solana-mainnet" || network === "solana-devnet";
69
+ }
70
+ export function isEvmNetwork(network) {
71
+ if (network.startsWith("eip155:"))
72
+ return true;
73
+ return ["base", "base-sepolia", "ethereum", "ethereum-sepolia"].includes(network);
74
+ }
75
+ function chainSpecFor(network) {
76
+ if (network === "base" || network === "eip155:8453") {
77
+ return { chainId: 8453, name: "Base", rpcUrl: "https://mainnet.base.org" };
78
+ }
79
+ if (network === "base-sepolia" || network === "eip155:84532") {
80
+ return { chainId: 84532, name: "Base Sepolia", rpcUrl: "https://sepolia.base.org" };
81
+ }
82
+ if (network === "ethereum" || network === "eip155:1") {
83
+ return { chainId: 1, name: "Ethereum", rpcUrl: "https://eth.llamarpc.com" };
84
+ }
85
+ throw new Error(`Unsupported EVM network "${network}". x402 EVM payments currently support: base, base-sepolia, ethereum.`);
86
+ }
87
+ function decodeSolanaSecret(raw, deps) {
88
+ if (raw.startsWith("[")) {
89
+ const arr = JSON.parse(raw);
90
+ if (!Array.isArray(arr))
91
+ throw new Error("Solana secret JSON array malformed");
92
+ return new Uint8Array(arr);
93
+ }
94
+ if (/^[0-9a-fA-F]+$/.test(raw) && raw.length % 2 === 0) {
95
+ return new Uint8Array(Buffer.from(raw, "hex"));
96
+ }
97
+ return deps.bs58.decode(raw);
98
+ }
99
+ async function buildSolanaSigner(rawKey, deps) {
100
+ let bytes = decodeSolanaSecret(rawKey, deps);
101
+ if (bytes.length === 32) {
102
+ // 32-byte seed — kit's createKeyPairSignerFromBytes wants the 64-byte
103
+ // expanded key. Derive via tweetnacl.
104
+ const kp = deps.nacl.sign.keyPair.fromSeed(bytes);
105
+ bytes = kp.secretKey;
106
+ }
107
+ else if (bytes.length !== 64) {
108
+ throw new Error(`Solana private key must be a 32-byte seed or a 64-byte expanded key; got ${bytes.length} bytes.`);
109
+ }
110
+ return await deps.createKeyPairSignerFromBytes(bytes);
111
+ }
112
+ // ── SVM signer ──────────────────────────────────────────────────────────────
113
+ const DEFAULT_SOLANA_RPC = "https://api.mainnet-beta.solana.com";
114
+ export async function signX402PaymentSvm(requirements, rawKey, rpcUrlOverride) {
115
+ if (requirements.scheme !== "exact") {
116
+ throw new Error(`SVM x402 scheme "${requirements.scheme}" not implemented; only "exact" is supported.`);
117
+ }
118
+ if (!requirements.extra?.feePayer) {
119
+ throw new Error(`SVM x402 challenge is missing extra.feePayer. The facilitator must advertise a fee-payer address per the x402 spec. ` +
120
+ `If you are calling MPP32 itself, upgrade the backend; if a third-party service, ask them to fix their challenge.`);
121
+ }
122
+ const decimals = requirements.extra?.decimals ?? 6;
123
+ const amount = BigInt(requirements.maxAmountRequired);
124
+ if (amount <= 0n)
125
+ throw new Error(`Invalid maxAmountRequired: ${requirements.maxAmountRequired}`);
126
+ const deps = await loadSvmDeps();
127
+ const signer = await buildSolanaSigner(rawKey, deps);
128
+ const payerAddress = signer.address;
129
+ const mintAddress = deps.address(requirements.asset);
130
+ const recipientAddress = deps.address(requirements.payTo);
131
+ const feePayerAddress = deps.address(requirements.extra.feePayer);
132
+ // Derive both sides' associated token accounts (classic SPL Token program).
133
+ const [sourceAtaTuple, destinationAtaTuple] = await Promise.all([
134
+ deps.findAssociatedTokenPda({
135
+ owner: payerAddress,
136
+ mint: mintAddress,
137
+ tokenProgram: deps.TOKEN_PROGRAM_ADDRESS,
138
+ }),
139
+ deps.findAssociatedTokenPda({
140
+ owner: recipientAddress,
141
+ mint: mintAddress,
142
+ tokenProgram: deps.TOKEN_PROGRAM_ADDRESS,
143
+ }),
144
+ ]);
145
+ const sourceAta = sourceAtaTuple[0];
146
+ const destinationAta = destinationAtaTuple[0];
147
+ const rpcUrl = rpcUrlOverride && rpcUrlOverride.length > 0 ? rpcUrlOverride : DEFAULT_SOLANA_RPC;
148
+ const rpc = deps.createSolanaRpc(rpcUrl);
149
+ const { value: latestBlockhash } = await rpc.getLatestBlockhash({ commitment: "confirmed" }).send();
150
+ const instructions = [
151
+ deps.getSetComputeUnitLimitInstruction({ units: 150_000 }),
152
+ deps.getSetComputeUnitPriceInstruction({ microLamports: 1000n }),
153
+ deps.getTransferCheckedInstruction({
154
+ source: sourceAta,
155
+ mint: mintAddress,
156
+ destination: destinationAta,
157
+ authority: signer,
158
+ amount,
159
+ decimals,
160
+ }),
161
+ ];
162
+ const message = deps.pipe(deps.createTransactionMessage({ version: 0 }), (m) => deps.setTransactionMessageFeePayer(feePayerAddress, m), (m) => deps.setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m), (m) => deps.appendTransactionMessageInstructions(instructions, m));
163
+ // Partially sign — fills the payer's signature slot, leaves the fee payer's
164
+ // slot empty for the facilitator to fill in at /settle time.
165
+ const partiallySigned = await deps.partiallySignTransactionMessageWithSigners(message);
166
+ const base64Tx = deps.getBase64EncodedWireTransaction(partiallySigned);
167
+ const envelope = {
168
+ x402Version: 1,
169
+ scheme: "exact",
170
+ network: requirements.network,
171
+ payload: { transaction: base64Tx },
172
+ };
173
+ return Buffer.from(JSON.stringify(envelope)).toString("base64");
174
+ }
175
+ // ── EVM signer (EIP-3009 transferWithAuthorization) ─────────────────────────
176
+ function randomHex32() {
177
+ const buf = Buffer.alloc(32);
178
+ for (let i = 0; i < 32; i++)
179
+ buf[i] = Math.floor(Math.random() * 256);
180
+ return ("0x" + buf.toString("hex"));
181
+ }
182
+ export async function signX402PaymentEvm(requirements, rawKey) {
183
+ if (requirements.scheme !== "exact") {
184
+ throw new Error(`EVM x402 scheme "${requirements.scheme}" not implemented; only "exact" is supported.`);
185
+ }
186
+ const chain = chainSpecFor(requirements.network);
187
+ const tokenName = requirements.extra?.name ?? "USD Coin";
188
+ const tokenVersion = requirements.extra?.version ?? "2";
189
+ const assetAddr = requirements.asset;
190
+ if (!/^0x[0-9a-fA-F]{40}$/.test(assetAddr)) {
191
+ throw new Error(`EVM x402 challenge asset is not a valid 0x address: ${requirements.asset}`);
192
+ }
193
+ const recipientAddr = requirements.payTo;
194
+ if (!/^0x[0-9a-fA-F]{40}$/.test(recipientAddr)) {
195
+ throw new Error(`EVM x402 challenge payTo is not a valid 0x address: ${requirements.payTo}`);
196
+ }
197
+ const value = BigInt(requirements.maxAmountRequired);
198
+ if (value <= 0n)
199
+ throw new Error(`Invalid maxAmountRequired: ${requirements.maxAmountRequired}`);
200
+ const keyHex = rawKey.startsWith("0x") ? rawKey : `0x${rawKey}`;
201
+ if (!/^0x[0-9a-fA-F]{64}$/.test(keyHex)) {
202
+ throw new Error("MPP32_PRIVATE_KEY must be a 64-character hex EVM private key (0x-prefixed or bare).");
203
+ }
204
+ const { privateKeyToAccount } = await loadEvmDeps();
205
+ const account = privateKeyToAccount(keyHex);
206
+ const now = Math.floor(Date.now() / 1000);
207
+ const validAfter = BigInt(0);
208
+ const validBefore = BigInt(now + (requirements.maxTimeoutSeconds ?? 600));
209
+ const nonce = randomHex32();
210
+ const domain = {
211
+ name: tokenName,
212
+ version: tokenVersion,
213
+ chainId: chain.chainId,
214
+ verifyingContract: assetAddr,
215
+ };
216
+ const types = {
217
+ TransferWithAuthorization: [
218
+ { name: "from", type: "address" },
219
+ { name: "to", type: "address" },
220
+ { name: "value", type: "uint256" },
221
+ { name: "validAfter", type: "uint256" },
222
+ { name: "validBefore", type: "uint256" },
223
+ { name: "nonce", type: "bytes32" },
224
+ ],
225
+ };
226
+ const messageObj = {
227
+ from: account.address,
228
+ to: recipientAddr,
229
+ value,
230
+ validAfter,
231
+ validBefore,
232
+ nonce,
233
+ };
234
+ const signature = await account.signTypedData({
235
+ domain,
236
+ types,
237
+ primaryType: "TransferWithAuthorization",
238
+ message: messageObj,
239
+ });
240
+ const envelope = {
241
+ x402Version: 1,
242
+ scheme: "exact",
243
+ network: requirements.network,
244
+ payload: {
245
+ signature,
246
+ authorization: {
247
+ from: messageObj.from,
248
+ to: messageObj.to,
249
+ value: messageObj.value.toString(),
250
+ validAfter: messageObj.validAfter.toString(),
251
+ validBefore: messageObj.validBefore.toString(),
252
+ nonce: messageObj.nonce,
253
+ },
254
+ },
255
+ };
256
+ return Buffer.from(JSON.stringify(envelope)).toString("base64");
257
+ }
258
+ export async function signX402Payment(args) {
259
+ let requirements;
260
+ try {
261
+ const json = Buffer.from(args.paymentRequiredHeader, "base64").toString("utf-8");
262
+ requirements = JSON.parse(json);
263
+ }
264
+ catch (err) {
265
+ throw new Error(`Could not decode Payment-Required header as base64 JSON: ${err instanceof Error ? err.message : String(err)}`);
266
+ }
267
+ if (!requirements.network)
268
+ throw new Error("x402 payment requirements missing 'network'");
269
+ if (!requirements.asset)
270
+ throw new Error("x402 payment requirements missing 'asset'");
271
+ if (!requirements.payTo)
272
+ throw new Error("x402 payment requirements missing 'payTo'");
273
+ if (!requirements.maxAmountRequired)
274
+ throw new Error("x402 payment requirements missing 'maxAmountRequired'");
275
+ if (isSvmNetwork(requirements.network)) {
276
+ if (!args.solanaKey) {
277
+ throw new Error(`Provider requires SVM payment on ${requirements.network}, but MPP32_SOLANA_PRIVATE_KEY is not configured. ` +
278
+ `Set it in your MCP config to enable USDC-on-Solana payments.`);
279
+ }
280
+ const header = await signX402PaymentSvm(requirements, args.solanaKey, args.solanaRpcUrl);
281
+ return {
282
+ xPaymentHeader: header,
283
+ network: requirements.network,
284
+ scheme: requirements.scheme,
285
+ protocolUsed: "x402-svm",
286
+ };
287
+ }
288
+ if (isEvmNetwork(requirements.network)) {
289
+ if (!args.evmKey) {
290
+ throw new Error(`Provider requires EVM payment on ${requirements.network}, but MPP32_PRIVATE_KEY is not configured. ` +
291
+ `Set it in your MCP config to enable USDC-on-Base payments.`);
292
+ }
293
+ const header = await signX402PaymentEvm(requirements, args.evmKey);
294
+ return {
295
+ xPaymentHeader: header,
296
+ network: requirements.network,
297
+ scheme: requirements.scheme,
298
+ protocolUsed: "x402-evm",
299
+ };
300
+ }
301
+ if (requirements.network === "" || requirements.network === undefined) {
302
+ throw new Error(`Provider's x402 challenge does not specify a network. We cannot pay it. Ask the provider to fix their challenge.`);
303
+ }
304
+ throw new Error(`Unsupported x402 network "${requirements.network}". Supported: solana:*, base, base-sepolia, ethereum (and their eip155:* aliases). ` +
305
+ `If this network is real and we should support it, file an issue.`);
306
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mpp32-mcp-server",
3
- "version": "1.1.4",
3
+ "version": "1.2.1",
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",
@@ -69,20 +69,20 @@
69
69
  },
70
70
  "dependencies": {
71
71
  "@modelcontextprotocol/sdk": "^1.12.0",
72
+ "@solana-program/compute-budget": "^0.15.0",
73
+ "@solana-program/token": "^0.13.0",
74
+ "@solana/kit": "^6.9.0",
72
75
  "bs58": "^6.0.0",
73
76
  "tweetnacl": "^1.0.3",
77
+ "viem": "^2.48.11",
74
78
  "zod": "^3.23.0"
75
79
  },
76
80
  "peerDependencies": {
77
- "mppx": ">=0.4.0",
78
- "viem": ">=2.0.0"
81
+ "mppx": ">=0.4.0"
79
82
  },
80
83
  "peerDependenciesMeta": {
81
84
  "mppx": {
82
85
  "optional": true
83
- },
84
- "viem": {
85
- "optional": true
86
86
  }
87
87
  },
88
88
  "devDependencies": {