mpp32-mcp-server 1.2.2 → 1.2.4

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,59 @@ 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.4] - 2026-05-11
8
+
9
+ ### Fixed
10
+
11
+ * **Self-payment now fails fast with a useful error.** When the wallet
12
+ derived from `MPP32_SOLANA_PRIVATE_KEY` (or `MPP32_PRIVATE_KEY`) is the
13
+ same address as a challenge's `payTo`, the signer used to construct the
14
+ transaction anyway and the facilitator surfaced an opaque "transaction
15
+ failed simulation" error a few seconds later. Both the SVM and EVM
16
+ signers now detect this case before signing and return:
17
+ `Refusing to sign an x402 payment to yourself: the wallet derived from
18
+ MPP32_SOLANA_PRIVATE_KEY (<addr>) is also the payment recipient (payTo)
19
+ for this service.` This happens most often when an operator uses their
20
+ own receiving wallet to test a paid endpoint they themselves run.
21
+
22
+ ## [1.2.3] - 2026-05-11
23
+
24
+ ### Fixed
25
+
26
+ * **Solana-only users were being pushed to EVM and failing.** When a third-
27
+ party x402 v2 challenge advertised both an EVM and an SVM entry in
28
+ `accepts[]`, 1.2.2's `pickRequirements` preferred EVM unconditionally —
29
+ so a user who had only set `MPP32_SOLANA_PRIVATE_KEY` got a "no EVM key
30
+ set" error instead of a successful Solana payment. The picker now:
31
+ - Honors `MPP32_PREFERRED_NETWORK` if set (accepts `solana`, `base`,
32
+ `evm`, `ethereum`, or a full CAIP-2 like `solana:5eykt...`).
33
+ - Otherwise, if only one key is configured, prefers that network.
34
+ - Otherwise, when both keys are configured, prefers EVM (Base settles
35
+ faster and is the dominant chain across providers).
36
+ - Otherwise falls back to the first `accepts[]` entry so the per-network
37
+ signer can surface a precise "you need MPP32_X_PRIVATE_KEY" error.
38
+
39
+ ### Added
40
+
41
+ * **`MPP32_PREFERRED_NETWORK` env var.** Explicit override for the network
42
+ picker. Useful when both keys are configured but the user wants every
43
+ payment to go through one specific chain.
44
+ * **`debug_mpp32` tool (alias of `get_mpp32_diagnostics`).** Several
45
+ external docs and skills already reference this name; the alias keeps
46
+ them working without a documentation churn.
47
+
48
+ ### Changed
49
+
50
+ * **`get_mpp32_diagnostics` now probes the API live.** In addition to
51
+ reporting which env vars the process loaded, it issues a 5-second
52
+ `GET /api/agent/protocols` against the configured `MPP32_API_URL` and
53
+ reports `OK / Reachable but {status} / UNREACHABLE: {reason}`. Adds a
54
+ single `Ready to pay end-to-end: YES/NO` line so users do not need to
55
+ cross-reference three separate fields to know whether payments will
56
+ work. Windows-specific guidance (no surrounding quotes inside the JSON
57
+ string) is now called out explicitly — this tripped multiple users in
58
+ 1.2.1/1.2.2.
59
+
7
60
  ## [1.2.2] - 2026-05-11
8
61
 
9
62
  ### Fixed
package/README.md CHANGED
@@ -9,7 +9,18 @@
9
9
  </p>
10
10
  </p>
11
11
 
12
- One install. Five payment rails. Thousands of paid APIs your agent can reach without you setting up a single account.
12
+ One install. Pay any x402 endpoint on Solana from your agent. Browse a federated index of thousands of machine payable APIs without setting up a single provider account.
13
+
14
+ ## What works today
15
+
16
+ | Rail | Status | Network | Asset |
17
+ |:-----|:-------|:--------|:------|
18
+ | x402 | Production | Solana (mainnet) | USDC |
19
+ | x402 | Production | Base | USDC |
20
+ | Tempo | Envelope wired, gated off in production until the client flow is verified end to end | Ethereum L2 | pathUSD |
21
+ | ACP / AP2 / AGTP | Envelopes wired, gated off in production | Multi chain | per protocol |
22
+
23
+ The MCP server ships signers for both Solana and Base out of the box. When a paid call returns a 402, the server picks the network that matches the key you configured and settles the payment in one round trip.
13
24
 
