mpp32-mcp-server 1.1.1 → 1.1.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.
Files changed (3) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/dist/index.js +252 -54
  3. package/package.json +4 -1
package/CHANGELOG.md ADDED
@@ -0,0 +1,112 @@
1
+ # Changelog
2
+
3
+ All notable changes to `mpp32-mcp-server` are documented here. The format
4
+ follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the
5
+ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [1.1.3] - 2026-05-11
8
+
9
+ ### Fixed
10
+
11
+ * **x402 payments actually work now.** `completeX402Payment` previously
12
+ dynamically imported `@solana/web3.js`, `bs58`, and `tweetnacl`, but
13
+ none of those packages were declared in `dependencies`. On a clean
14
+ `npx mpp32-mcp-server` install they were missing from `node_modules`,
15
+ the import threw, the catch block returned a misleading "check wallet
16
+ balance" message, and Claude paraphrased that as "no funded Solana
17
+ wallet configured" even when `MPP32_SOLANA_PRIVATE_KEY` was set.
18
+ * **Dropped `@solana/web3.js` entirely.** Adding it to `dependencies`
19
+ uncovered a second, deeper bug: its transitive `rpc-websockets`
20
+ bundle is CJS and `require()`s a now ESM-only `uuid`, which throws
21
+ `ERR_REQUIRE_ESM` on Node 20+ the first time the signer loads. We
22
+ only ever used `Keypair.fromSecretKey` and `publicKey.toBase58()`,
23
+ both of which are trivial Ed25519/base58 operations. Signing is now
24
+ done directly with `tweetnacl` + `bs58`, so the failure mode is gone
25
+ and the install footprint is ~30 MB smaller.
26
+ * **Properly declared signing dependencies.** `bs58` and `tweetnacl`
27
+ are now real `dependencies` rather than implicit assumptions.
28
+ * **Better error when a payment dependency genuinely fails to load.**
29
+ The catch around each dynamic import now tells the user the package
30
+ ships with `mpp32-mcp-server` and directs them to upgrade to
31
+ `@latest` instead of suggesting a manual `npm install` that won't
32
+ stick across `npx` runs.
33
+ * **32-byte seed Solana keys now accepted** in addition to 64-byte
34
+ expanded keys (previously `Keypair.fromSecretKey` rejected seeds).
35
+
36
+ ### Added
37
+
38
+ * **Catalog total surfaced in `list_mpp32_services`.** The federated
39
+ catalog has 4,500+ external services but the API returns at most 500
40
+ per call (default 100). The response now includes
41
+ `data.totalAvailable` and a `truncated`/`hint` pair so the model
42
+ knows results were paginated and how to drill in with `q`,
43
+ `category`, `source`, or `protocol`. The MCP client formats the
44
+ header as `Found N services (of M total available in catalog)` and
45
+ prints the hint when applicable.
46
+
47
+ ## [1.1.2] - 2026-05-10
48
+
49
+ ### Fixed
50
+
51
+ * **Configuration robustness.** Every environment variable is now trimmed,
52
+ stripped of a leading byte-order mark, and unwrapped if the value was
53
+ pasted with literal surrounding quotes. A trailing newline or stray
54
+ whitespace in `MPP32_AGENT_KEY` used to make Node's HTTP client throw
55
+ `ERR_INVALID_CHAR` on every catalog call. That failure mode is now
56
+ impossible.
57
+ * **ASCII-only header guard.** Any non-ASCII byte in a value that would
58
+ end up in an HTTP header is rejected at startup with a clear message
59
+ pointing at the offending variable, instead of a low-level fetch error
60
+ at call time.
61
+ * **Format validation.** `MPP32_AGENT_KEY` must look like
62
+ `mpp32_agent_*`. `MPP32_PRIVATE_KEY` must be a 0x-prefixed 64-character
63
+ EVM hex key. `MPP32_SOLANA_PRIVATE_KEY` is recognized as base58, hex,
64
+ or a JSON byte-array. Malformed values warn at startup with the first
65
+ hex bytes so invisible characters are easy to spot.
66
+ * **Case-insensitive payment challenge parsing.** `Payment-Required` and
67
+ `payment-required` headers are now treated identically.
68
+ * **Liberal `WWW-Authenticate` parser.** Token regex broadened to RFC
69
+ 7235 token68 so valid challenges with `+`, `/`, `:`, and `,` no longer
70
+ get silently dropped.
71
+ * **`walletAddress` sanitization.** The optional `walletAddress` argument
72
+ to `get_solana_token_intelligence` is trimmed and ASCII-validated
73
+ before going into the `X-Wallet-Address` header.
74
+
75
+ ### Added
76
+
77
+ * **Request timeouts.** Every outbound fetch is wrapped with an
78
+ `AbortController`. Default 30 seconds, configurable through
79
+ `MPP32_TIMEOUT_MS` (1000-300000).
80
+ * **Startup banner.** The server prints the version, API base URL,
81
+ configured keys, and timeout to stderr on start, so misconfigurations
82
+ are visible without making a call.
83
+
84
+ ### Internal
85
+
86
+ * `SERVER_VERSION` is now a single constant, so the protocol handshake,
87
+ banner, and `package.json` cannot drift apart.
88
+
89
+ ## [1.1.1] - 2026-05-09
90
+
91
+ ### Added
92
+
93
+ * `mcpName` field added to `package.json` to satisfy the MCP registry
94
+ publishing requirement.
95
+ * First registry listing under `io.github.MPP32/mpp32-mcp-server` at
96
+ `registry.modelcontextprotocol.io`.
97
+
98
+ ## [1.1.0] - 2026-05-09
99
+
100
+ ### Added
101
+
102
+ * Three tools: `list_mpp32_services`, `call_mpp32_endpoint`,
103
+ `get_solana_token_intelligence`.
104
+ * End to end support for five payment rails: x402, Tempo, ACP, AP2,
105
+ AGTP. Server picks the rail the wallet is funded for and falls back
106
+ if the first attempt does not settle.
107
+ * Federated catalog (native, curated free, x402 Bazaar, MCP registry).
108
+ * Automatic 402 sign-and-retry through `/api/agent/execute`.
109
+
110
+ ## [1.0.0] - 2026-05-08
111
+
112
+ * Initial public release on npm.
package/dist/index.js CHANGED
@@ -2,20 +2,165 @@
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 API_URL = process.env.MPP32_API_URL?.replace(/\/$/, "") || "https://mpp32.org";
6
- const PRIVATE_KEY = process.env.MPP32_PRIVATE_KEY;
7
- const SOLANA_PRIVATE_KEY = process.env.MPP32_SOLANA_PRIVATE_KEY;
8
- // MPP32_AGENT_KEY is the canonical name; MPP32_API_KEY is accepted as an alias
9
- // because earlier docs used that name.
10
- const AGENT_KEY = process.env.MPP32_AGENT_KEY ?? process.env.MPP32_API_KEY;
5
+ const SERVER_VERSION = "1.1.3";
6
+ // ── Env loading: trim and sanitize aggressively ─────────────────────────────
7
+ // Copy-paste from Claude Desktop / Cursor / Windsurf JSON config UIs frequently
8
+ // adds trailing \n, \r, NBSP, BOM, or wraps the value in literal quotes. Any
9
+ // non-ASCII byte (including a stray newline) in a value that ends up in an
10
+ // HTTP header makes Node's undici fetch throw ERR_INVALID_CHAR, which used to
11
+ // surface as a confusing "invalid byte character" error on every catalog call.
12
+ function readEnv(name) {
13
+ const raw = process.env[name];
14
+ if (raw === undefined)
15
+ return undefined;
16
+ let v = raw;
17
+ if (v.charCodeAt(0) === 0xfeff)
18
+ v = v.slice(1);
19
+ v = v.trim();
20
+ if (v.length >= 2 &&
21
+ ((v.startsWith('"') && v.endsWith('"')) ||
22
+ (v.startsWith("'") && v.endsWith("'")))) {
23
+ v = v.slice(1, -1).trim();
24
+ }
25
+ if (v.length === 0)
26
+ return undefined;
27
+ return v;
28
+ }
29
+ function isPrintableAscii(v) {
30
+ for (let i = 0; i < v.length; i++) {
31
+ const c = v.charCodeAt(i);
32
+ if (c < 0x20 || c > 0x7e)
33
+ return false;
34
+ }
35
+ return true;
36
+ }
37
+ function safeHeaderValue(name, value) {
38
+ if (!isPrintableAscii(value)) {
39
+ throw new Error(`Environment variable contains non-printable or non-ASCII characters that cannot be sent as an HTTP header (${name}). ` +
40
+ `Re-copy the value from https://mpp32.org/agent-console without any surrounding whitespace, quotes, or newlines.`);
41
+ }
42
+ return value;
43
+ }
44
+ function describeEnvProblem(name, raw, expected) {
45
+ const hex = Array.from(raw.slice(0, 4))
46
+ .map((c) => c.charCodeAt(0).toString(16).padStart(2, "0"))
47
+ .join(" ");
48
+ return `${name} looks malformed. Expected ${expected}. First bytes: 0x${hex}. Re-copy from ${API_URL}/agent-console.`;
49
+ }
50
+ const RAW_API_URL = readEnv("MPP32_API_URL") ?? "https://mpp32.org";
51
+ const API_URL = (() => {
52
+ try {
53
+ const u = new URL(RAW_API_URL.replace(/\/+$/, ""));
54
+ if (u.protocol !== "https:" && u.protocol !== "http:") {
55
+ throw new Error(`MPP32_API_URL must be http(s), got ${u.protocol}`);
56
+ }
57
+ return u.toString().replace(/\/+$/, "");
58
+ }
59
+ catch (err) {
60
+ console.error(`[mpp32] MPP32_API_URL is not a valid URL: ${err instanceof Error ? err.message : String(err)}. ` +
61
+ `Falling back to https://mpp32.org.`);
62
+ return "https://mpp32.org";
63
+ }
64
+ })();
65
+ // Default request timeout. Configurable via MPP32_TIMEOUT_MS.
66
+ const TIMEOUT_MS = (() => {
67
+ const raw = readEnv("MPP32_TIMEOUT_MS");
68
+ if (!raw)
69
+ return 30_000;
70
+ const n = Number.parseInt(raw, 10);
71
+ if (!Number.isFinite(n) || n < 1_000 || n > 300_000) {
72
+ console.error(`[mpp32] MPP32_TIMEOUT_MS=${raw} out of range. Using 30000ms.`);
73
+ return 30_000;
74
+ }
75
+ return n;
76
+ })();
77
+ // MPP32_AGENT_KEY is canonical; MPP32_API_KEY is an accepted alias from older docs.
78
+ const AGENT_KEY = (() => {
79
+ const v = readEnv("MPP32_AGENT_KEY") ?? readEnv("MPP32_API_KEY");
80
+ if (!v)
81
+ return undefined;
82
+ if (!isPrintableAscii(v)) {
83
+ console.error(`[mpp32] MPP32_AGENT_KEY contains non-ASCII characters and will be ignored. ` +
84
+ `Re-copy the key from ${API_URL}/agent-console.`);
85
+ return undefined;
86
+ }
87
+ if (!/^mpp32_agent_[A-Za-z0-9_-]+$/.test(v)) {
88
+ console.error(`[mpp32] ${describeEnvProblem("MPP32_AGENT_KEY", v, "a value starting with 'mpp32_agent_'")}`);
89
+ }
90
+ return v;
91
+ })();
92
+ const PRIVATE_KEY = (() => {
93
+ const v = readEnv("MPP32_PRIVATE_KEY");
94
+ if (!v)
95
+ return undefined;
96
+ if (!isPrintableAscii(v)) {
97
+ console.error(`[mpp32] MPP32_PRIVATE_KEY contains non-ASCII characters and will be ignored. ` +
98
+ `Re-paste the hex key (0x-prefixed or 64 hex chars).`);
99
+ return undefined;
100
+ }
101
+ if (!/^(0x)?[0-9a-fA-F]{64}$/.test(v)) {
102
+ console.error(`[mpp32] ${describeEnvProblem("MPP32_PRIVATE_KEY", v, "0x-prefixed 64-hex-char EVM private key")}`);
103
+ }
104
+ return v;
105
+ })();
106
+ const SOLANA_PRIVATE_KEY = (() => {
107
+ const v = readEnv("MPP32_SOLANA_PRIVATE_KEY");
108
+ if (!v)
109
+ return undefined;
110
+ if (!isPrintableAscii(v)) {
111
+ console.error(`[mpp32] MPP32_SOLANA_PRIVATE_KEY contains non-ASCII characters and will be ignored. ` +
112
+ `Re-paste the base58 (or [byte,byte,...] array, or hex) key.`);
113
+ return undefined;
114
+ }
115
+ const looksValid = v.startsWith("[") ||
116
+ /^[0-9a-fA-F]+$/.test(v) ||
117
+ /^[1-9A-HJ-NP-Za-km-z]{43,90}$/.test(v); // base58
118
+ if (!looksValid) {
119
+ console.error(`[mpp32] ${describeEnvProblem("MPP32_SOLANA_PRIVATE_KEY", v, "base58 string, hex string, or [byte,byte,...] array")}`);
120
+ }
121
+ return v;
122
+ })();
123
+ // Wrap fetch with a default timeout. AbortSignal.timeout exists in Node 20+,
124
+ // but we ship for Node 18+, so we build the signal ourselves.
125
+ async function fetchWithTimeout(url, init) {
126
+ const controller = new AbortController();
127
+ const timer = setTimeout(() => controller.abort(), init?.timeoutMs ?? TIMEOUT_MS);
128
+ try {
129
+ return await fetch(url, { ...init, signal: controller.signal });
130
+ }
131
+ catch (err) {
132
+ if (err instanceof Error && err.name === "AbortError") {
133
+ throw new Error(`Request to ${url} timed out after ${init?.timeoutMs ?? TIMEOUT_MS}ms. ` +
134
+ `Set MPP32_TIMEOUT_MS in your MCP config to extend.`);
135
+ }
136
+ throw err;
137
+ }
138
+ finally {
139
+ clearTimeout(timer);
140
+ }
141
+ }
142
+ // Lowercase all keys in a headers-like object. The backend may emit
143
+ // "Payment-Required" or "payment-required"; downstream code must not care.
144
+ function lowercaseHeaderKeys(obj) {
145
+ if (!obj)
146
+ return {};
147
+ const out = {};
148
+ for (const [k, v] of Object.entries(obj))
149
+ out[k.toLowerCase()] = v;
150
+ return out;
151
+ }
11
152
  const server = new McpServer({
12
153
  name: "mpp32",
13
- version: "1.1.0",
154
+ version: SERVER_VERSION,
14
155
  });
