mpp32-mcp-server 1.2.2 → 1.2.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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,44 @@ 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.3] - 2026-05-11
8
+
9
+ ### Fixed
10
+
11
+ * **Solana-only users were being pushed to EVM and failing.** When a third-
12
+ party x402 v2 challenge advertised both an EVM and an SVM entry in
13
+ `accepts[]`, 1.2.2's `pickRequirements` preferred EVM unconditionally —
14
+ so a user who had only set `MPP32_SOLANA_PRIVATE_KEY` got a "no EVM key
15
+ set" error instead of a successful Solana payment. The picker now:
16
+ - Honors `MPP32_PREFERRED_NETWORK` if set (accepts `solana`, `base`,
17
+ `evm`, `ethereum`, or a full CAIP-2 like `solana:5eykt...`).
18
+ - Otherwise, if only one key is configured, prefers that network.
19
+ - Otherwise, when both keys are configured, prefers EVM (Base settles
20
+ faster and is the dominant chain across providers).
21
+ - Otherwise falls back to the first `accepts[]` entry so the per-network
22
+ signer can surface a precise "you need MPP32_X_PRIVATE_KEY" error.
23
+
24
+ ### Added
25
+
26
+ * **`MPP32_PREFERRED_NETWORK` env var.** Explicit override for the network
27
+ picker. Useful when both keys are configured but the user wants every
28
+ payment to go through one specific chain.
29
+ * **`debug_mpp32` tool (alias of `get_mpp32_diagnostics`).** Several
30
+ external docs and skills already reference this name; the alias keeps
31
+ them working without a documentation churn.
32
+
33
+ ### Changed
34
+
35
+ * **`get_mpp32_diagnostics` now probes the API live.** In addition to
36
+ reporting which env vars the process loaded, it issues a 5-second
37
+ `GET /api/agent/protocols` against the configured `MPP32_API_URL` and
38
+ reports `OK / Reachable but {status} / UNREACHABLE: {reason}`. Adds a
39
+ single `Ready to pay end-to-end: YES/NO` line so users do not need to
40
+ cross-reference three separate fields to know whether payments will
41
+ work. Windows-specific guidance (no surrounding quotes inside the JSON
42
+ string) is now called out explicitly — this tripped multiple users in
43
+ 1.2.1/1.2.2.
44
+
7
45
  ## [1.2.2] - 2026-05-11
8
46
 
9
47
  ### Fixed
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.2";
6
+ const SERVER_VERSION = "1.2.3";
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
@@ -121,6 +121,22 @@ const SOLANA_PRIVATE_KEY = (() => {
121
121
  }
122
122
  return v;
123
123
  })();
124
+ // Optional override: when both an EVM and a Solana key are configured, this
125
+ // decides which network to use for mixed challenges. Accepts:
126
+ // "solana" (or any "solana:*" CAIP-2), "base", "evm", "ethereum".
127
+ // Defaults to undefined — pickRequirements then falls back to its own rules.
128
+ const PREFERRED_NETWORK = (() => {
129
+ const v = readEnv("MPP32_PREFERRED_NETWORK");
130
+ if (!v)
131
+ return undefined;
132
+ const lower = v.toLowerCase();
133
+ const allowed = ["solana", "base", "evm", "ethereum"];
134
+ if (!allowed.some((a) => lower === a || lower.startsWith(a))) {
135
+ console.error(`[mpp32] MPP32_PREFERRED_NETWORK="${v}" not recognized. Allowed: ${allowed.join(", ")} (or full CAIP-2 like solana:5eykt...). Ignoring.`);
136
+ return undefined;
137
+ }
138
+ return lower;
139
+ })();
124
140
  // Wrap fetch with a default timeout. AbortSignal.timeout exists in Node 20+,
125
141
  // but we ship for Node 18+, so we build the signal ourselves.
