mpp32-mcp-server 1.2.3 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,98 @@ 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.3.1] - 2026-05-15
8
+
9
+ ### Fixed
10
+
11
+ * **Diagnostics tool referenced non-existent `query_intelligence`.** The
12
+ `get_mpp32_diagnostics` "Ready to pay" message told users to try
13
+ `query_intelligence`, which was never a tool name. Now correctly says
14
+ `get_solana_token_intelligence`.
15
+
16
+ * **`protocol` filter missing from `list_mpp32_services`.** The tool
17
+ description and the backend both support filtering by payment protocol
18
+ (x402, tempo, acp, ap2, agtp), but the parameter was never wired
19
+ through. Added `protocol` as an optional enum parameter and forwarded
20
+ it to the backend query string.
21
+
22
+ * **`server.json` version stuck at 1.2.2.** The MCP registry manifest
23
+ was never bumped past 1.2.2, causing the official registry listing to
24
+ advertise the wrong version. Updated to 1.3.1 with all new env vars.
25
+
26
+ * **`server.json` missing `MPP32_PREFERRED_NETWORK` and `MPP32_TIMEOUT_MS`
27
+ environment variables.** Both were supported since 1.2.3 and 1.1.2
28
+ respectively but never declared in the registry manifest.
29
+
30
+ ### Added
31
+
32
+ * **README now documents all M32-gated tools.** `get_m32_whale_tracker`,
33
+ `compare_tokens_m32`, and `scan_portfolio_m32` were live in the server
34
+ but missing from the README's tools section.
35
+
36
+ * **README now documents `MPP32_SOLANA_RPC_URL` and `MPP32_TIMEOUT_MS`**
37
+ in the configuration table.
38
+
39
+ ## [1.3.0] - 2026-05-14
40
+
41
+ ### Added
42
+
43
+ * **Economic Circuit Breakers — infrastructure-level spending guardrails.**
44
+ The first MCP payment server with built-in budget enforcement. No more
45
+ runaway agent bills.
46
+
47
+ - **`manage_agent_budget` tool** — new MCP tool to view, set, or reset
48
+ spending limits directly from Claude, Cursor, or any MCP client.
49
+ Supports three actions:
50
+ - `get`: View current budget status (remaining balance, hourly velocity,
51
+ circuit breaker state, per-service spending breakdown).
52
+ - `set`: Configure `budgetLimitUsd` (total session cap), `velocityLimitUsd`
53
+ (hourly max), and `alertThresholdPercent` (warning threshold).
54
+ - `reset`: Manually clear a tripped circuit breaker so the session can
55
+ resume spending.
56
+
57
+ - **Per-session budget limits** — set a maximum total spend in USD when
58
+ creating a session or at any time via the new `manage_agent_budget` tool.
59
+ When the limit is reached, the circuit breaker trips and all paid calls
60
+ are blocked until manually reset or the budget is increased.
61
+
62
+ - **Velocity (hourly) limits** — cap how fast an agent can spend. Prevents
63
+ infinite-loop scenarios where an agent drains a wallet in minutes.
64
+
65
+ - **Automatic circuit breaker** — trips instantly when budget or velocity
66
+ limits are exceeded. Sticky by design: agents cannot auto-resume spending.
67
+ Requires explicit reset via the MCP tool or API.
68
+
69
+ - **Budget status in every response** — all `call_mpp32_endpoint` and
70
+ `get_solana_token_intelligence` responses now include remaining budget,
71
+ utilization percentage, and circuit breaker state when limits are
72
+ configured.
73
+
74
+ - **Circuit breaker error handling** — when a circuit breaker is tripped,
75
+ the MCP server returns a clear, actionable message explaining what
76
+ happened and how to resume (via `manage_agent_budget`).
77
+
78
+ ### Changed
79
+
80
+ * **`formatExecuteSuccess` now includes budget status.** When budget limits
81
+ are configured, successful responses include a one-line budget summary
82
+ (remaining balance and utilization %).
83
+
84
+ ## [1.2.4] - 2026-05-11
85
+
86
+ ### Fixed
87
+
88
+ * **Self-payment now fails fast with a useful error.** When the wallet
89
+ derived from `MPP32_SOLANA_PRIVATE_KEY` (or `MPP32_PRIVATE_KEY`) is the
90
+ same address as a challenge's `payTo`, the signer used to construct the
91
+ transaction anyway and the facilitator surfaced an opaque "transaction
92
+ failed simulation" error a few seconds later. Both the SVM and EVM
93
+ signers now detect this case before signing and return:
94
+ `Refusing to sign an x402 payment to yourself: the wallet derived from
95
+ MPP32_SOLANA_PRIVATE_KEY (<addr>) is also the payment recipient (payTo)
96
+ for this service.` This happens most often when an operator uses their
97
+ own receiving wallet to test a paid endpoint they themselves run.
98
+
7
99
  ## [1.2.3] - 2026-05-11