15
156
  function buildHeaders(extra = {}) {
16
- const headers = { ...extra };
17
- if (AGENT_KEY)
18
- headers["X-Agent-Key"] = AGENT_KEY;
157
+ const headers = {};
158
+ for (const [k, v] of Object.entries(extra)) {
159
+ headers[k] = safeHeaderValue(k, v);
160
+ }
161
+ if (AGENT_KEY) {
162
+ headers["X-Agent-Key"] = safeHeaderValue("MPP32_AGENT_KEY", AGENT_KEY);
163
+ }
19
164
  return headers;
20
165
  }
21
166
  function isHttpCallable(svc) {
@@ -29,7 +174,7 @@ function isHttpCallable(svc) {
29
174
  return /^https?:\/\//.test(url);
30
175
  }
31
176
  // ── Tool 1: list_mpp32_services ─────────────────────────────────────────────
32
- server.tool("list_mpp32_services", "Browse the MPP32 federated catalog of machine-payable APIs and data services. Includes native MPP32 services (callable end-to-end through this MCP), the x402 Bazaar (USDC on Solana), curated free APIs (DexScreener, Jupiter, CoinGecko health, httpbin, etc.), and the public MCP Registry (npx-installable servers; listing-only). Each result indicates whether it is callable through `call_mpp32_endpoint` or listing-only. Use the `category`, `q`, or `source` filters to narrow down.", {
177
+ server.tool("list_mpp32_services", "Browse the MPP32 federated catalog of 4,500+ machine-payable APIs and data services. Includes native MPP32 services (callable end-to-end through this MCP), the x402 Bazaar (USDC on Solana), curated free APIs (DexScreener, Jupiter, CoinGecko health, httpbin, etc.), and the public MCP Registry (npx-installable servers; listing-only). Each result indicates whether it is callable through `call_mpp32_endpoint` or listing-only. The catalog is large (~4,500 entries) — by default a single call returns up to 100 results and the response will tell you the true total and whether the page was truncated. Use `q` (free-text search), `category`, `source`, or `protocol` to narrow down, or raise `limit` (max 500) for broader pages.", {
33
178
  category: z
34
179
  .string()
35
180
  .optional()
@@ -59,7 +204,7 @@ server.tool("list_mpp32_services", "Browse the MPP32 federated catalog of machin
59
204
  if (source)
60
205
  url.searchParams.set("source", source);
61
206
  url.searchParams.set("limit", String(limit ?? 100));
62
- const res = await fetch(url.toString(), { headers: buildHeaders() });
207
+ const res = await fetchWithTimeout(url.toString(), { headers: buildHeaders() });
63
208
  if (!res.ok) {
64
209
  return {
65
210
  content: [
@@ -106,17 +251,24 @@ server.tool("list_mpp32_services", "Browse the MPP32 federated catalog of machin
106
251
  .join("\n");
107
252
  });
108
253
  const counts = json.data.counts;
254
+ const totalAvailable = json.data.totalAvailable;
109
255
  const callableCount = services.filter(isHttpCallable).length;
256
+ const sourcesLine = totalAvailable
257
+ ? `**Sources:** ${counts.native} native + ${counts.external} external (of ${totalAvailable.combined} total available in catalog). **Callable through this MCP:** ${callableCount}.`
258
+ : `**Sources:** ${counts.native} native + ${counts.external} external. **Callable through this MCP:** ${callableCount}.`;
110
259
  const header = [
111
260
  `# MPP32 Federated Catalog — ${services.length} result${services.length !== 1 ? "s" : ""}`,
112
261
  ``,
113
- `**Sources:** ${counts.native} native + ${counts.external} external. **Callable through this MCP:** ${callableCount}.`,
262
+ sourcesLine,
263
+ json.data.truncated && json.data.hint ? `\n> ⚠️ ${json.data.hint}` : ``,
114
264
  ``,
115
265
  AGENT_KEY
116
266
  ? `Calls through \`call_mpp32_endpoint\` are tracked in your dashboard at ${API_URL}/agent-console (your X-Agent-Key is set).`
117
267
  : `**Tip:** set \`MPP32_AGENT_KEY\` in your MCP config to track usage at ${API_URL}/agent-console. Get a key at ${API_URL}/agent-console.`,
118
268
  ``,
119
- ].join("\n");
269
+ ]
270
+ .filter((l) => l !== ``)
271
+ .join("\n");
120
272
  return {
121
273
  content: [{ type: "text", text: header + "\n" + lines.join("\n\n") }],
122
274
  };
@@ -194,7 +346,7 @@ async function callViaAgentExecute(service, method, body, query) {
194
346
  ...(query ? { query } : {}),
195
347
  });
196
348
  // Round 1: no payment headers
197
- const firstRes = await fetch(execUrl, {
349
+ const firstRes = await fetchWithTimeout(execUrl, {
198
350
  method: "POST",
199
351
  headers: buildHeaders({ "Content-Type": "application/json" }),
200
352
  body: reqBody,
@@ -231,7 +383,7 @@ function detectPaymentRequired(resp) {
231
383
  const result = resp?.data?.result;
232
384
  if (!result?.error || result.error.code !== "PAYMENT_REQUIRED")
233
385
  return null;
234
- const headers = result.error.challenge?.headers ?? {};
386
+ const headers = lowercaseHeaderKeys(result.error.challenge?.headers);
235
387
  return {
236
388
  wwwAuthenticate: headers["www-authenticate"],
237
389
  paymentRequired: headers["payment-required"],
@@ -308,7 +460,7 @@ async function signAndRetry(execUrl, reqBody, challenge) {
308
460
  };
309
461
  }
310
462
  // Round 2: with payment headers
311
- const secondRes = await fetch(execUrl, {
463
+ const secondRes = await fetchWithTimeout(execUrl, {
312
464
  method: "POST",
313
465
  headers: buildHeaders({ "Content-Type": "application/json", ...paymentHeaders }),
314
466
  body: reqBody,
@@ -437,7 +589,9 @@ function paymentKeyMissingMessage(resp, challenge) {
437
589
  ' "command": "npx",',
438
590
  ' "args": ["mpp32-mcp-server"],',
439
591
  ' "env": {',
440
- AGENT_KEY ? ` "MPP32_AGENT_KEY": "${AGENT_KEY.slice(0, 12)}…",` : "",
592
+ AGENT_KEY
593
+ ? ` "MPP32_AGENT_KEY": "${AGENT_KEY.slice(0, 12).replace(/[^A-Za-z0-9_-]/g, "?")}…",`
594
+ : "",
441
595
  ' "MPP32_SOLANA_PRIVATE_KEY": "<solana-base58-key for USDC>",',
442
596
  ' "MPP32_PRIVATE_KEY": "<EVM-hex-key for pathUSD>"',
443
597
  " }",
@@ -477,7 +631,7 @@ async function callViaLegacyProxy(slug, method, body, query) {
477
631
  // Without an agent key, only native /api/proxy/<slug> is reachable.
478
632
  // We fetch /info first to detect that the slug exists as a native service.
479
633
  const infoUrl = new URL(`/api/proxy/${encodeURIComponent(slug)}/info`, API_URL).toString();
480
- const infoRes = await fetch(infoUrl);
634
+ const infoRes = await fetchWithTimeout(infoUrl);
481
635
  if (!infoRes.ok) {
482
636
  return {
483
637
  content: [
@@ -500,7 +654,7 @@ async function callViaLegacyProxy(slug, method, body, query) {
500
654
  const baseHeaders = { Accept: "application/json" };
501
655
  if (body !== undefined)
502
656
  baseHeaders["Content-Type"] = "application/json";
503
- const challengeRes = await fetch(proxyUrl.toString(), {
657
+ const challengeRes = await fetchWithTimeout(proxyUrl.toString(), {
504
658
  method,
505
659
  headers: baseHeaders,
506
660
  body: method !== "GET" && body !== undefined ? JSON.stringify(body) : undefined,
@@ -592,7 +746,7 @@ async function callViaLegacyProxy(slug, method, body, query) {
592
746
  ],
593
747
  };
594
748
  }
595
- const paidRes = await fetch(proxyUrl.toString(), {
749
+ const paidRes = await fetchWithTimeout(proxyUrl.toString(), {
596
750
  method,
597
751
  headers: { ...baseHeaders, ...paymentHeaders },
598
752
  body: method !== "GET" && body !== undefined ? JSON.stringify(body) : undefined,
@@ -628,9 +782,21 @@ async function callViaLegacyProxy(slug, method, body, query) {
628
782
  async function legacyIntelligenceCall(token, walletAddress) {
629
783
  try {
630
784
  const reqHeaders = { "Content-Type": "application/json" };
631
- if (walletAddress)
632
- reqHeaders["X-Wallet-Address"] = walletAddress;
633
- const res = await fetch(`${API_URL}/api/intelligence`, {
785
+ if (walletAddress) {
786
+ const trimmed = walletAddress.trim();
787
+ if (!isPrintableAscii(trimmed)) {
788
+ return {
789
+ content: [
790
+ {
791
+ type: "text",
792
+ text: `walletAddress contains non-ASCII characters. Pass a Solana base58 address only.`,
793
+ },
794
+ ],
795
+ };
796
+ }
797
+ reqHeaders["X-Wallet-Address"] = trimmed;
798
+ }
799
+ const res = await fetchWithTimeout(`${API_URL}/api/intelligence`, {
634
800
  method: "POST",
635
801
  headers: reqHeaders,
636
802
  body: JSON.stringify({ token }),
@@ -723,7 +889,7 @@ async function legacyIntelligenceCall(token, walletAddress) {
723
889
  };
724
890
  }
725
891
  }
726
- const paidRes = await fetch(`${API_URL}/api/intelligence`, {
892
+ const paidRes = await fetchWithTimeout(`${API_URL}/api/intelligence`, {
727
893
  method: "POST",
728
894
  headers: { ...reqHeaders, ...paymentHeaders },
729
895
  body: JSON.stringify({ token }),
@@ -762,13 +928,19 @@ function parseWwwAuthenticate(header) {
762
928
  const match = header.match(/^(\w+)\s+(.+)$/);
763
929
  if (!match)
764
930
  return { scheme: null, params: {} };
765
- const scheme = match[1];
766
- const rest = match[2];
931
+ const scheme = match[1] ?? null;
932
+ const rest = match[2] ?? "";
767
933
  const params = {};
768
- const paramRegex = /(\w+)=(?:"([^"]*)"|([\w.+/=-]+))/g;
934
+ // Tokens per RFC 7235: quoted-string OR a token68-ish value covering all
935
+ // base64url, base58, hex, JSON-pointers, etc. Liberal on purpose so we do
936
+ // not silently drop valid challenges.
937
+ const paramRegex = /([A-Za-z0-9_-]+)=(?:"((?:[^"\\]|\\.)*)"|([^\s,]+))/g;
769
938
  let m;
770
939
  while ((m = paramRegex.exec(rest)) !== null) {
771
- params[m[1]] = m[2] ?? m[3];
940
+ const key = m[1];
941
+ const val = m[2] ?? m[3];
942
+ if (key && val !== undefined)
943
+ params[key] = val;
772
944
  }
773
945
  return { scheme, params };
774
946
  }
@@ -805,40 +977,70 @@ async function completeX402Payment(paymentRequiredHeader, solanaPrivateKey) {
805
977
  catch {
806
978
  throw new Error("Could not decode Payment-Required header");
807
979
  }
980
+ // We sign x402 challenges with raw Ed25519 (Solana keys are Ed25519). No
981
+ // @solana/web3.js needed — it pulls in rpc-websockets which has a
982
+ // CJS/ESM uuid incompat on Node 20+ that breaks every paid call.
808
983
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
809
- let solanaWeb3;
984
+ let tweetnacl;
810
985
  try {
811
- const pkg = "@solana/web3.js";
812
- solanaWeb3 = await import(pkg);
986
+ const pkg = "tweetnacl";
987
+ tweetnacl = await import(pkg);
813
988
  }
814
- catch {
815
- throw new Error("x402 payment requires @solana/web3.js: npm install @solana/web3.js");
989
+ catch (err) {
990
+ throw new Error(`x402 signing requires tweetnacl, which ships with mpp32-mcp-server. ` +
991
+ `If you're seeing this on a clean npx install, upgrade to mpp32-mcp-server@latest. ` +
992
+ `Underlying error: ${err instanceof Error ? err.message : String(err)}`);
816
993
  }
817
994
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
818
- let keypair;
995
+ let bs58;
996
+ try {
997
+ const pkg = "bs58";
998
+ bs58 = await import(pkg);
999
+ }
1000
+ catch (err) {
1001
+ throw new Error(`x402 signing requires bs58, which ships with mpp32-mcp-server. ` +
1002
+ `Upgrade to mpp32-mcp-server@latest. ` +
1003
+ `Underlying error: ${err instanceof Error ? err.message : String(err)}`);
1004
+ }
1005
+ const bs58Decode = bs58.default?.decode ?? bs58.decode;
1006
+ const bs58Encode = bs58.default?.encode ?? bs58.encode;
1007
+ const naclSign = tweetnacl.default?.sign ?? tweetnacl.sign;
1008
+ let rawKey;
819
1009
  try {
820
1010
  if (solanaPrivateKey.startsWith("[")) {
821
- keypair = solanaWeb3.Keypair.fromSecretKey(new Uint8Array(JSON.parse(solanaPrivateKey)));
1011
+ rawKey = new Uint8Array(JSON.parse(solanaPrivateKey));
822
1012
  }
823
1013
  else if (/^[0-9a-fA-F]+$/.test(solanaPrivateKey) && solanaPrivateKey.length % 2 === 0) {
824
- keypair = solanaWeb3.Keypair.fromSecretKey(new Uint8Array(Buffer.from(solanaPrivateKey, "hex")));
1014
+ rawKey = new Uint8Array(Buffer.from(solanaPrivateKey, "hex"));
825
1015
  }
826
1016
  else {
827
- const bs58Pkg = "bs58";
828
- const bs58 = await import(bs58Pkg);
829
- keypair = solanaWeb3.Keypair.fromSecretKey(bs58.default.decode(solanaPrivateKey));
1017
+ rawKey = bs58Decode(solanaPrivateKey);
830
1018
  }
831
1019
  }
832
1020
  catch (err) {
833
1021
  throw new Error(`Could not decode Solana private key: ${err instanceof Error ? err.message : String(err)}`);
834
1022
  }
1023
+ let secretKey;
1024
+ let publicKey;
1025
+ if (rawKey.length === 64) {
1026
+ secretKey = rawKey;
1027
+ publicKey = rawKey.slice(32);
1028
+ }
1029
+ else if (rawKey.length === 32) {
1030
+ const kp = naclSign.keyPair.fromSeed(rawKey);
1031
+ secretKey = kp.secretKey;
1032
+ publicKey = kp.publicKey;
1033
+ }
1034
+ else {
1035
+ throw new Error(`Solana private key must be a 32-byte seed or 64-byte expanded key; got ${rawKey.length} bytes.`);
1036
+ }
835
1037
  const payload = {
836
1038
  x402Version: 1,
837
1039
  scheme: requirements.scheme ?? "exact",
838
1040
  network: requirements.network ?? "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
839
1041
  payload: {
840
1042
  signature: "",
841
- from: keypair.publicKey.toBase58(),
1043
+ from: bs58Encode(publicKey),
842
1044
  amount: requirements.maxAmountRequired,
843
1045
  asset: requirements.asset,
844
1046
  payTo: requirements.payTo,
@@ -847,17 +1049,7 @@ async function completeX402Payment(paymentRequiredHeader, solanaPrivateKey) {
847
1049
  };
848
1050
  const message = JSON.stringify(payload.payload);
849
1051
  const messageBytes = new TextEncoder().encode(message);
850
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
851
- let tweetnacl;
852
- try {
853
- const pkg = "tweetnacl";
854
- tweetnacl = await import(pkg);
855
- }
856
- catch {
857
- throw new Error("x402 signing requires tweetnacl: npm install tweetnacl");
858
- }
859
- const naclSign = tweetnacl.default?.sign ?? tweetnacl.sign;
860
- const signed = naclSign.detached(messageBytes, keypair.secretKey);
1052
+ const signed = naclSign.detached(messageBytes, secretKey);
861
1053
  payload.payload.signature = Buffer.from(signed).toString("base64");
862
1054
  return Buffer.from(JSON.stringify(payload)).toString("base64");
863
1055
  }
@@ -865,8 +1057,14 @@ async function completeX402Payment(paymentRequiredHeader, solanaPrivateKey) {
865
1057
  async function main() {
866
1058
  const transport = new StdioServerTransport();
867
1059
  await server.connect(transport);
868
- const keyHint = AGENT_KEY ? "agent-key configured" : "no agent-key (legacy mode)";
869
- console.error(`MPP32 MCP server v1.1.1 running on stdio. ${keyHint}`);
1060
+ const features = [
1061
+ AGENT_KEY ? "agent-key" : null,
1062
+ SOLANA_PRIVATE_KEY ? "x402-key" : null,
1063
+ PRIVATE_KEY ? "tempo-key" : null,
1064
+ ]
1065
+ .filter(Boolean)
1066
+ .join(", ") || "no keys (catalog-only legacy mode)";
1067
+ console.error(`[mpp32] MCP server v${SERVER_VERSION} on stdio. API ${API_URL}. Configured: ${features}. Timeout ${TIMEOUT_MS}ms.`);
870
1068
  }
871
1069
  main().catch((err) => {
872
1070
  console.error("Fatal:", err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mpp32-mcp-server",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
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",
@@ -19,6 +19,7 @@
19
19
  "dist/**/*.js",
20
20
  "dist/**/*.d.ts",
21
21
  "README.md",
22
+ "CHANGELOG.md",
22
23
  "LICENSE"
23
24
  ],
24
25
  "sideEffects": false,
@@ -68,6 +69,8 @@
68
69
  },
69
70
  "dependencies": {
70
71
  "@modelcontextprotocol/sdk": "^1.12.0",
72
+ "bs58": "^6.0.0",
73
+ "tweetnacl": "^1.0.3",
71
74
  "zod": "^3.23.0"
72
75
  },
73
76
  "peerDependencies": {