126
142
  async function fetchWithTimeout(url, init) {
@@ -197,12 +213,29 @@ function describeEnvVarStatus(name, value) {
197
213
  : `${value.slice(0, 6)}…${value.slice(-4)} (${value.length} chars)`;
198
214
  return `${name}: SET (${fingerprint})`;
199
215
  }
200
- server.tool("get_mpp32_diagnostics", "Report what the mpp32-mcp-server detected at startup: version, API URL, and which env vars (MPP32_AGENT_KEY, MPP32_SOLANA_PRIVATE_KEY, MPP32_PRIVATE_KEY) were loaded into this process. Use this FIRST if payments fail with 'no key configured' even though you set one in claude_desktop_config.json — it confirms whether your env vars actually reached the MCP process or got dropped by a typo / wrong file / stale restart.", {}, async () => {
216
+ server.tool("get_mpp32_diagnostics", "Report what the mpp32-mcp-server detected at startup: version, API URL, env vars (MPP32_AGENT_KEY, MPP32_SOLANA_PRIVATE_KEY, MPP32_PRIVATE_KEY, MPP32_PREFERRED_NETWORK), and a live API connectivity check. Use this FIRST if payments fail with 'no key configured' even though you set one in claude_desktop_config.json.", {}, async () => {
217
+ // Live connectivity probe so the user knows whether the *backend* is
218
+ // reachable too — not just whether their env loaded.
219
+ let apiReachable;
220
+ try {
221
+ const probe = await fetchWithTimeout(`${API_URL}/api/agent/protocols`, {
222
+ timeoutMs: 5_000,
223
+ });
224
+ apiReachable = probe.ok
225
+ ? `OK (${probe.status})`
226
+ : `Reachable but returned ${probe.status}`;
227
+ }
228
+ catch (err) {
229
+ apiReachable = `UNREACHABLE: ${err instanceof Error ? err.message : String(err)}`;
230
+ }
231
+ const haveAnyKey = !!(SOLANA_PRIVATE_KEY || PRIVATE_KEY);
232
+ const readyToPay = !!AGENT_KEY && haveAnyKey;
201
233
  const lines = [
202
234
  `**mpp32-mcp-server diagnostics**`,
203
235
  ``,
204
236
  `Version: ${SERVER_VERSION}`,
205
237
  `API URL: ${API_URL}`,
238
+ `API reachable: ${apiReachable}`,
206
239
  `Timeout: ${TIMEOUT_MS}ms`,
207
240
  `Node: ${process.version} on ${process.platform}/${process.arch}`,
208
241
  ``,
@@ -211,26 +244,56 @@ server.tool("get_mpp32_diagnostics", "Report what the mpp32-mcp-server detected
211
244
  describeEnvVarStatus("MPP32_AGENT_KEY", AGENT_KEY),
212
245
  describeEnvVarStatus("MPP32_SOLANA_PRIVATE_KEY", SOLANA_PRIVATE_KEY),
213
246
  describeEnvVarStatus("MPP32_PRIVATE_KEY", PRIVATE_KEY),
247
+ `MPP32_PREFERRED_NETWORK: ${PREFERRED_NETWORK ?? "not set (auto: prefer the only key you have)"}`,
214
248
  ``,
215
249
  `**Capabilities:**`,
216
250
  `- Catalog browsing: yes (always available)`,
217
251
  `- Federated service execution: ${AGENT_KEY ? "yes" : "no — set MPP32_AGENT_KEY"}`,
218
252
  `- x402 (USDC on Solana) payment: ${SOLANA_PRIVATE_KEY ? "yes" : "no — set MPP32_SOLANA_PRIVATE_KEY"}`,
219
- `- Tempo (pathUSD on Eth L2) payment: ${PRIVATE_KEY ? "yes" : "no — set MPP32_PRIVATE_KEY"}`,
253
+ `- x402 (USDC on Base/EVM) payment: ${PRIVATE_KEY ? "yes" : "no — set MPP32_PRIVATE_KEY"}`,
254
+ ``,
255
+ `**Ready to pay end-to-end:** ${readyToPay ? "YES — try `query_intelligence` with token=\"M32\" to confirm." : "NO — see the missing items above."}`,
220
256
  ``,
221
- `If a variable shows NOT SET but you put it in claude_desktop_config.json:`,
222
- `1. Confirm the file path Claude Desktop actually reads from:`,
223
- ` - macOS: ~/Library/Application Support/Claude/claude_desktop_config.json`,
257
+ `**If a variable shows NOT SET but you set it in claude_desktop_config.json:**`,
258
+ `1. Confirm the file path Claude Desktop actually reads:`,
259
+ ` - macOS: ~/Library/Application Support/Claude/claude_desktop_config.json`,
224
260
  ` - Windows: %APPDATA%\\Claude\\claude_desktop_config.json`,
225
- `2. The 'env' block must sit INSIDE the server entry, alongside 'command' and 'args' — not at the top level.`,
226
- `3. Fully quit Claude Desktop (Cmd+Q on Mac, right-click tray → Quit on Windows). Closing the window is not enough.`,
227
- `4. Re-open Claude Desktop. The new MCP process inherits the env from the JSON.`,
228
- `5. Call get_mpp32_diagnostics again — if it still shows NOT SET, the JSON did not load (check for a syntax error).`,
261
+ `2. The 'env' block must sit INSIDE the server entry, beside 'command' and 'args' — not at the top level.`,
262
+ `3. Validate the JSON: a single missing comma silently throws the whole file out.`,
263
+ `4. Fully quit Claude Desktop:`,
264
+ ` - macOS: Cmd+Q (or Claude menu Quit)`,
265
+ ` - Windows: right-click the system-tray icon → Quit (closing the window is NOT enough)`,
266
+ `5. Re-open Claude Desktop. The new MCP child process inherits env from the JSON.`,
267
+ `6. Call get_mpp32_diagnostics again. If it STILL shows NOT SET, the JSON did not load — check the Claude Desktop log for a parse error.`,
268
+ ``,
269
+ `**On Windows specifically:** the value must NOT include surrounding quotes inside the JSON string. Bad: "\\"mpp32_agent_abc...\\"". Good: "mpp32_agent_abc...".`,
229
270
  ];
230
271
  return {
231
272
  content: [{ type: "text", text: lines.join("\n") }],
232
273
  };
233
274
  });
275
+ // Back-compat alias. Older docs and skills say `debug_mpp32`.
276
+ server.tool("debug_mpp32", "Alias for get_mpp32_diagnostics. Reports env-var detection, API connectivity, and ready-to-pay status.", {}, async () => {
277
+ let apiReachable;
278
+ try {
279
+ const probe = await fetchWithTimeout(`${API_URL}/api/agent/protocols`, { timeoutMs: 5_000 });
280
+ apiReachable = probe.ok ? `OK (${probe.status})` : `Reachable but returned ${probe.status}`;
281
+ }
282
+ catch (err) {
283
+ apiReachable = `UNREACHABLE: ${err instanceof Error ? err.message : String(err)}`;
284
+ }
285
+ const readyToPay = !!AGENT_KEY && !!(SOLANA_PRIVATE_KEY || PRIVATE_KEY);
286
+ const lines = [
287
+ `mpp32-mcp-server v${SERVER_VERSION} (${process.platform}/${process.arch}, Node ${process.version})`,
288
+ `API: ${API_URL} — ${apiReachable}`,
289
+ describeEnvVarStatus("MPP32_AGENT_KEY", AGENT_KEY),
290
+ describeEnvVarStatus("MPP32_SOLANA_PRIVATE_KEY", SOLANA_PRIVATE_KEY),
291
+ describeEnvVarStatus("MPP32_PRIVATE_KEY", PRIVATE_KEY),
292
+ `MPP32_PREFERRED_NETWORK: ${PREFERRED_NETWORK ?? "not set"}`,
293
+ `Ready to pay: ${readyToPay ? "YES" : "NO"}`,
294
+ ];
295
+ return { content: [{ type: "text", text: lines.join("\n") }] };
296
+ });
234
297
  // ── Tool 1: list_mpp32_services ─────────────────────────────────────────────
235
298
  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.", {
236
299
  category: z
@@ -1063,6 +1126,7 @@ async function completeX402Payment(paymentRequiredHeader, keys) {
1063
1126
  solanaKey: keys.solana,
1064
1127
  evmKey: keys.evm,
1065
1128
  solanaRpcUrl,
1129
+ preferredNetwork: PREFERRED_NETWORK,
1066
1130
  });
1067
1131
  return {
1068
1132
  xPaymentHeader: result.xPaymentHeader,
@@ -32,6 +32,7 @@ export interface SignX402Args {
32
32
  solanaKey?: string;
33
33
  evmKey?: string;
34
34
  solanaRpcUrl?: string;
35
+ preferredNetwork?: string;
35
36
  }
36
37
  export interface SignX402Result {
37
38
  xPaymentHeader: string;
@@ -279,21 +279,55 @@ function normalizeRequirements(raw) {
279
279
  extra: raw.extra,
280
280
  };
281
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) {
282
+ // Pick the first entry in `accepts` we can actually sign.
283
+ //
284
+ // Preference rules (in order):
285
+ // 1. If MPP32_PREFERRED_NETWORK env var matches a `network` field, use it.
286
+ // 2. If only one key is configured, prefer that network (this is the most
287
+ // common real-world case a Solana-only user offered a mixed challenge
288
+ // previously got signed EVM and failed with "no EVM key set". Now they
289
+ // get signed SVM.).
290
+ // 3. If both keys are configured, prefer EVM (Base is the dominant chain
291
+ // and tends to settle faster).
292
+ // 4. Otherwise fall back to the first accepted entry and let the per-network
293
+ // signer throw a precise "you need MPP32_X_PRIVATE_KEY" error.
294
+ function pickRequirements(accepts, haveSvm, haveEvm, preferredNetwork) {
288
295
  if (accepts.length === 0) {
289
296
  throw new Error("x402 v2 challenge has empty `accepts` array — nothing to pay.");
290
297
  }
291
- if (haveEvm) {
298
+ // Explicit preference wins.
299
+ if (preferredNetwork) {
300
+ const want = preferredNetwork.toLowerCase();
301
+ const explicit = accepts.find((a) => {
302
+ const n = a.network.toLowerCase();
303
+ if (n === want)
304
+ return true;
305
+ if (want === "solana" && isSvmNetwork(a.network))
306
+ return true;
307
+ if ((want === "base" || want === "evm") && isEvmNetwork(a.network))
308
+ return true;
309
+ return false;
310
+ });
311
+ if (explicit)
312
+ return explicit;
313
+ }
314
+ // Solana-only user: prefer SVM.
315
+ if (haveSvm && !haveEvm) {
316
+ const svm = accepts.find((a) => isSvmNetwork(a.network));
317
+ if (svm)
318
+ return svm;
319
+ }
320
+ // EVM-only user: prefer EVM.
321
+ if (haveEvm && !haveSvm) {
292
322
  const evm = accepts.find((a) => isEvmNetwork(a.network));
293
323
  if (evm)
294
324
  return evm;
295
325
  }
296
- if (haveSvm) {
326
+ // Both keys configured: prefer EVM (Base is the dominant chain).
327
+ if (haveEvm && haveSvm) {
328
+ const evm = accepts.find((a) => isEvmNetwork(a.network));
329
+ if (evm)
330
+ return evm;
297
331
  const svm = accepts.find((a) => isSvmNetwork(a.network));
298
332
  if (svm)
299
333
  return svm;
@@ -317,7 +351,7 @@ export async function signX402Payment(args) {
317
351
  const accepts = decoded.accepts
318
352
  .filter((a) => !!a && typeof a === "object")
319
353
  .map(normalizeRequirements);
320
- requirements = pickRequirements(accepts, !!args.solanaKey, !!args.evmKey);
354
+ requirements = pickRequirements(accepts, !!args.solanaKey, !!args.evmKey, args.preferredNetwork);
321
355
  echoedVersion = decoded.x402Version || 2;
322
356
  }
323
357
  else if (decoded && typeof decoded === "object") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mpp32-mcp-server",
3
- "version": "1.2.2",
3
+ "version": "1.2.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",