8
100
 
9
101
  ### 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
 
@@ -24,23 +35,12 @@ MPP32 replaces all of that with one MCP server. Your agent asks for a service by
24
35
  * Run real time Solana token intelligence with alpha scoring, rug risk, whale flow, and 24 hour pump probability.
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
-
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.
38
+ * Access M32-gated exclusive APIs: Whale Tracker (1M M32), Token Comparison (2.5M M32), and Portfolio Scanner (5M M32) — free for holders, unavailable to non-holders.
39
39
 
40
40
  ## Install
41
41
 
42
42
  ```bash
43
- npx mpp32-mcp-server
43
+ npx -y mpp32-mcp-server@latest
44
44
  ```
45
45
 
46
46
  ### Claude Desktop, Claude Code, Cursor, Windsurf
@@ -52,30 +52,58 @@ Drop this into the MCP servers section of your client config.
52
52
  "mcpServers": {
53
53
  "mpp32": {
54
54
  "command": "npx",
55
- "args": ["mpp32-mcp-server"],
55
+ "args": ["-y", "mpp32-mcp-server@latest"],
56
56
  "env": {
57
57
  "MPP32_AGENT_KEY": "mpp32_agent_…",
58
- "MPP32_SOLANA_PRIVATE_KEY": "your_solana_private_key_for_usdc"
58
+ "MPP32_SOLANA_PRIVATE_KEY": "<your base58 Solana secret key>"
59
59
  }
60
60
  }
61
61
  }
62
62
  }
63
63
  ```
64
64
 
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.
65
+ Config file locations:
66
+
67
+ * **Claude Desktop (macOS):** `~/Library/Application Support/Claude/claude_desktop_config.json`
68
+ * **Claude Desktop (Windows):** `%APPDATA%\Claude\claude_desktop_config.json`
69
+ * **Cursor:** `~/.cursor/mcp.json`
70
+ * **Windsurf:** `~/.codeium/windsurf/mcp_config.json`
66
71
 
67
- `MPP32_SOLANA_PRIVATE_KEY` and `MPP32_PRIVATE_KEY` are only needed for paid services. Free services in the catalog work without any key.
72
+ Fully quit and reopen the client after editing. The MCP child process inherits env vars from this file at launch.
73
+
74
+ 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.
75
+
76
+ `MPP32_SOLANA_PRIVATE_KEY` is only needed for paid services. Free services in the catalog work without any payment key.
77
+
78
+ ### What format does `MPP32_SOLANA_PRIVATE_KEY` take?
79
+
80
+ 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:
81
+
82
+ ```bash
83
+ node -e "console.log(require('bs58').encode(Buffer.from(JSON.parse(require('fs').readFileSync('keypair.json')))))"
84
+ ```
85
+
86
+ Then paste the resulting string into the env block. Treat it like a password. It can spend any USDC and SOL in the wallet.
87
+
88
+ ### Fund the wallet with both USDC and SOL
89
+
90
+ 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.
91
+
92
+ 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
93
 
69
94
  ## Configuration
70
95
 
71
96
  | Variable | When you need it | What it does |
72
97
  |:---------|:-----------------|:-------------|
73
98
  | `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. |