14
25
  ## Why this beats running your own integrations
15
26
 
@@ -25,22 +36,10 @@ MPP32 replaces all of that with one MCP server. Your agent asks for a service by
25
36
  * Track every call, every dollar settled, and every protocol used from a dashboard at mpp32.org.
26
37
  * Get an automatic 20 percent or 40 percent discount on native services for holding M32 once your wallet is verified.
27
38
 
28
- ## Payment rails it speaks natively
29
-
30
- | Rail | Settles in | Network |
31
- |:-----|:-----------|:--------|
32
- | x402 | USDC | Solana |
33
- | Tempo | pathUSD | Ethereum L2 |
34
- | ACP | Checkout session | Multi chain |
35
- | AP2 | W3C verifiable credentials | Chain agnostic |
36
- | AGTP | HMAC signed agent certificates | Chain agnostic |
37
-
38
- Every native endpoint accepts all five. The server picks whichever your wallet is funded for and falls back gracefully if the first attempt fails.
39
-
40
39
  ## Install
41
40
 
42
41
  ```bash
43
- npx mpp32-mcp-server
42
+ npx -y mpp32-mcp-server@latest
44
43
  ```
45
44
 
46
45
  ### Claude Desktop, Claude Code, Cursor, Windsurf
@@ -52,30 +51,56 @@ Drop this into the MCP servers section of your client config.
52
51
  "mcpServers": {
53
52
  "mpp32": {
54
53
  "command": "npx",
55
- "args": ["mpp32-mcp-server"],
54
+ "args": ["-y", "mpp32-mcp-server@latest"],
56
55
  "env": {
57
56
  "MPP32_AGENT_KEY": "mpp32_agent_…",
58
- "MPP32_SOLANA_PRIVATE_KEY": "your_solana_private_key_for_usdc"
57
+ "MPP32_SOLANA_PRIVATE_KEY": "<your base58 Solana secret key>"
59
58
  }
60
59
  }
61
60
  }
62
61
  }
