mpp32-mcp-server 1.1.1 → 1.1.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.
Files changed (3) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/dist/index.js +199 -28
  3. package/package.json +2 -1
package/CHANGELOG.md ADDED
@@ -0,0 +1,72 @@
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.2] - 2026-05-10
8
+
9
+ ### Fixed
10
+
11
+ * **Configuration robustness.** Every environment variable is now trimmed,
12
+ stripped of a leading byte-order mark, and unwrapped if the value was
13
+ pasted with literal surrounding quotes. A trailing newline or stray
14
+ whitespace in `MPP32_AGENT_KEY` used to make Node's HTTP client throw
15
+ `ERR_INVALID_CHAR` on every catalog call. That failure mode is now
16
+ impossible.
17
+ * **ASCII-only header guard.** Any non-ASCII byte in a value that would
18
+ end up in an HTTP header is rejected at startup with a clear message
19
+ pointing at the offending variable, instead of a low-level fetch error
20
+ at call time.
21
+ * **Format validation.** `MPP32_AGENT_KEY` must look like
22
+ `mpp32_agent_*`. `MPP32_PRIVATE_KEY` must be a 0x-prefixed 64-character
23
+ EVM hex key. `MPP32_SOLANA_PRIVATE_KEY` is recognized as base58, hex,
24
+ or a JSON byte-array. Malformed values warn at startup with the first
25
+ hex bytes so invisible characters are easy to spot.
26
+ * **Case-insensitive payment challenge parsing.** `Payment-Required` and
27
+ `payment-required` headers are now treated identically.
28
+ * **Liberal `WWW-Authenticate` parser.** Token regex broadened to RFC
29
+ 7235 token68 so valid challenges with `+`, `/`, `:`, and `,` no longer
30
+ get silently dropped.
31
+ * **`walletAddress` sanitization.** The optional `walletAddress` argument
32
+ to `get_solana_token_intelligence` is trimmed and ASCII-validated
33
+ before going into the `X-Wallet-Address` header.
34
+
35
+ ### Added
36
+
37
+ * **Request timeouts.** Every outbound fetch is wrapped with an
38
+ `AbortController`. Default 30 seconds, configurable through
39
+ `MPP32_TIMEOUT_MS` (1000-300000).
40
+ * **Startup banner.** The server prints the version, API base URL,
41
+ configured keys, and timeout to stderr on start, so misconfigurations
42
+ are visible without making a call.
43
+
44
+ ### Internal
45
+
46
+ * `SERVER_VERSION` is now a single constant, so the protocol handshake,
47
+ banner, and `package.json` cannot drift apart.
48
+
49
+ ## [1.1.1] - 2026-05-09
50
+
51
+ ### Added
52
+
53
+ * `mcpName` field added to `package.json` to satisfy the MCP registry
54
+ publishing requirement.
55
+ * First registry listing under `io.github.MPP32/mpp32-mcp-server` at
56
+ `registry.modelcontextprotocol.io`.
57
+
58
+ ## [1.1.0] - 2026-05-09
59
+
60
+ ### Added
61
+
62
+ * Three tools: `list_mpp32_services`, `call_mpp32_endpoint`,
63
+ `get_solana_token_intelligence`.
64
+ * End to end support for five payment rails: x402, Tempo, ACP, AP2,
65
+ AGTP. Server picks the rail the wallet is funded for and falls back
66
+ if the first attempt does not settle.
67
+ * Federated catalog (native, curated free, x402 Bazaar, MCP registry).
68
+ * Automatic 402 sign-and-retry through `/api/agent/execute`.
69
+
70
+ ## [1.0.0] - 2026-05-08
71
+
72
+ * 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.2";
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) {
@@ -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: [
@@ -194,7 +339,7 @@ async function callViaAgentExecute(service, method, body, query) {
194
339
  ...(query ? { query } : {}),
195
340
  });
196
341
  // Round 1: no payment headers
197
- const firstRes = await fetch(execUrl, {
342
+ const firstRes = await fetchWithTimeout(execUrl, {
198
343
  method: "POST",
199
344
  headers: buildHeaders({ "Content-Type": "application/json" }),
200
345
  body: reqBody,
@@ -231,7 +376,7 @@ function detectPaymentRequired(resp) {
231
376
  const result = resp?.data?.result;
232
377
  if (!result?.error || result.error.code !== "PAYMENT_REQUIRED")
233
378
  return null;
234
- const headers = result.error.challenge?.headers ?? {};
379
+ const headers = lowercaseHeaderKeys(result.error.challenge?.headers);
235
380
  return {
236
381
  wwwAuthenticate: headers["www-authenticate"],
237
382
  paymentRequired: headers["payment-required"],
@@ -308,7 +453,7 @@ async function signAndRetry(execUrl, reqBody, challenge) {
308
453
  };
309
454
  }
310
455
  // Round 2: with payment headers
311
- const secondRes = await fetch(execUrl, {
456
+ const secondRes = await fetchWithTimeout(execUrl, {
312
457
  method: "POST",
313
458
  headers: buildHeaders({ "Content-Type": "application/json", ...paymentHeaders }),
314
459
  body: reqBody,
@@ -437,7 +582,9 @@ function paymentKeyMissingMessage(resp, challenge) {
437
582
  ' "command": "npx",',
438
583
  ' "args": ["mpp32-mcp-server"],',
439
584
  ' "env": {',
440
- AGENT_KEY ? ` "MPP32_AGENT_KEY": "${AGENT_KEY.slice(0, 12)}…",` : "",
585
+ AGENT_KEY
586
+ ? ` "MPP32_AGENT_KEY": "${AGENT_KEY.slice(0, 12).replace(/[^A-Za-z0-9_-]/g, "?")}…",`
587
+ : "",
441
588
  ' "MPP32_SOLANA_PRIVATE_KEY": "<solana-base58-key for USDC>",',
442
589
  ' "MPP32_PRIVATE_KEY": "<EVM-hex-key for pathUSD>"',
443
590
  " }",
@@ -477,7 +624,7 @@ async function callViaLegacyProxy(slug, method, body, query) {
477
624
  // Without an agent key, only native /api/proxy/<slug> is reachable.
478
625
  // We fetch /info first to detect that the slug exists as a native service.
479
626
  const infoUrl = new URL(`/api/proxy/${encodeURIComponent(slug)}/info`, API_URL).toString();
480
- const infoRes = await fetch(infoUrl);
627
+ const infoRes = await fetchWithTimeout(infoUrl);
481
628
  if (!infoRes.ok) {
482
629
  return {
483
630
  content: [
@@ -500,7 +647,7 @@ async function callViaLegacyProxy(slug, method, body, query) {
500
647
  const baseHeaders = { Accept: "application/json" };
501
648
  if (body !== undefined)
502
649
  baseHeaders["Content-Type"] = "application/json";
503
- const challengeRes = await fetch(proxyUrl.toString(), {
650
+ const challengeRes = await fetchWithTimeout(proxyUrl.toString(), {
504
651
  method,
505
652
  headers: baseHeaders,
506
653
  body: method !== "GET" && body !== undefined ? JSON.stringify(body) : undefined,
@@ -592,7 +739,7 @@ async function callViaLegacyProxy(slug, method, body, query) {
592
739
  ],
593
740
  };
594
741
  }
595
- const paidRes = await fetch(proxyUrl.toString(), {
742
+ const paidRes = await fetchWithTimeout(proxyUrl.toString(), {
596
743
  method,
597
744
  headers: { ...baseHeaders, ...paymentHeaders },
598
745
  body: method !== "GET" && body !== undefined ? JSON.stringify(body) : undefined,
@@ -628,9 +775,21 @@ async function callViaLegacyProxy(slug, method, body, query) {
628
775
  async function legacyIntelligenceCall(token, walletAddress) {
629
776
  try {
630
777
  const reqHeaders = { "Content-Type": "application/json" };
631
- if (walletAddress)
632
- reqHeaders["X-Wallet-Address"] = walletAddress;
633
- const res = await fetch(`${API_URL}/api/intelligence`, {
778
+ if (walletAddress) {
779
+ const trimmed = walletAddress.trim();
780
+ if (!isPrintableAscii(trimmed)) {
781
+ return {
782
+ content: [
783
+ {
784
+ type: "text",
785
+ text: `walletAddress contains non-ASCII characters. Pass a Solana base58 address only.`,
786
+ },
787
+ ],
788
+ };
789
+ }
790
+ reqHeaders["X-Wallet-Address"] = trimmed;
791
+ }
792
+ const res = await fetchWithTimeout(`${API_URL}/api/intelligence`, {
634
793
  method: "POST",
635
794
  headers: reqHeaders,
636
795
  body: JSON.stringify({ token }),
@@ -723,7 +882,7 @@ async function legacyIntelligenceCall(token, walletAddress) {
723
882
  };
724
883
  }
725
884
  }
726
- const paidRes = await fetch(`${API_URL}/api/intelligence`, {
885
+ const paidRes = await fetchWithTimeout(`${API_URL}/api/intelligence`, {
727
886
  method: "POST",
728
887
  headers: { ...reqHeaders, ...paymentHeaders },
729
888
  body: JSON.stringify({ token }),
@@ -762,13 +921,19 @@ function parseWwwAuthenticate(header) {
762
921
  const match = header.match(/^(\w+)\s+(.+)$/);
763
922
  if (!match)
764
923
  return { scheme: null, params: {} };
765
- const scheme = match[1];
766
- const rest = match[2];
924
+ const scheme = match[1] ?? null;
925
+ const rest = match[2] ?? "";
767
926
  const params = {};
768
- const paramRegex = /(\w+)=(?:"([^"]*)"|([\w.+/=-]+))/g;
927
+ // Tokens per RFC 7235: quoted-string OR a token68-ish value covering all
928
+ // base64url, base58, hex, JSON-pointers, etc. Liberal on purpose so we do
929
+ // not silently drop valid challenges.
930
+ const paramRegex = /([A-Za-z0-9_-]+)=(?:"((?:[^"\\]|\\.)*)"|([^\s,]+))/g;
769
931
  let m;
770
932
  while ((m = paramRegex.exec(rest)) !== null) {
771
- params[m[1]] = m[2] ?? m[3];
933
+ const key = m[1];
934
+ const val = m[2] ?? m[3];
935
+ if (key && val !== undefined)
936
+ params[key] = val;
772
937
  }
773
938
  return { scheme, params };
774
939
  }
@@ -865,8 +1030,14 @@ async function completeX402Payment(paymentRequiredHeader, solanaPrivateKey) {
865
1030
  async function main() {
866
1031
  const transport = new StdioServerTransport();
867
1032
  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}`);
1033
+ const features = [
1034
+ AGENT_KEY ? "agent-key" : null,
1035
+ SOLANA_PRIVATE_KEY ? "x402-key" : null,
1036
+ PRIVATE_KEY ? "tempo-key" : null,
1037
+ ]
1038
+ .filter(Boolean)
1039
+ .join(", ") || "no keys (catalog-only legacy mode)";
1040
+ console.error(`[mpp32] MCP server v${SERVER_VERSION} on stdio. API ${API_URL}. Configured: ${features}. Timeout ${TIMEOUT_MS}ms.`);
870
1041
  }
871
1042
  main().catch((err) => {
872
1043
  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.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",
@@ -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,