99
+ | `MPP32_SOLANA_PRIVATE_KEY` | Paid x402 calls on Solana | Base58 encoded Solana secret key. Used to sign USDC payments locally. Never leaves your machine. |
100
+ | `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. |
101
+ | `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. |
102
+ | `MPP32_SOLANA_RPC_URL` | Optional | Override the Solana RPC endpoint used to fetch recent blockhashes when building x402 transactions. Defaults to `https://api.mainnet-beta.solana.com`. Set this if you hit public rate limits. |
103
+ | `MPP32_TIMEOUT_MS` | Optional | Request timeout in milliseconds for all outbound API calls. Range 1000 to 300000. Defaults to 30000 (30 seconds). |
76
104
  | `MPP32_API_URL` | Custom deployments | Override the API base URL. Defaults to `https://mpp32.org`. |
77
105
 
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.
106
+ 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
107
 
80
108
  ## Tools your agent will see
81
109
 
@@ -111,15 +139,67 @@ Under the hood:
111
139
 
112
140
  No payment logic in your code. No per provider keys to juggle.
113
141
 
142
+ ### `get_mpp32_diagnostics`
143
+
144
+ 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`.
145
+
114
146
  ### `get_solana_token_intelligence`
115
147
 
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.
148
+ 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
149
 
118
150
  ```json
119
151
  { "token": "BONK" }