63
62
  ```
64
63
 
65
- Get an `MPP32_AGENT_KEY` at [mpp32.org/agent-console](https://mpp32.org/agent-console). No signup, no email, just a session form. With an agent key every call is attributed to your dashboard so you can see spend, success rate, and protocol breakdown. Without it the server still works but you only see native services and the calls stay anonymous.
64
+ Config file locations:
65
+
66
+ * **Claude Desktop (macOS):** `~/Library/Application Support/Claude/claude_desktop_config.json`
67
+ * **Claude Desktop (Windows):** `%APPDATA%\Claude\claude_desktop_config.json`
68
+ * **Cursor:** `~/.cursor/mcp.json`
69
+ * **Windsurf:** `~/.codeium/windsurf/mcp_config.json`
70
+
71
+ Fully quit and reopen the client after editing. The MCP child process inherits env vars from this file at launch.
72
+
73
+ Get an `MPP32_AGENT_KEY` at [mpp32.org/agent-console](https://mpp32.org/agent-console). No signup, no email, just click **Create Agent Session** and the form returns a key plus this exact config snippet ready to copy. With an agent key every call is attributed to your dashboard so you can see spend, success rate, and protocol breakdown. Without it the server still works but you only see native services and the calls stay anonymous.
74
+
75
+ `MPP32_SOLANA_PRIVATE_KEY` is only needed for paid services. Free services in the catalog work without any payment key.
76
+
77
+ ### What format does `MPP32_SOLANA_PRIVATE_KEY` take?
78
+
79
+ A base58 encoded 64 byte Solana secret key. The same string that Phantom shows under **export private key** (not the seed phrase). If you have a `keypair.json` (the 64 byte array Solana CLI uses), convert it once with:
80
+
81
+ ```bash
82
+ node -e "console.log(require('bs58').encode(Buffer.from(JSON.parse(require('fs').readFileSync('keypair.json')))))"
83
+ ```
84
+
85
+ Then paste the resulting string into the env block. Treat it like a password. It can spend any USDC and SOL in the wallet.
86
+
87
+ ### Fund the wallet with both USDC and SOL
88
+
89
+ x402 settles the payment in USDC, but Solana itself charges a tiny native SOL fee on every transaction. A USDC only wallet will fail with `insufficient funds for rent` even though the USDC balance is plentiful. About `0.001 SOL` covers many calls.
66
90
 
67
- `MPP32_SOLANA_PRIVATE_KEY` and `MPP32_PRIVATE_KEY` are only needed for paid services. Free services in the catalog work without any key.
91
+ Tip: call the `get_mpp32_diagnostics` tool from inside Claude. It probes the API, reports which env vars actually reached the MCP process, and prints `Ready to pay: YES` once everything is wired.
68
92
 
69
93
  ## Configuration
70
94
 
71
95
  | Variable | When you need it | What it does |
72
96
  |:---------|:-----------------|:-------------|
73
97
  | `MPP32_AGENT_KEY` | Recommended | Session key from mpp32.org/agent-console. Unlocks the full federated catalog and dashboard tracking. Also accepted as `MPP32_API_KEY`. |
74
- | `MPP32_SOLANA_PRIVATE_KEY` | Paid x402 calls | Solana key used to sign USDC payments locally. |
75
- | `MPP32_PRIVATE_KEY` | Paid Tempo calls | EVM key used to sign pathUSD payments locally on Ethereum L2. |
98
+ | `MPP32_SOLANA_PRIVATE_KEY` | Paid x402 calls on Solana | Base58 encoded Solana secret key. Used to sign USDC payments locally. Never leaves your machine. |
99
+ | `MPP32_PRIVATE_KEY` | Paid x402 calls on Base | 0x prefixed EVM private key. Used to sign USDC payments on Base when a provider only accepts EVM. |
100
+ | `MPP32_PREFERRED_NETWORK` | Optional override | When both keys are configured and the challenge advertises multiple networks, force one. Accepts `solana`, `base`, `evm`, or a full CAIP-2 string. |
76
101
  | `MPP32_API_URL` | Custom deployments | Override the API base URL. Defaults to `https://mpp32.org`. |
77
102
 
78
- Private keys stay on your machine. They sign payments locally and never travel to MPP32 servers. Provide one or both for paid calls. If both are present, x402 is tried first and Tempo is used as a fallback.
103
+ Private keys stay on your machine. They sign payments locally and never travel to MPP32 servers. Provide either or both for paid calls. If both are present and a challenge advertises both networks, EVM is preferred unless `MPP32_PREFERRED_NETWORK` is set; if only one key is configured, that network wins automatically.
79
104
 
80
105
  ## Tools your agent will see
81
106
 
@@ -111,15 +136,19 @@ Under the hood:
111
136
 
112
137
  No payment logic in your code. No per provider keys to juggle.
113
138
 
139
+ ### `get_mpp32_diagnostics`
140
+
141
+ Reports the MCP server version, which env vars actually reached the child process, the API URL it will hit, and a live API reachability probe. Prints `Ready to pay: YES` only when an agent key plus at least one signing key are detected and the API is reachable. Call this first if anything misbehaves. Also available as `debug_mpp32`.
142
+
114
143
  ### `get_solana_token_intelligence`
115
144
 
116
- Real time analysis of any Solana token. Pulls live data from DexScreener, Jupiter, and CoinGecko and merges it into one report. Returns an alpha score from 0 to 100, rug risk, whale activity, smart money signals, 24 hour pump probability, projected ROI ranges, and full market data. Costs $0.008 per call, paid automatically through x402 or Tempo when a key is set.
145
+ Real time analysis of any Solana token. Pulls live data from DexScreener, Jupiter, and CoinGecko and merges it into one report. Returns an alpha score from 0 to 100, rug risk, whale activity, smart money signals, 24 hour pump probability, projected ROI ranges, and full market data. Costs $0.008 per call, paid automatically through x402 when a Solana key is set.
117
146
 
118
147
  ```json
119
148
  { "token": "BONK" }
120
149
  ```
121
150
 