120
152
  ```
121
153
 
122
- M32 holders get tiered discounts (20 percent at 250k, 40 percent at 1M) the moment their wallet is verified at the agent console.
154
+ M32 holders 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. M32 holders also get exclusive access to three token-gated APIs: Whale Tracker (1M M32), Token Comparison (2.5M M32), and Portfolio Scanner (5M M32) — these are live now and require passing your wallet address via the `X-Wallet-Address` header.
155
+
156
+ ### `get_m32_whale_tracker`
157
+
158
+ M32-gated whale analysis for any Solana token. Returns top 20 holders, concentration risk, holder distribution, and buy/sell pressure. Requires the caller to hold 1,000,000+ M32 tokens (balance verified on-chain). Free for qualifying holders.
159
+
160
+ ```json
161
+ { "token": "<mint-address>", "walletAddress": "<your-solana-wallet>" }
162
+ ```
163
+
164
+ ### `compare_tokens_m32`
165
+
166
+ M32-gated head-to-head intelligence comparison of two Solana tokens. Returns side-by-side alpha scores, rug risk, whale activity, volume, liquidity, and a winner verdict. Requires 2,500,000+ M32 tokens.
167
+
168
+ ```json
169
+ { "tokenA": "<mint-A>", "tokenB": "<mint-B>", "walletAddress": "<your-solana-wallet>" }
170
+ ```
171
+
172
+ ### `scan_portfolio_m32`
173
+
174
+ M32-gated full wallet portfolio scan. Discovers all SPL tokens in a wallet, runs intelligence on top holdings, and returns per-token analysis with aggregate risk metrics. Requires 5,000,000+ M32 tokens.
175
+
176
+ ```json
177
+ { "wallet": "<wallet-to-scan>", "walletAddress": "<your-solana-wallet>" }
178
+ ```
179
+
180
+ ### `manage_agent_budget`
181
+
182
+ View, set, or reset the economic circuit breaker for your agent session. Prevents runaway spending with infrastructure-level budget enforcement.
183
+
184
+ ```json
185
+ { "action": "get" }
186
+ ```
187
+
188
+ ```json
189
+ { "action": "set", "budgetLimitUsd": 10.0, "velocityLimitUsd": 1.0, "alertThresholdPercent": 80 }
190
+ ```
191
+
192
+ ```json
193
+ { "action": "reset" }
194
+ ```
195
+
196
+ Three actions:
197
+
198
+ * **get** returns current budget status: remaining balance, hourly velocity, circuit breaker state, and per-service spending breakdown.
199
+ * **set** configures `budgetLimitUsd` (max total session spend), `velocityLimitUsd` (max spend per hour), and `alertThresholdPercent` (warn at this percentage of budget used). All three are optional and can be set independently.
200
+ * **reset** clears a tripped circuit breaker so the session can resume spending. Circuit breakers trip automatically when limits are exceeded and stay tripped until manually reset.
201
+
202
+ Budget limits can also be set at session creation time via the agent console at mpp32.org/agent-console or by passing `budgetLimitUsd`, `velocityLimitUsd`, and `alertThresholdPercent` to `POST /api/agent/sessions`.
123
203
 
124
204
  ## How discovery works
125
205
 
@@ -147,13 +227,13 @@ Sessions are scoped, revocable, and rotate cleanly. The key is hashed at rest on
147
227
 
148
228
  ## How payment verification actually works
149
229
 
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.
230
+ 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
231
 
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.
232
+ 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
233
 
154
234
  ## For API providers
155
235
 
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.
236
+ 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
237
 
158
238
  Register at [mpp32.org/build](https://mpp32.org/build).
159
239
 
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.3";
6
+ const SERVER_VERSION = "1.3.1";
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
@@ -252,7 +252,7 @@ server.tool("get_mpp32_diagnostics", "Report what the mpp32-mcp-server detected
252
252
  `- x402 (USDC on Solana) payment: ${SOLANA_PRIVATE_KEY ? "yes" : "no — set MPP32_SOLANA_PRIVATE_KEY"}`,
253
253
  `- x402 (USDC on Base/EVM) payment: ${PRIVATE_KEY ? "yes" : "no — set MPP32_PRIVATE_KEY"}`,
254
254
  ``,
255
- `**Ready to pay end-to-end:** ${readyToPay ? "YES — try `query_intelligence` with token=\"M32\" to confirm." : "NO — see the missing items above."}`,
255
+ `**Ready to pay end-to-end:** ${readyToPay ? "YES — try `get_solana_token_intelligence` with token=\"M32\" to confirm." : "NO — see the missing items above."}`,
256
256
  ``,
257
257
  `**If a variable shows NOT SET but you set it in claude_desktop_config.json:**`,
258
258
  `1. Confirm the file path Claude Desktop actually reads:`,
@@ -308,6 +308,10 @@ server.tool("list_mpp32_services", "Browse the MPP32 federated catalog of 4,500+
308
308
  .enum(["native", "x402-bazaar", "mcp-registry", "curated", "free"])
309
309
  .optional()
310
310
  .describe("Filter by catalog source. 'native' = callable end-to-end; 'curated'/'free' = often callable; 'x402-bazaar'/'mcp-registry' = mostly listing-only."),
311
+ protocol: z
312
+ .enum(["x402", "tempo", "acp", "ap2", "agtp"])
313
+ .optional()
314
+ .describe("Filter by payment protocol (e.g. 'x402' for USDC-settled services)."),
311
315
  limit: z
312
316
  .number()
313
317
  .int()
@@ -315,7 +319,7 @@ server.tool("list_mpp32_services", "Browse the MPP32 federated catalog of 4,500+
315
319
  .max(500)
316
320
  .optional()
317
321
  .describe("Max results (default 100, max 500)."),
318
- }, async ({ category, q, source, limit }) => {
322
+ }, async ({ category, q, source, protocol, limit }) => {
319
323
  try {
320
324
  const url = new URL("/api/agent/services", API_URL);
321
325
  if (category)
@@ -324,6 +328,8 @@ server.tool("list_mpp32_services", "Browse the MPP32 federated catalog of 4,500+
324
328
  url.searchParams.set("q", q);
325
329
  if (source)
326
330
  url.searchParams.set("source", source);
331
+ if (protocol)
332
+ url.searchParams.set("protocol", protocol);
327
333
  url.searchParams.set("limit", String(limit ?? 100));
328
334
  const res = await fetchWithTimeout(url.toString(), { headers: buildHeaders() });
329
335
  if (!res.ok) {
@@ -460,6 +466,221 @@ server.tool("get_solana_token_intelligence", "Get real-time Solana token intelli
460
466
  // Legacy path — direct call to /api/intelligence with manual 402 handling.
461
467
  return await legacyIntelligenceCall(token, walletAddress);
462
468
  });
469
+ // ── Tool 4: M32-gated Whale Tracker ───────────────────────────────────────
470
+ server.tool("get_m32_whale_tracker", "M32-gated whale analysis for any Solana token. Returns top 20 holders, concentration risk, holder distribution, and buy/sell pressure. Requires the caller to hold 1,000,000+ M32 tokens (balance verified on-chain via X-Wallet-Address header). Free for qualifying holders — no payment required. Returns 403 if the wallet holds insufficient M32.", {
471
+ token: z
472
+ .string()
473
+ .describe("Solana token mint address to analyze for whale activity."),
474
+ walletAddress: z
475
+ .string()
476
+ .describe("Your Solana wallet address. M32 balance is checked on-chain to verify you hold 1M+ M32."),
477
+ }, async ({ token, walletAddress }) => {
478
+ try {
479
+ const res = await fetchWithTimeout(`${API_URL}/api/m32/whale-tracker`, {
480
+ method: "POST",
481
+ headers: buildHeaders({
482
+ "Content-Type": "application/json",
483
+ "X-Wallet-Address": safeHeaderValue("walletAddress", walletAddress),
484
+ }),
485
+ body: JSON.stringify({ token }),
486
+ });
487
+ const text = await res.text();
488
+ let formatted;
489
+ try {
490
+ formatted = JSON.stringify(JSON.parse(text), null, 2);
491
+ }
492
+ catch {
493
+ formatted = text;
494
+ }
495
+ if (res.status === 403) {
496
+ return { content: [{ type: "text", text: `**Access denied.** Whale Tracker requires holding 1,000,000+ M32 tokens. Your wallet does not meet the threshold.\n\nBuy M32: https://raydium.io/swap/?inputMint=sol&outputMint=6hKtz8FV7cAQMrbjcBZeTQAcrYep3WCM83164JpJpump` }] };
497
+ }
498
+ return { content: [{ type: "text", text: `**Whale Tracker** — \`${token}\`\n\n\`\`\`json\n${formatted}\n\`\`\`` }] };
499
+ }
500
+ catch (err) {
501
+ return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }] };
502
+ }
503
+ });
504
+ // ── Tool 5: M32-gated Token Comparison ────────────────────────────────────
505
+ server.tool("compare_tokens_m32", "M32-gated head-to-head intelligence comparison of two Solana tokens. Returns side-by-side alpha scores, rug risk, whale activity, volume, liquidity, market data, and a winner verdict. Requires the caller to hold 2,500,000+ M32 tokens (balance verified on-chain via X-Wallet-Address header). Free for qualifying holders. Returns 403 if insufficient M32.", {
506
+ tokenA: z
507
+ .string()
508
+ .describe("First Solana token mint address."),
509
+ tokenB: z
510
+ .string()
511
+ .describe("Second Solana token mint address."),
512
+ walletAddress: z
513
+ .string()
514
+ .describe("Your Solana wallet address. M32 balance is checked on-chain to verify you hold 2.5M+ M32."),
515
+ }, async ({ tokenA, tokenB, walletAddress }) => {
516
+ try {
517
+ const res = await fetchWithTimeout(`${API_URL}/api/m32/compare`, {
518
+ method: "POST",
519
+ headers: buildHeaders({
520
+ "Content-Type": "application/json",
521
+ "X-Wallet-Address": safeHeaderValue("walletAddress", walletAddress),
522
+ }),
523
+ body: JSON.stringify({ tokenA, tokenB }),
524
+ });
525
+ const text = await res.text();
526
+ let formatted;
527
+ try {
528
+ formatted = JSON.stringify(JSON.parse(text), null, 2);
529
+ }
530
+ catch {
531
+ formatted = text;
532
+ }
533
+ if (res.status === 403) {
534
+ return { content: [{ type: "text", text: `**Access denied.** Token Comparison requires holding 2,500,000+ M32 tokens. Your wallet does not meet the threshold.\n\nBuy M32: https://raydium.io/swap/?inputMint=sol&outputMint=6hKtz8FV7cAQMrbjcBZeTQAcrYep3WCM83164JpJpump` }] };
535
+ }
536
+ return { content: [{ type: "text", text: `**Token Comparison** — \`${tokenA}\` vs \`${tokenB}\`\n\n\`\`\`json\n${formatted}\n\`\`\`` }] };
537
+ }
538
+ catch (err) {
539
+ return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }] };
540
+ }
541
+ });
542
+ // ── Tool 6: M32-gated Portfolio Scanner ───────────────────────────────────
543
+ server.tool("scan_portfolio_m32", "M32-gated full wallet portfolio scan. Discovers all SPL tokens in a Solana wallet, runs intelligence on top holdings, and returns per-token analysis with aggregate portfolio risk metrics. Requires the caller to hold 5,000,000+ M32 tokens (balance verified on-chain via X-Wallet-Address header). Free for qualifying holders. Returns 403 if insufficient M32.", {
544
+ wallet: z
545
+ .string()
546
+ .describe("Solana wallet address to scan for token holdings."),
547
+ walletAddress: z
548
+ .string()
549
+ .describe("Your Solana wallet address. M32 balance is checked on-chain to verify you hold 5M+ M32."),
550
+ }, async ({ wallet, walletAddress }) => {
551
+ try {
552
+ const res = await fetchWithTimeout(`${API_URL}/api/m32/portfolio`, {
553
+ method: "POST",
554
+ headers: buildHeaders({
555
+ "Content-Type": "application/json",
556
+ "X-Wallet-Address": safeHeaderValue("walletAddress", walletAddress),
557
+ }),
558
+ body: JSON.stringify({ wallet }),
559
+ });
560
+ const text = await res.text();
561
+ let formatted;
562
+ try {
563
+ formatted = JSON.stringify(JSON.parse(text), null, 2);
564
+ }
565
+ catch {
566
+ formatted = text;
567
+ }
568
+ if (res.status === 403) {
569
+ return { content: [{ type: "text", text: `**Access denied.** Portfolio Scanner requires holding 5,000,000+ M32 tokens. Your wallet does not meet the threshold.\n\nBuy M32: https://raydium.io/swap/?inputMint=sol&outputMint=6hKtz8FV7cAQMrbjcBZeTQAcrYep3WCM83164JpJpump` }] };
570
+ }
571
+ return { content: [{ type: "text", text: `**Portfolio Scanner** — wallet \`${wallet}\`\n\n\`\`\`json\n${formatted}\n\`\`\`` }] };
572
+ }
573
+ catch (err) {
574
+ return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }] };
575
+ }
576
+ });
577
+ // ── Tool 8: manage_agent_budget ────────────────────────────────────────────
578
+ server.tool("manage_agent_budget", "View, set, or reset the spending circuit breaker for your MPP32 agent session. Use 'get' to check current budget status (remaining budget, hourly velocity, circuit breaker state). Use 'set' to configure spending limits (budget cap in USD, hourly velocity limit, alert threshold percentage). Use 'reset' to manually reset a tripped circuit breaker so the session can resume spending. Circuit breakers trip automatically when budget or velocity limits are exceeded, preventing runaway agent spending.", {
579
+ action: z.enum(["get", "set", "reset"]).describe("Action: 'get' = view budget status, 'set' = update limits, 'reset' = clear tripped circuit breaker"),
580
+ budgetLimitUsd: z.number().positive().max(1_000_000).optional().describe("Maximum total session spend in USD. Only used with action='set'."),
581
+ velocityLimitUsd: z.number().positive().max(1_000_000).optional().describe("Maximum spend per hour in USD. Only used with action='set'."),
582
+ alertThresholdPercent: z.number().int().min(1).max(100).optional().describe("Budget percentage at which to warn (e.g. 80 = warn at 80% spent). Only used with action='set'."),
583
+ }, async ({ action, budgetLimitUsd, velocityLimitUsd, alertThresholdPercent }) => {
584
+ if (!AGENT_KEY) {
585
+ return {
586
+ content: [{ type: "text", text: "**MPP32_AGENT_KEY not configured.** Set it in your MCP config to manage budgets." }],
587
+ };
588
+ }
589
+ try {
590
+ if (action === "get") {
591
+ const res = await fetchWithTimeout(new URL("/api/agent/spending", API_URL).toString(), { headers: buildHeaders() });
592
+ if (!res.ok) {
593
+ const err = await res.json().catch(() => null);
594
+ return { content: [{ type: "text", text: `Error fetching budget: ${err?.error?.message ?? res.statusText}` }] };
595
+ }
596
+ const data = (await res.json()).data;
597
+ const lines = [
598
+ `**MPP32 Session Budget Status**`,
599
+ ``,
600
+ ];
601
+ if (data.budgetLimitUsd != null) {
602
+ lines.push(`Budget: $${data.totalSpentUsd.toFixed(4)} spent of $${data.budgetLimitUsd.toFixed(4)} ($${data.remainingBudgetUsd.toFixed(4)} remaining, ${data.budgetUtilizationPercent}% used)`);
603
+ }
604
+ else {
605
+ lines.push(`Budget: unlimited (no cap set)`);
606
+ lines.push(`Total spent: $${data.totalSpentUsd.toFixed(4)} across ${data.totalSettledCalls} settled calls`);
607
+ }
608
+ if (data.velocityLimitUsd != null) {
609
+ lines.push(`Velocity: $${data.hourlySpendUsd.toFixed(4)}/hr of $${data.velocityLimitUsd.toFixed(4)}/hr limit (${data.hourlySettledCalls} calls this hour)`);
610
+ }
611
+ if (data.circuitBreakerTripped) {
612
+ lines.push(``);
613
+ lines.push(`**CIRCUIT BREAKER TRIPPED** — ${data.circuitBreakerReason}`);
614
+ lines.push(`Tripped at: ${data.circuitBreakerTrippedAt}`);
615
+ lines.push(`Use action="reset" to resume spending.`);
616
+ }
617
+ if (data.byService?.length) {
618
+ lines.push(``);
619
+ lines.push(`**Spending by service:**`);
620
+ for (const s of data.byService) {
621
+ lines.push(`- ${s.service}: $${s.totalSpentUsd.toFixed(4)} (${s.count} calls)`);
622
+ }
623
+ }
624
+ return { content: [{ type: "text", text: lines.join("\n") }] };
625
+ }
626
+ if (action === "set") {
627
+ const payload = {};
628
+ if (budgetLimitUsd !== undefined)
629
+ payload.budgetLimitUsd = budgetLimitUsd;
630
+ if (velocityLimitUsd !== undefined)
631
+ payload.velocityLimitUsd = velocityLimitUsd;
632
+ if (alertThresholdPercent !== undefined)
633
+ payload.alertThresholdPercent = alertThresholdPercent;
634
+ if (Object.keys(payload).length === 0) {
635
+ return { content: [{ type: "text", text: "Provide at least one of: budgetLimitUsd, velocityLimitUsd, alertThresholdPercent." }] };
636
+ }
637
+ const res = await fetchWithTimeout(new URL("/api/agent/budget", API_URL).toString(), {
638
+ method: "PATCH",
639
+ headers: buildHeaders({ "Content-Type": "application/json" }),
640
+ body: JSON.stringify(payload),
641
+ });
642
+ if (!res.ok) {
643
+ const err = await res.json().catch(() => null);
644
+ return { content: [{ type: "text", text: `Error updating budget: ${err?.error?.message ?? res.statusText}` }] };
645
+ }
646
+ const data = (await res.json()).data;
647
+ const lines = [
648
+ `**Budget updated successfully.**`,
649
+ ``,
650
+ data.budgetLimitUsd != null ? `Budget limit: $${data.budgetLimitUsd.toFixed(4)}` : `Budget limit: unlimited`,
651
+ data.velocityLimitUsd != null ? `Velocity limit: $${data.velocityLimitUsd.toFixed(4)}/hr` : `Velocity limit: unlimited`,
652
+ `Total spent: $${data.totalSpentUsd.toFixed(4)}`,
653
+ data.remainingBudgetUsd != null ? `Remaining: $${data.remainingBudgetUsd.toFixed(4)}` : ``,
654
+ data.circuitBreakerTripped ? `\n**Note:** Circuit breaker is still tripped. Use action="reset" to resume.` : ``,
655
+ ].filter(Boolean);
656
+ return { content: [{ type: "text", text: lines.join("\n") }] };
657
+ }
658
+ if (action === "reset") {
659
+ const res = await fetchWithTimeout(new URL("/api/agent/circuit-breaker/reset", API_URL).toString(), {
660
+ method: "POST",
661
+ headers: buildHeaders({ "Content-Type": "application/json" }),
662
+ });
663
+ if (!res.ok) {
664
+ const err = await res.json().catch(() => null);
665
+ return { content: [{ type: "text", text: `Error resetting circuit breaker: ${err?.error?.message ?? res.statusText}` }] };
666
+ }
667
+ const data = (await res.json()).data;
668
+ const lines = [
669
+ data.previousReason
670
+ ? `**Circuit breaker reset.** Previous reason: ${data.previousReason}.`
671
+ : `**Circuit breaker was not tripped.** No action needed.`,
672
+ ``,
673
+ data.budgetLimitUsd != null ? `Budget: $${data.totalSpentUsd.toFixed(4)} / $${data.budgetLimitUsd.toFixed(4)} ($${data.remainingBudgetUsd.toFixed(4)} remaining)` : ``,
674
+ `Session can resume spending.`,
675
+ ].filter(Boolean);
676
+ return { content: [{ type: "text", text: lines.join("\n") }] };
677
+ }
678
+ return { content: [{ type: "text", text: "Unknown action. Use 'get', 'set', or 'reset'." }] };
679
+ }
680
+ catch (err) {
681
+ return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }] };
682
+ }
683
+ });
463
684
  // ── Core: agent/execute path with 402 sign-and-retry ────────────────────────
464
685
  async function callViaAgentExecute(service, method, body, query, path) {
465
686
  try {
@@ -637,6 +858,10 @@ function formatExecuteSuccess(resp, protoOverride) {
637
858
  else if (meta?.paymentMethod === "unsettled") {
638
859
  lines.push(`Service responded but no payment was verified. This should not happen for paid services.`);
639
860
  }
861
+ if (meta?.budget && meta.budget.budgetLimitUsd != null) {
862
+ const b = meta.budget;
863
+ lines.push(`Budget: $${b.remainingBudgetUsd.toFixed(4)} remaining (${b.budgetUtilizationPercent}% of $${b.budgetLimitUsd.toFixed(4)} used)`);
864
+ }
640
865
  lines.push("");
641
866
  lines.push("```json");
642
867
  lines.push(formatted);
@@ -645,6 +870,25 @@ function formatExecuteSuccess(resp, protoOverride) {
645
870
  }
646
871
  function formatExecuteHardError(status, body) {
647
872
  const code = body?.error?.code;
873
+ if (code === "MPP32_CIRCUIT_BREAKER_TRIPPED" || (status === 429 && body?.error?.budgetStatus)) {
874
+ const bs = body?.error?.budgetStatus;
875
+ return {
876
+ content: [
877
+ {
878
+ type: "text",
879
+ text: [
880
+ `**Circuit breaker tripped** — spending limit reached.`,
881
+ ``,
882
+ body?.error?.message ?? "Session budget exhausted.",
883
+ bs?.budgetLimitUsd != null ? `Budget: $${bs.totalSpentUsd?.toFixed(4) ?? "?"} / $${bs.budgetLimitUsd.toFixed(4)}` : "",
884
+ bs?.velocityLimitUsd != null ? `Velocity: $${bs.hourlySpendUsd?.toFixed(4) ?? "?"}/hr of $${bs.velocityLimitUsd.toFixed(4)}/hr limit` : "",
885
+ ``,
886
+ `To resume: use \`manage_agent_budget\` with action="reset", or increase the budget with action="set".`,
887
+ ].filter(Boolean).join("\n"),
888
+ },
889
+ ],
890
+ };
891
+ }
648
892
  if (code === "NOT_HTTP_CALLABLE") {
649
893
  return {
650
894
  content: [
@@ -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));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mpp32-mcp-server",
3
- "version": "1.2.3",
3
+ "version": "1.3.1",
4
4
  "mcpName": "io.github.MPP32/mpp32-mcp-server",
5
5
  "description": "Payment layer for AI agents. One MCP, five protocols, thousands of paid APIs your agent can call.",
6
6
  "type": "module",