122
- M32 holders get tiered discounts (20 percent at 250k, 40 percent at 1M) the moment their wallet is verified at the agent console.
151
+ M32 holders will get tiered discounts (20 percent at 250k, 40 percent at 1M) once SIWS wallet signature verification ships. The discount path is gated off in production until then so it cannot be claimed by spoofing a wallet header.
123
152
 
124
153
  ## How discovery works
125
154
 
@@ -147,13 +176,13 @@ Sessions are scoped, revocable, and rotate cleanly. The key is hashed at rest on
147
176
 
148
177
  ## How payment verification actually works
149
178
 
150
- x402 payments are verified on chain through the Solana facilitator. Tempo payments are verified cryptographically through the mppx SDK. ACP sessions are database backed with a real checkout flow. AP2 mandates use ECDSA P-256 signature verification. AGTP uses HMAC SHA256 signed agent certificates with a server held salt so signatures cannot be forged from a public agent id alone.
179
+ x402 payments are verified on chain through the Solana facilitator (for SVM) or the Base facilitator (for EVM). MPP32 never holds custody of funds. Every paid call settles directly from the caller's wallet to the provider's wallet.
151
180
 
152
- Every protocol has its own verification path. None of them are stubbed. MPP32 never holds custody of funds. Every paid call settles directly from the caller's wallet to the provider's wallet.
181
+ Tempo, ACP, AP2, and AGTP envelopes are implemented in the proxy and tested against synthetic challenges, but the corresponding client signing flows are not yet exposed by this MCP. Those rails stay disabled in production until each has a verified end to end client flow. Treat the catalog as an x402 catalog today; the rest will light up as their clients land.
153
182
 
154
183
  ## For API providers
155
184
 
156
- List your endpoint once and get paid through every protocol automatically. MPP32 handles the payment negotiation, the on chain verification, the discovery listings via OpenAPI and A2A and MCP standards, the 24 hour health re check, and the analytics dashboard. Settlement lands in USDC or pathUSD straight to your wallet.
185
+ List your endpoint once and receive payment over x402. MPP32 handles the payment negotiation, the on chain verification, the discovery listings via OpenAPI and A2A and MCP standards, the periodic health re check, and the analytics dashboard. Settlement lands in USDC on Solana or Base straight to your wallet.
157
186
 
158
187
  Register at [mpp32.org/build](https://mpp32.org/build).
159
188
 
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.4";
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;
@@ -129,6 +129,18 @@ export async function signX402PaymentSvm(requirements, rawKey, rpcUrlOverride, e
129
129
  const mintAddress = deps.address(requirements.asset);
130
130
  const recipientAddress = deps.address(requirements.payTo);
131
131
  const feePayerAddress = deps.address(requirements.extra.feePayer);
132
+ // Self-payment guard. An SPL TransferChecked where source ATA == destination
133
+ // ATA is a no-op the Solana runtime rejects, and the resulting facilitator
134
+ // error is opaque ("transaction failed simulation"). This usually means the
135
+ // user configured the same wallet as both their MPP32_SOLANA_PRIVATE_KEY and
136
+ // the upstream payTo (e.g. calling their own listed service, or testing
137
+ // against the MPP32 oracle from the operator wallet). Catch it here with a
138
+ // clear, actionable message before we burn an RPC round-trip.
139
+ if (String(payerAddress) === String(recipientAddress)) {
140
+ throw new Error(`Refusing to sign an x402 payment to yourself: the wallet derived from MPP32_SOLANA_PRIVATE_KEY ` +
141
+ `(${String(payerAddress)}) is also the payment recipient (payTo) for this service. ` +
142
+ `Use a different wallet to call this endpoint, or fund a separate payer key.`);
143
+ }
132
144
  // Derive both sides' associated token accounts (classic SPL Token program).
133
145
  const [sourceAtaTuple, destinationAtaTuple] = await Promise.all([
134
146
  deps.findAssociatedTokenPda({
@@ -203,6 +215,14 @@ export async function signX402PaymentEvm(requirements, rawKey, echoedVersion = 1
203
215
  }
204
216
  const { privateKeyToAccount } = await loadEvmDeps();
205
217
  const account = privateKeyToAccount(keyHex);
218
+ // Self-payment guard (EVM). EIP-3009 transferWithAuthorization with
219
+ // from == to is rejected on-chain by USDC. Surfacing this here keeps the
220
+ // error message useful instead of an opaque facilitator failure.
221
+ if (account.address.toLowerCase() === recipientAddr.toLowerCase()) {
222
+ throw new Error(`Refusing to sign an x402 payment to yourself: the wallet derived from MPP32_PRIVATE_KEY ` +
223
+ `(${account.address}) is also the payment recipient (payTo) for this service. ` +
224
+ `Use a different wallet to call this endpoint, or fund a separate payer key.`);
225
+ }
206
226
  const now = Math.floor(Date.now() / 1000);
207
227
  const validAfter = BigInt(0);
208
228
  const validBefore = BigInt(now + (requirements.maxTimeoutSeconds ?? 600));
@@ -279,21 +299,55 @@ function normalizeRequirements(raw) {
279
299
  extra: raw.extra,
280
300
  };
281
301
  }
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) {
302
+ // Pick the first entry in `accepts` we can actually sign.
303
+ //
304
+ // Preference rules (in order):
305
+ // 1. If MPP32_PREFERRED_NETWORK env var matches a `network` field, use it.
306
+ // 2. If only one key is configured, prefer that network (this is the most
307
+ // common real-world case a Solana-only user offered a mixed challenge
308
+ // previously got signed EVM and failed with "no EVM key set". Now they
309
+ // get signed SVM.).
310
+ // 3. If both keys are configured, prefer EVM (Base is the dominant chain
311
+ // and tends to settle faster).
312
+ // 4. Otherwise fall back to the first accepted entry and let the per-network
313
+ // signer throw a precise "you need MPP32_X_PRIVATE_KEY" error.
314
+ function pickRequirements(accepts, haveSvm, haveEvm, preferredNetwork) {
288
315
  if (accepts.length === 0) {
289
316
  throw new Error("x402 v2 challenge has empty `accepts` array — nothing to pay.");
290
317
  }
291
- if (haveEvm) {
318
+ // Explicit preference wins.
319
+ if (preferredNetwork) {
320
+ const want = preferredNetwork.toLowerCase();
321
+ const explicit = accepts.find((a) => {
322
+ const n = a.network.toLowerCase();
323
+ if (n === want)
324
+ return true;
325
+ if (want === "solana" && isSvmNetwork(a.network))
326
+ return true;
327
+ if ((want === "base" || want === "evm") && isEvmNetwork(a.network))
328
+ return true;
329
+ return false;
330
+ });
331
+ if (explicit)
332
+ return explicit;
333
+ }
334
+ // Solana-only user: prefer SVM.
335
+ if (haveSvm && !haveEvm) {
336
+ const svm = accepts.find((a) => isSvmNetwork(a.network));
337
+ if (svm)
338
+ return svm;
339
+ }
340
+ // EVM-only user: prefer EVM.
341
+ if (haveEvm && !haveSvm) {
292
342
  const evm = accepts.find((a) => isEvmNetwork(a.network));
293
343
  if (evm)
294
344
  return evm;
295
345
  }
296
- if (haveSvm) {
346
+ // Both keys configured: prefer EVM (Base is the dominant chain).
347
+ if (haveEvm && haveSvm) {
348
+ const evm = accepts.find((a) => isEvmNetwork(a.network));
349
+ if (evm)
350
+ return evm;
297
351
  const svm = accepts.find((a) => isSvmNetwork(a.network));
298
352
  if (svm)
299
353
  return svm;
@@ -317,7 +371,7 @@ export async function signX402Payment(args) {
317
371
  const accepts = decoded.accepts
318
372
  .filter((a) => !!a && typeof a === "object")
319
373
  .map(normalizeRequirements);
320
- requirements = pickRequirements(accepts, !!args.solanaKey, !!args.evmKey);
374
+ requirements = pickRequirements(accepts, !!args.solanaKey, !!args.evmKey, args.preferredNetwork);
321
375
  echoedVersion = decoded.x402Version || 2;
322
376
  }
323
377
  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.4",
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",