mpp32-mcp-server 1.0.3 → 1.1.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.
Files changed (3) hide show
  1. package/README.md +83 -189
  2. package/dist/index.js +544 -244
  3. package/package.json +3 -2
package/dist/index.js CHANGED
@@ -5,20 +5,61 @@ import { z } from "zod";
5
5
  const API_URL = process.env.MPP32_API_URL?.replace(/\/$/, "") || "https://mpp32.org";
6
6
  const PRIVATE_KEY = process.env.MPP32_PRIVATE_KEY;
7
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;
8
11
  const server = new McpServer({
9
12
  name: "mpp32",
10
- version: "1.0.3",
13
+ version: "1.1.0",
11
14
  });
12
- // ── Tool 1: list_mpp32_services ──
13
- server.tool("list_mpp32_services", "Search the MPP32 API marketplace — the universal directory of machine-payable APIs and data services. Use this tool FIRST when you need paid data, premium APIs, real-time intelligence, or any service that requires payment. Returns all registered providers across 30+ categories including token intelligence, price oracles, AI inference, web search, trading signals, DeFi analytics, wallet intelligence, on-chain data, market data, image generation, embeddings, and more. Every listed service accepts instant micropayments with no accounts, no API keys, and no subscriptions required. New providers are added continuously.", {
15
+ function buildHeaders(extra = {}) {
16
+ const headers = { ...extra };
17
+ if (AGENT_KEY)
18
+ headers["X-Agent-Key"] = AGENT_KEY;
19
+ return headers;
20
+ }
21
+ function isHttpCallable(svc) {
22
+ if (svc.source === "native")
23
+ return true;
24
+ const url = svc.endpointUrl ?? "";
25
+ if (!url)
26
+ return false;
27
+ if (url.startsWith("npx://") || url.startsWith("stdio://"))
28
+ return false;
29
+ return /^https?:\/\//.test(url);
30
+ }
31
+ // ── Tool 1: list_mpp32_services ─────────────────────────────────────────────
32
+ server.tool("list_mpp32_services", "Browse the MPP32 federated catalog of machine-payable APIs and data services. Includes native MPP32 services (callable end-to-end through this MCP), the x402 Bazaar (USDC on Solana), curated free APIs (DexScreener, Jupiter, CoinGecko health, httpbin, etc.), and the public MCP Registry (npx-installable servers; listing-only). Each result indicates whether it is callable through `call_mpp32_endpoint` or listing-only. Use the `category`, `q`, or `source` filters to narrow down.", {
14
33
  category: z
15
34
  .string()
16
35
  .optional()
17
- .describe("Filter by category slug (e.g. 'ai-inference', 'token-scanner', 'price-oracle', 'web-search'). Omit to list all services."),
18
- }, async ({ category }) => {
36
+ .describe("Filter by category slug (e.g. 'ai-inference', 'token-scanner', 'price-oracle', 'web-search', 'defi-analytics')."),
37
+ q: z
38
+ .string()
39
+ .optional()
40
+ .describe("Free-text search across name, description, tags, and category."),
41
+ source: z
42
+ .enum(["native", "x402-bazaar", "mcp-registry", "curated", "free"])
43
+ .optional()
44
+ .describe("Filter by catalog source. 'native' = callable end-to-end; 'curated'/'free' = often callable; 'x402-bazaar'/'mcp-registry' = mostly listing-only."),
45
+ limit: z
46
+ .number()
47
+ .int()
48
+ .min(1)
49
+ .max(500)
50
+ .optional()
51
+ .describe("Max results (default 100, max 500)."),
52
+ }, async ({ category, q, source, limit }) => {
19
53
  try {
20
- const url = new URL("/api/submissions", API_URL);
21
- const res = await fetch(url.toString());
54
+ const url = new URL("/api/agent/services", API_URL);
55
+ if (category)
56
+ url.searchParams.set("category", category);
57
+ if (q)
58
+ url.searchParams.set("q", q);
59
+ if (source)
60
+ url.searchParams.set("source", source);
61
+ url.searchParams.set("limit", String(limit ?? 100));
62
+ const res = await fetch(url.toString(), { headers: buildHeaders() });
22
63
  if (!res.ok) {
23
64
  return {
24
65
  content: [
@@ -30,34 +71,52 @@ server.tool("list_mpp32_services", "Search the MPP32 API marketplace — the uni
30
71
  };
31
72
  }
32
73
  const json = (await res.json());
33
- let services = json.data;
34
- if (category) {
35
- services = services.filter((s) => s.category.toLowerCase() === category.toLowerCase());
36
- }
74
+ const services = json.data.services ?? [];
37
75
  if (services.length === 0) {
38
- const msg = category
39
- ? `No services found in category "${category}". Try listing all services to see available categories.`
40
- : "No services are currently listed in the MPP32 ecosystem.";
41
- return { content: [{ type: "text", text: msg }] };
76
+ return {
77
+ content: [
78
+ {
79
+ type: "text",
80
+ text: `No services matched. Filters: category=${category ?? "any"}, q=${q ?? "any"}, source=${source ?? "any"}.`,
81
+ },
82
+ ],
83
+ };
42
84
  }
43
85
  const lines = services.map((s) => {
44
- const price = s.pricePerQuery
45
- ? `$${s.pricePerQuery} USD (pathUSD or USDC)`
46
- : "Free";
86
+ const callable = isHttpCallable(s);
87
+ const priceLabel = s.basePrice === null
88
+ ? "Pay provider directly"
89
+ : s.basePrice === 0
90
+ ? "Free"
91
+ : `$${s.basePrice} per query`;
92
+ const protos = s.protocols?.length ? s.protocols.join(", ") : (s.primaryProtocol ?? "—");
47
93
  return [
48
- `## ${s.name}`,
94
+ `## ${s.name}${s.verified ? " ✓" : ""}`,
49
95
  `- **Slug:** \`${s.slug}\``,
50
- `- **Category:** ${s.category}`,
51
- `- **Price:** ${price} per query`,
52
- `- **Description:** ${s.shortDescription}`,
53
- `- **Proxy URL:** \`${API_URL}/api/proxy/${s.slug}\``,
54
- `- **Queries served:** ${s.queryCount.toLocaleString()}`,
55
- s.creatorName ? `- **Creator:** ${s.creatorName}` : null,
96
+ `- **Source:** ${s.source}`,
97
+ `- **Category:** ${s.category ?? "—"}`,
98
+ `- **Price:** ${priceLabel}`,
99
+ `- **Protocols:** ${protos}`,
100
+ `- **Callable via this MCP:** ${callable ? "Yes — use `call_mpp32_endpoint`" : "No — listing only"}`,
101
+ s.description ? `- **Description:** ${s.description}` : null,
102
+ s.endpointUrl && !callable ? `- **Install / direct URL:** \`${s.endpointUrl}\`` : null,
103
+ s.websiteUrl ? `- **Website:** ${s.websiteUrl}` : null,
56
104
  ]
57
105
  .filter(Boolean)
58
106
  .join("\n");
59
107
  });
60
- const header = `# MPP32 API Marketplace — ${services.length} service${services.length !== 1 ? "s" : ""} available${category ? ` in "${category}"` : ""}\n\nEvery service below accepts instant micropayments. Use \`call_mpp32_endpoint\` with the service slug to call any of them. Payment is handled automatically.\n\nProviders can register new APIs at https://mpp32.org/build — all registered services are instantly discoverable by every agent with the MPP32 MCP server installed.\n`;
108
+ const counts = json.data.counts;
109
+ const callableCount = services.filter(isHttpCallable).length;
110
+ const header = [
111
+ `# MPP32 Federated Catalog — ${services.length} result${services.length !== 1 ? "s" : ""}`,
112
+ ``,
113
+ `**Sources:** ${counts.native} native + ${counts.external} external. **Callable through this MCP:** ${callableCount}.`,
114
+ ``,
115
+ AGENT_KEY
116
+ ? `Calls through \`call_mpp32_endpoint\` are tracked in your dashboard at ${API_URL}/agent-console (your X-Agent-Key is set).`
117
+ : `**Tip:** set \`MPP32_AGENT_KEY\` in your MCP config to track usage at ${API_URL}/agent-console. Get a key at ${API_URL}/agent-console.`,
118
+ ``,
119
+ ].join("\n");
61
120
  return {
62
121
  content: [{ type: "text", text: header + "\n" + lines.join("\n\n") }],
63
122
  };
@@ -73,244 +132,484 @@ server.tool("list_mpp32_services", "Search the MPP32 API marketplace — the uni
73
132
  };
74
133
  }
75
134
  });
76
- // ── Tool 2: call_mpp32_endpoint ──
77
- server.tool("call_mpp32_endpoint", "Call any machine-payable API in the MPP32 marketplace. Handles the entire payment flow automatically you send the request, MPP32 negotiates payment via the best available protocol (x402 USDC on Solana, Tempo pathUSD, ACP, AP2, or AGTP), signs the transaction, and returns the API response. Use list_mpp32_services first to find the service slug you need. Works with any provider registered in the ecosystem. Requires at least one payment key configured (MPP32_SOLANA_PRIVATE_KEY for USDC or MPP32_PRIVATE_KEY for pathUSD).", {
135
+ // ── Tool 2: call_mpp32_endpoint ─────────────────────────────────────────────
136
+ server.tool("call_mpp32_endpoint", "Call any HTTP-callable service in the MPP32 federated catalog. Free services return immediately. Paid services return a 402 challenge that this tool will sign and retry automatically when a payment key (MPP32_SOLANA_PRIVATE_KEY for x402/USDC, MPP32_PRIVATE_KEY for Tempo/pathUSD) is configured. Set MPP32_AGENT_KEY for dashboard tracking. Use `list_mpp32_services` first to find a slug. Listing-only entries (npx-installable MCP servers, x402 Bazaar non-mirrored items) cannot be called through this tool install them directly per the catalog instructions.", {
78
137
  slug: z
79
138
  .string()
80
- .describe("The service slug to call (e.g. 'solana-token-intelligence'). Use list_mpp32_services to discover available slugs."),
139
+ .describe("Service slug from `list_mpp32_services` (e.g. 'mpp32-intelligence')."),
81
140
  method: z
82
141
  .enum(["GET", "POST", "PUT", "DELETE"])
83
- .default("GET")
84
- .describe("HTTP method for the request"),
142
+ .default("POST")
143
+ .describe("HTTP method."),
85
144
  body: z
86
- .string()
145
+ .union([z.string(), z.record(z.unknown())])
87
146
  .optional()
88
- .describe("JSON request body (for POST/PUT requests)"),
147
+ .describe("JSON body (object or stringified) for POST/PUT/DELETE."),
89
148
  query: z
90
149
  .record(z.string())
91
150
  .optional()
92
- .describe("URL query parameters as key-value pairs"),
151
+ .describe("URL query parameters as key-value pairs."),
93
152
  }, async ({ slug, method, body, query }) => {
94
- if (!PRIVATE_KEY && !SOLANA_PRIVATE_KEY) {
153
+ // Normalize body to an object so it can be JSON.stringified by the upstream call
154
+ let parsedBody = body;
155
+ if (typeof body === "string") {
156
+ try {
157
+ parsedBody = body.length > 0 ? JSON.parse(body) : undefined;
158
+ }
159
+ catch {
160
+ parsedBody = body;
161
+ }
162
+ }
163
+ if (AGENT_KEY) {
164
+ return await callViaAgentExecute(slug, method, parsedBody, query);
165
+ }
166
+ // Legacy path — only works for native services with payment keys
167
+ return await callViaLegacyProxy(slug, method, parsedBody, query);
168
+ });
169
+ // ── Tool 3: get_solana_token_intelligence ───────────────────────────────────
170
+ server.tool("get_solana_token_intelligence", "Get real-time Solana token intelligence from the MPP32 Intelligence Oracle. Returns alpha score (0-100), rug risk assessment, whale activity, smart money signals, 24h pump probability, projected ROI ranges, and aggregated DexScreener/Jupiter/CoinGecko market data. Costs $0.008 per query, paid automatically via x402 (USDC on Solana) or Tempo (pathUSD on Eth L2). M32 token holders receive up to 40% discount once their wallet is signature-verified. Set MPP32_AGENT_KEY in config to attribute calls to your dashboard.", {
171
+ token: z
172
+ .string()
173
+ .describe("Solana token mint address or ticker symbol (e.g. SOL, BONK, JUP, M32, or full base58 address)."),
174
+ walletAddress: z
175
+ .string()
176
+ .optional()
177
+ .describe("Optional Solana wallet address. Used for M32-holder discount preview; discount only applies after SIWS wallet-signature verification."),
178
+ }, async ({ token, walletAddress }) => {
179
+ if (AGENT_KEY) {
180
+ // Route through /api/agent/execute so the call shows up in the user's dashboard.
181
+ return await callViaAgentExecute("intelligence", "POST", { token, ...(walletAddress ? { walletAddress } : {}) }, undefined);
182
+ }
183
+ // Legacy path — direct call to /api/intelligence with manual 402 handling.
184
+ return await legacyIntelligenceCall(token, walletAddress);
185
+ });
186
+ // ── Core: agent/execute path with 402 sign-and-retry ────────────────────────
187
+ async function callViaAgentExecute(service, method, body, query) {
188
+ try {
189
+ const execUrl = new URL("/api/agent/execute", API_URL).toString();
190
+ const reqBody = JSON.stringify({
191
+ service,
192
+ method,
193
+ ...(body !== undefined ? { body } : {}),
194
+ ...(query ? { query } : {}),
195
+ });
196
+ // Round 1: no payment headers
197
+ const firstRes = await fetch(execUrl, {
198
+ method: "POST",
199
+ headers: buildHeaders({ "Content-Type": "application/json" }),
200
+ body: reqBody,
201
+ });
202
+ // Hard errors from /execute (auth, validation, not-callable)
203
+ if (!firstRes.ok) {
204
+ const errJson = (await firstRes.json().catch(() => null));
205
+ return formatExecuteHardError(firstRes.status, errJson);
206
+ }
207
+ const firstJson = (await firstRes.json());
208
+ // Wrapped 402 — sign and retry if we have keys
209
+ const paymentRequired = detectPaymentRequired(firstJson);
210
+ if (paymentRequired) {
211
+ if (!PRIVATE_KEY && !SOLANA_PRIVATE_KEY) {
212
+ return paymentKeyMissingMessage(firstJson, paymentRequired);
213
+ }
214
+ return await signAndRetry(execUrl, reqBody, paymentRequired);
215
+ }
216
+ // Free or otherwise-successful call
217
+ return formatExecuteSuccess(firstJson);
218
+ }
219
+ catch (err) {
220
+ return {
221
+ content: [
222
+ {
223
+ type: "text",
224
+ text: `Network error reaching ${API_URL}: ${err instanceof Error ? err.message : String(err)}. Check connectivity and that MPP32_API_URL (if set) is correct.`,
225
+ },
226
+ ],
227
+ };
228
+ }
229
+ }
230
+ function detectPaymentRequired(resp) {
231
+ const result = resp?.data?.result;
232
+ if (!result?.error || result.error.code !== "PAYMENT_REQUIRED")
233
+ return null;
234
+ const headers = result.error.challenge?.headers ?? {};
235
+ return {
236
+ wwwAuthenticate: headers["www-authenticate"],
237
+ paymentRequired: headers["payment-required"],
238
+ rawHeaders: headers,
239
+ priceQuoted: result.error.challenge?.priceQuoted ?? resp.data.meta?.priceQuoted ?? 0,
240
+ serviceName: resp.data.meta?.service ?? "service",
241
+ };
242
+ }
243
+ async function signAndRetry(execUrl, reqBody, challenge) {
244
+ const paymentHeaders = {};
245
+ let usedProtocol = "";
246
+ // Prefer x402 if Solana key present and server offered Payment-Required
247
+ if (challenge.paymentRequired && SOLANA_PRIVATE_KEY) {
248
+ try {
249
+ paymentHeaders["X-Payment"] = await completeX402Payment(challenge.paymentRequired, SOLANA_PRIVATE_KEY);
250
+ usedProtocol = "USDC (x402)";
251
+ }
252
+ catch (err) {
253
+ // Fall through to Tempo if available
254
+ if (challenge.wwwAuthenticate && PRIVATE_KEY) {
255
+ const parsed = parseWwwAuthenticate(challenge.wwwAuthenticate);
256
+ try {
257
+ const token = await completeTempoPayment(parsed.params, PRIVATE_KEY);
258
+ paymentHeaders["Authorization"] = `Payment ${token}`;
259
+ usedProtocol = "pathUSD (Tempo)";
260
+ }
261
+ catch (tempoErr) {
262
+ return paymentFailedMessage(challenge, "x402+tempo", `${err}; ${tempoErr}`);
263
+ }
264
+ }
265
+ else {
266
+ return paymentFailedMessage(challenge, "x402", err);
267
+ }
268
+ }
269
+ }
270
+ else if (challenge.wwwAuthenticate && PRIVATE_KEY) {
271
+ const parsed = parseWwwAuthenticate(challenge.wwwAuthenticate);
272
+ if (!parsed.scheme || !parsed.params) {
273
+ return {
274
+ content: [
275
+ {
276
+ type: "text",
277
+ text: `Could not parse Tempo challenge. WWW-Authenticate: ${challenge.wwwAuthenticate}`,
278
+ },
279
+ ],
280
+ };
281
+ }
282
+ try {
283
+ const token = await completeTempoPayment(parsed.params, PRIVATE_KEY);
284
+ paymentHeaders["Authorization"] = `Payment ${token}`;
285
+ usedProtocol = "pathUSD (Tempo)";
286
+ }
287
+ catch (err) {
288
+ return paymentFailedMessage(challenge, "tempo", err);
289
+ }
290
+ }
291
+ else {
292
+ const offered = [
293
+ challenge.wwwAuthenticate ? "Tempo (pathUSD)" : null,
294
+ challenge.paymentRequired ? "x402 (USDC)" : null,
295
+ ]
296
+ .filter(Boolean)
297
+ .join(", ");
298
+ const have = [PRIVATE_KEY ? "Tempo" : null, SOLANA_PRIVATE_KEY ? "x402" : null]
299
+ .filter(Boolean)
300
+ .join(", ") || "none";
301
+ return {
302
+ content: [
303
+ {
304
+ type: "text",
305
+ text: `No compatible payment method. Server offers: ${offered}. You have keys for: ${have}.`,
306
+ },
307
+ ],
308
+ };
309
+ }
310
+ // Round 2: with payment headers
311
+ const secondRes = await fetch(execUrl, {
312
+ method: "POST",
313
+ headers: buildHeaders({ "Content-Type": "application/json", ...paymentHeaders }),
314
+ body: reqBody,
315
+ });
316
+ if (!secondRes.ok) {
317
+ const errJson = (await secondRes.json().catch(() => null));
318
+ return formatExecuteHardError(secondRes.status, errJson);
319
+ }
320
+ const secondJson = (await secondRes.json());
321
+ return formatExecuteSuccess(secondJson, usedProtocol);
322
+ }
323
+ function formatExecuteSuccess(resp, protoOverride) {
324
+ const meta = resp.data.meta;
325
+ const result = resp.data.result;
326
+ const formatted = (() => {
327
+ try {
328
+ return JSON.stringify(result, null, 2);
329
+ }
330
+ catch {
331
+ return String(result);
332
+ }
333
+ })();
334
+ const lines = [];
335
+ const safeStatus = meta?.statusCode ?? 0;
336
+ const safeLatency = meta?.latencyMs ?? 0;
337
+ lines.push(`**${meta?.service ?? "service"}** — HTTP ${safeStatus} (${safeLatency}ms)`);
338
+ if (meta?.isFree) {
339
+ lines.push(`Free service. No payment.`);
340
+ }
341
+ else if (meta?.settled) {
342
+ const proto = protoOverride ?? meta.paymentMethod ?? "—";
343
+ const txLine = meta.settlementTxSignature
344
+ ? `Settlement tx: ${meta.settlementExplorerUrl ?? meta.settlementTxSignature}`
345
+ : "Settled by upstream facilitator.";
346
+ const settled = typeof meta.priceSettled === "number" ? meta.priceSettled.toFixed(6) : "—";
347
+ lines.push(`Paid $${settled} via ${proto}. ${txLine}`);
348
+ if ((meta.discountPercent ?? 0) > 0) {
349
+ lines.push(`M32 holder discount applied: ${meta.discountPercent}%.`);
350
+ }
351
+ }
352
+ else if (meta?.paymentMethod === "unsettled") {
353
+ lines.push(`Service responded but no payment was verified. This should not happen for paid services.`);
354
+ }
355
+ lines.push("");
356
+ lines.push("```json");
357
+ lines.push(formatted);
358
+ lines.push("```");
359
+ return { content: [{ type: "text", text: lines.join("\n") }] };
360
+ }
361
+ function formatExecuteHardError(status, body) {
362
+ const code = body?.error?.code;
363
+ if (code === "NOT_HTTP_CALLABLE") {
95
364
  return {
96
365
  content: [
97
366
  {
98
367
  type: "text",
99
368
  text: [
100
- "**No payment key configured.**",
101
- "",
102
- "To make paid API calls, set at least one payment key in your MCP server configuration:",
103
- "",
104
- "```json",
105
- '{',
106
- ' "mcpServers": {',
107
- ' "mpp32": {',
108
- ' "command": "npx",',
109
- ' "args": ["mpp32-mcp-server"],',
110
- ' "env": {',
111
- ' "MPP32_PRIVATE_KEY": "your-evm-private-key (for Tempo/pathUSD)",',
112
- ' "MPP32_SOLANA_PRIVATE_KEY": "your-solana-private-key (for x402/USDC)"',
113
- " }",
114
- " }",
115
- " }",
116
- "}",
117
- "```",
118
- "",
119
- "Provide either key — or both for maximum compatibility.",
120
- "- **MPP32_PRIVATE_KEY**: EVM key for a wallet funded with pathUSD on Tempo.",
121
- "- **MPP32_SOLANA_PRIVATE_KEY**: Solana key for a wallet funded with USDC.",
369
+ `**Not callable through HTTP.**`,
370
+ ``,
371
+ body?.error?.message ?? "This service is a stdio MCP server.",
372
+ body?.error?.installCommand ? `\nInstall: \`${body.error.installCommand}\`` : "",
373
+ body?.error?.hint ? `\nHint: ${body.error.hint}` : "",
122
374
  ].join("\n"),
123
375
  },
124
376
  ],
125
377
  };
126
378
  }
379
+ if (code === "AUTH_REQUIRED" || status === 401) {
380
+ return {
381
+ content: [
382
+ {
383
+ type: "text",
384
+ text: [
385
+ `**Agent session is missing or invalid.**`,
386
+ ``,
387
+ `Set \`MPP32_AGENT_KEY\` in your MCP config (the value of \`apiKey\` from POST /api/agent/sessions).`,
388
+ `Get one at ${API_URL}/agent-console.`,
389
+ body?.error?.message ? `\nServer said: ${body.error.message}` : "",
390
+ ].join("\n"),
391
+ },
392
+ ],
393
+ };
394
+ }
395
+ if (code === "SERVICE_NOT_FOUND") {
396
+ return {
397
+ content: [
398
+ {
399
+ type: "text",
400
+ text: `Service not found. Use \`list_mpp32_services\` to discover valid slugs.`,
401
+ },
402
+ ],
403
+ };
404
+ }
405
+ return {
406
+ content: [
407
+ {
408
+ type: "text",
409
+ text: `MPP32 returned HTTP ${status}: ${body?.error?.message ?? "unknown error"}`,
410
+ },
411
+ ],
412
+ };
413
+ }
414
+ function paymentKeyMissingMessage(resp, challenge) {
415
+ const offered = [
416
+ challenge.wwwAuthenticate ? "Tempo (pathUSD on Ethereum L2)" : null,
417
+ challenge.paymentRequired ? "x402 (USDC on Solana)" : null,
418
+ ]
419
+ .filter(Boolean)
420
+ .join(" or ");
421
+ const price = challenge.priceQuoted ?? resp.data.meta.priceQuoted;
422
+ return {
423
+ content: [
424
+ {
425
+ type: "text",
426
+ text: [
427
+ `**${resp.data.meta.service} requires payment** (~$${price}).`,
428
+ ``,
429
+ `The provider accepts: ${offered || "(unknown)"}.`,
430
+ ``,
431
+ `To enable automatic payment, add a private key to your MCP config:`,
432
+ ``,
433
+ "```json",
434
+ "{",
435
+ ' "mcpServers": {',
436
+ ' "mpp32": {',
437
+ ' "command": "npx",',
438
+ ' "args": ["mpp32-mcp-server"],',
439
+ ' "env": {',
440
+ AGENT_KEY ? ` "MPP32_AGENT_KEY": "${AGENT_KEY.slice(0, 12)}…",` : "",
441
+ ' "MPP32_SOLANA_PRIVATE_KEY": "<solana-base58-key for USDC>",',
442
+ ' "MPP32_PRIVATE_KEY": "<EVM-hex-key for pathUSD>"',
443
+ " }",
444
+ " }",
445
+ " }",
446
+ "}",
447
+ "```",
448
+ ``,
449
+ `Free services (DexScreener, Jupiter price, CoinGecko ping, httpbin) work without any private key.`,
450
+ ]
451
+ .filter((l) => l !== "")
452
+ .join("\n"),
453
+ },
454
+ ],
455
+ };
456
+ }
457
+ function paymentFailedMessage(challenge, proto, err) {
458
+ const msg = err instanceof Error ? err.message : String(err);
459
+ return {
460
+ content: [
461
+ {
462
+ type: "text",
463
+ text: [
464
+ `**Payment failed (${proto})** for ${challenge.serviceName} ($${challenge.priceQuoted}).`,
465
+ ``,
466
+ msg,
467
+ ``,
468
+ `Common causes: insufficient balance, malformed key, or expired challenge nonce.`,
469
+ ].join("\n"),
470
+ },
471
+ ],
472
+ };
473
+ }
474
+ // ── Legacy path (no MPP32_AGENT_KEY) ────────────────────────────────────────
475
+ async function callViaLegacyProxy(slug, method, body, query) {
127
476
  try {
128
- // First, get service info to confirm it exists and show the price
129
- const infoUrl = new URL(`/api/proxy/${encodeURIComponent(slug)}/info`, API_URL);
130
- const infoRes = await fetch(infoUrl.toString());
477
+ // Without an agent key, only native /api/proxy/<slug> is reachable.
478
+ // We fetch /info first to detect that the slug exists as a native service.
479
+ const infoUrl = new URL(`/api/proxy/${encodeURIComponent(slug)}/info`, API_URL).toString();
480
+ const infoRes = await fetch(infoUrl);
131
481
  if (!infoRes.ok) {
132
- if (infoRes.status === 404) {
133
- return {
134
- content: [
135
- {
136
- type: "text",
137
- text: `Service "${slug}" not found. Use list_mpp32_services to see available services.`,
138
- },
139
- ],
140
- };
141
- }
142
482
  return {
143
483
  content: [
144
484
  {
145
485
  type: "text",
146
- text: `Error checking service: HTTP ${infoRes.status}`,
486
+ text: [
487
+ `Service "${slug}" is not a native MPP32 service.`,
488
+ ``,
489
+ `Without \`MPP32_AGENT_KEY\` set, only native services are callable. To call federated catalog entries (free curated APIs, x402 Bazaar mirrors, etc.), add \`MPP32_AGENT_KEY\` to your MCP config — get one at ${API_URL}/agent-console.`,
490
+ ].join("\n"),
147
491
  },
148
492
  ],
149
493
  };
150
494
  }
151
495
  const info = (await infoRes.json());
152
- const price = info.data.pricePerQuery ?? 0.001;
153
- // Build the proxy request URL
154
496
  const proxyUrl = new URL(`/api/proxy/${encodeURIComponent(slug)}`, API_URL);
155
- if (query) {
156
- for (const [key, value] of Object.entries(query)) {
157
- proxyUrl.searchParams.set(key, value);
158
- }
159
- }
160
- // Step 1: Initial request to get the 402 payment challenge
161
- const headers = {
162
- Accept: "application/json",
163
- };
164
- if (body) {
165
- headers["Content-Type"] = "application/json";
166
- }
497
+ if (query)
498
+ for (const [k, v] of Object.entries(query))
499
+ proxyUrl.searchParams.set(k, v);
500
+ const baseHeaders = { Accept: "application/json" };
501
+ if (body !== undefined)
502
+ baseHeaders["Content-Type"] = "application/json";
167
503
  const challengeRes = await fetch(proxyUrl.toString(), {
168
504
  method,
169
- headers,
170
- body: method !== "GET" ? body : undefined,
505
+ headers: baseHeaders,
506
+ body: method !== "GET" && body !== undefined ? JSON.stringify(body) : undefined,
171
507
  });
172
- // If we get a non-402 response, the endpoint might not require payment
173
508
  if (challengeRes.status !== 402) {
174
- const responseText = await challengeRes.text();
509
+ const text = await challengeRes.text();
175
510
  let formatted;
176
511
  try {
177
- formatted = JSON.stringify(JSON.parse(responseText), null, 2);
512
+ formatted = JSON.stringify(JSON.parse(text), null, 2);
178
513
  }
179
514
  catch {
180
- formatted = responseText;
515
+ formatted = text;
181
516
  }
182
517
  return {
183
518
  content: [
184
519
  {
185
520
  type: "text",
186
- text: `**${info.data.name}** responded with HTTP ${challengeRes.status}:\n\n\`\`\`json\n${formatted}\n\`\`\``,
521
+ text: `**${info.data.name}** HTTP ${challengeRes.status}\n\n\`\`\`json\n${formatted}\n\`\`\``,
187
522
  },
188
523
  ],
189
524
  };
190
525
  }
191
- // Step 2: Parse the 402 challenge detect available protocols
192
- const wwwAuth = challengeRes.headers.get("www-authenticate");
193
- const paymentRequiredHeader = challengeRes.headers.get("payment-required");
194
- if (!wwwAuth && !paymentRequiredHeader) {
526
+ // Got 402 — sign with available keys
527
+ const wwwAuth = challengeRes.headers.get("www-authenticate") ?? undefined;
528
+ const paymentRequired = challengeRes.headers.get("payment-required") ?? undefined;
529
+ const challenge = {
530
+ wwwAuthenticate: wwwAuth,
531
+ paymentRequired,
532
+ rawHeaders: {},
533
+ priceQuoted: info.data.pricePerQuery ?? 0,
534
+ serviceName: info.data.name,
535
+ };
536
+ if (!PRIVATE_KEY && !SOLANA_PRIVATE_KEY) {
195
537
  return {
196
538
  content: [
197
539
  {
198
540
  type: "text",
199
- text: "Received 402 but no payment challenge headers. The payment protocol may have changed.",
541
+ text: [
542
+ `**${info.data.name}** requires payment ($${info.data.pricePerQuery}).`,
543
+ ``,
544
+ `Add a payment key to your MCP config (\`MPP32_SOLANA_PRIVATE_KEY\` for USDC or \`MPP32_PRIVATE_KEY\` for pathUSD), or set \`MPP32_AGENT_KEY\` to use the agent execute path.`,
545
+ ].join("\n"),
200
546
  },
201
547
  ],
202
548
  };
203
549
  }
204
- // Step 3: Choose protocol and complete payment
205
- let paymentAuthHeader;
206
- let paymentAuthKey;
207
- let usedProtocol;
208
- // Prefer x402 if Solana key is available and server supports it
209
- if (paymentRequiredHeader && SOLANA_PRIVATE_KEY) {
550
+ const paymentHeaders = {};
551
+ let usedProtocol = "";
552
+ if (paymentRequired && SOLANA_PRIVATE_KEY) {
210
553
  try {
211
- const x402Token = await completeX402Payment(paymentRequiredHeader, SOLANA_PRIVATE_KEY);
212
- paymentAuthHeader = x402Token;
213
- paymentAuthKey = "X-Payment";
554
+ paymentHeaders["X-Payment"] = await completeX402Payment(paymentRequired, SOLANA_PRIVATE_KEY);
214
555
  usedProtocol = "USDC (x402)";
215
556
  }
216
557
  catch (err) {
217
- // Fall back to Tempo if x402 fails and Tempo key is available
218
558
  if (wwwAuth && PRIVATE_KEY) {
219
- const challengeParams = parseWwwAuthenticate(wwwAuth);
220
- if (!challengeParams.scheme) {
221
- return { content: [{ type: "text", text: `x402 payment failed and could not parse Tempo challenge.` }] };
559
+ const parsed = parseWwwAuthenticate(wwwAuth);
560
+ try {
561
+ const token = await completeTempoPayment(parsed.params, PRIVATE_KEY);
562
+ paymentHeaders["Authorization"] = `Payment ${token}`;
563
+ usedProtocol = "pathUSD (Tempo)";
564
+ }
565
+ catch (te) {
566
+ return paymentFailedMessage(challenge, "x402+tempo", `${err}; ${te}`);
222
567
  }
223
- paymentAuthHeader = await completeTempoPayment(challengeParams.params, PRIVATE_KEY);
224
- paymentAuthKey = "Authorization";
225
- paymentAuthHeader = `Payment ${paymentAuthHeader}`;
226
- usedProtocol = "pathUSD (Tempo)";
227
568
  }
228
569
  else {
229
- return {
230
- content: [
231
- {
232
- type: "text",
233
- text: [
234
- `**x402 payment failed** for ${info.data.name} ($${price}):`,
235
- "",
236
- err instanceof Error ? err.message : String(err),
237
- "",
238
- "Ensure your Solana wallet has sufficient USDC balance.",
239
- ].join("\n"),
240
- },
241
- ],
242
- };
570
+ return paymentFailedMessage(challenge, "x402", err);
243
571
  }
244
572
  }
245
573
  }
246
574
  else if (wwwAuth && PRIVATE_KEY) {
247
- const challengeParams = parseWwwAuthenticate(wwwAuth);
248
- if (!challengeParams.scheme || !challengeParams.params) {
249
- return { content: [{ type: "text", text: `Could not parse payment challenge. WWW-Authenticate: ${wwwAuth}` }] };
250
- }
575
+ const parsed = parseWwwAuthenticate(wwwAuth);
251
576
  try {
252
- const token = await completeTempoPayment(challengeParams.params, PRIVATE_KEY);
253
- paymentAuthHeader = `Payment ${token}`;
254
- paymentAuthKey = "Authorization";
577
+ const token = await completeTempoPayment(parsed.params, PRIVATE_KEY);
578
+ paymentHeaders["Authorization"] = `Payment ${token}`;
255
579
  usedProtocol = "pathUSD (Tempo)";
256
580
  }
257
581
  catch (err) {
258
- return {
259
- content: [
260
- {
261
- type: "text",
262
- text: [
263
- `**Tempo payment failed** for ${info.data.name} ($${price}):`,
264
- "",
265
- err instanceof Error ? err.message : String(err),
266
- "",
267
- "Ensure your wallet has sufficient pathUSD balance on Tempo.",
268
- ].join("\n"),
269
- },
270
- ],
271
- };
582
+ return paymentFailedMessage(challenge, "tempo", err);
272
583
  }
273
584
  }
274
585
  else {
275
- const available = [wwwAuth ? "Tempo (pathUSD)" : null, paymentRequiredHeader ? "x402 (USDC)" : null].filter(Boolean).join(", ");
276
- const configured = [PRIVATE_KEY ? "Tempo" : null, SOLANA_PRIVATE_KEY ? "x402" : null].filter(Boolean).join(", ");
277
586
  return {
278
587
  content: [
279
588
  {
280
589
  type: "text",
281
- text: `No compatible payment method. Server offers: ${available}. You have keys for: ${configured || "none"}.`,
590
+ text: `No compatible payment method.`,
282
591
  },
283
592
  ],
284
593
  };
285
594
  }
286
- // Step 4: Retry with payment receipt
287
- const authedRes = await fetch(proxyUrl.toString(), {
595
+ const paidRes = await fetch(proxyUrl.toString(), {
288
596
  method,
289
- headers: {
290
- ...headers,
291
- [paymentAuthKey]: paymentAuthHeader,
292
- },
293
- body: method !== "GET" ? body : undefined,
597
+ headers: { ...baseHeaders, ...paymentHeaders },
598
+ body: method !== "GET" && body !== undefined ? JSON.stringify(body) : undefined,
294
599
  });
295
- const responseText = await authedRes.text();
600
+ const paidText = await paidRes.text();
296
601
  let formatted;
297
602
  try {
298
- formatted = JSON.stringify(JSON.parse(responseText), null, 2);
603
+ formatted = JSON.stringify(JSON.parse(paidText), null, 2);
299
604
  }
300
605
  catch {
301
- formatted = responseText;
606
+ formatted = paidText;
302
607
  }
303
608
  return {
304
609
  content: [
305
610
  {
306
611
  type: "text",
307
- text: [
308
- `**${info.data.name}** — HTTP ${authedRes.status} (paid $${price} via ${usedProtocol})`,
309
- "",
310
- "```json",
311
- formatted,
312
- "```",
313
- ].join("\n"),
612
+ text: `**${info.data.name}** — HTTP ${paidRes.status} (paid $${info.data.pricePerQuery} via ${usedProtocol})\n\n\`\`\`json\n${formatted}\n\`\`\``,
314
613
  },
315
614
  ],
316
615
  };
@@ -320,37 +619,24 @@ server.tool("call_mpp32_endpoint", "Call any machine-payable API in the MPP32 ma
320
619
  content: [
321
620
  {
322
621
  type: "text",
323
- text: `Request failed: ${err instanceof Error ? err.message : String(err)}`,
622
+ text: `Network error reaching ${API_URL}: ${err instanceof Error ? err.message : String(err)}`,
324
623
  },
325
624
  ],
326
625
  };
327
626
  }
328
- });
329
- // ── Tool 3: get_solana_token_intelligence ──
330
- server.tool("get_solana_token_intelligence", "Get real-time Solana token intelligence from the MPP32 marketplace. Returns alpha score (0-100), rug risk assessment with contributing factors, whale activity tracking with buy/sell breakdown, smart money signals, 24h pump probability, projected ROI ranges, and full market data aggregated from DexScreener, Jupiter, and CoinGecko. Accepts any token mint address or ticker symbol (SOL, BONK, JUP, etc.). $0.008 per query, paid automatically via x402 or Tempo. M32 token holders receive up to 40% discount.", {
331
- token: z
332
- .string()
333
- .describe("Solana token mint address or ticker symbol (e.g. SOL, BONK, JUP, or full base58 address)"),
334
- walletAddress: z
335
- .string()
336
- .optional()
337
- .describe("Optional Solana wallet address for M32 token holder discount verification"),
338
- }, async ({ token, walletAddress }) => {
627
+ }
628
+ async function legacyIntelligenceCall(token, walletAddress) {
339
629
  try {
340
- const headers = {
341
- "Content-Type": "application/json",
342
- };
343
- if (walletAddress) {
344
- headers["X-Wallet-Address"] = walletAddress;
345
- }
346
- // First request to get the 402 challenge
347
- const challengeRes = await fetch(`${API_URL}/api/intelligence`, {
630
+ const reqHeaders = { "Content-Type": "application/json" };
631
+ if (walletAddress)
632
+ reqHeaders["X-Wallet-Address"] = walletAddress;
633
+ const res = await fetch(`${API_URL}/api/intelligence`, {
348
634
  method: "POST",
349
- headers,
635
+ headers: reqHeaders,
350
636
  body: JSON.stringify({ token }),
351
637
  });
352
- if (challengeRes.status !== 402) {
353
- const text = await challengeRes.text();
638
+ if (res.status !== 402) {
639
+ const text = await res.text();
354
640
  let formatted;
355
641
  try {
356
642
  formatted = JSON.stringify(JSON.parse(text), null, 2);
@@ -358,87 +644,105 @@ server.tool("get_solana_token_intelligence", "Get real-time Solana token intelli
358
644
  catch {
359
645
  formatted = text;
360
646
  }
361
- if (challengeRes.ok) {
647
+ if (res.ok) {
362
648
  return {
363
649
  content: [
364
650
  {
365
651
  type: "text",
366
- text: `**Solana Token Intelligence** for \`${token}\`:\n\n\`\`\`json\n${formatted}\n\`\`\``,
652
+ text: `**Solana Token Intelligence** \`${token}\`\n\n\`\`\`json\n${formatted}\n\`\`\``,
367
653
  },
368
654
  ],
369
655
  };
370
656
  }
371
657
  return {
372
- content: [
373
- {
374
- type: "text",
375
- text: `Error: HTTP ${challengeRes.status}\n\n${formatted}`,
376
- },
377
- ],
658
+ content: [{ type: "text", text: `Error: HTTP ${res.status}\n\n${formatted}` }],
378
659
  };
379
660
  }
380
- // Handle payment
381
- const wwwAuth = challengeRes.headers.get("www-authenticate");
382
- const paymentRequired = challengeRes.headers.get("payment-required");
383
661
  if (!PRIVATE_KEY && !SOLANA_PRIVATE_KEY) {
384
662
  return {
385
663
  content: [
386
664
  {
387
665
  type: "text",
388
- text: "Payment required ($0.008 per query). Set MPP32_PRIVATE_KEY or MPP32_SOLANA_PRIVATE_KEY in your MCP config to enable automatic payments.",
666
+ text: [
667
+ "Intelligence Oracle requires payment ($0.008 per query).",
668
+ "",
669
+ "Set `MPP32_AGENT_KEY` (recommended — also gives dashboard tracking) and/or `MPP32_SOLANA_PRIVATE_KEY` / `MPP32_PRIVATE_KEY` in your MCP config.",
670
+ "",
671
+ `Create a session at ${API_URL}/agent-console.`,
672
+ ].join("\n"),
389
673
  },
390
674
  ],
391
675
  };
392
676
  }
393
- let paymentHeaders = {};
677
+ const wwwAuth = res.headers.get("www-authenticate") ?? undefined;
678
+ const paymentRequired = res.headers.get("payment-required") ?? undefined;
679
+ const paymentHeaders = {};
680
+ let usedProtocol = "";
394
681
  if (paymentRequired && SOLANA_PRIVATE_KEY) {
395
682
  try {
396
- const x402Token = await completeX402Payment(paymentRequired, SOLANA_PRIVATE_KEY);
397
- paymentHeaders["X-Payment"] = x402Token;
683
+ paymentHeaders["X-Payment"] = await completeX402Payment(paymentRequired, SOLANA_PRIVATE_KEY);
684
+ usedProtocol = "USDC (x402)";
398
685
  }
399
- catch {
686
+ catch (x402Err) {
400
687
  if (wwwAuth && PRIVATE_KEY) {
401
- const challenge = parseWwwAuthenticate(wwwAuth);
402
- const tempoToken = await completeTempoPayment(challenge.params, PRIVATE_KEY);
403
- paymentHeaders["Authorization"] = `Payment ${tempoToken}`;
688
+ try {
689
+ const parsed = parseWwwAuthenticate(wwwAuth);
690
+ const tempoToken = await completeTempoPayment(parsed.params, PRIVATE_KEY);
691
+ paymentHeaders["Authorization"] = `Payment ${tempoToken}`;
692
+ usedProtocol = "pathUSD (Tempo)";
693
+ }
694
+ catch (tempoErr) {
695
+ return {
696
+ content: [
697
+ { type: "text", text: `Payment failed (x402: ${x402Err instanceof Error ? x402Err.message : String(x402Err)}; tempo: ${tempoErr instanceof Error ? tempoErr.message : String(tempoErr)}). Check wallet balance and key format.` },
698
+ ],
699
+ };
700
+ }
404
701
  }
405
702
  else {
406
703
  return {
407
- content: [{ type: "text", text: "Payment failed. Check your wallet balance." }],
704
+ content: [
705
+ { type: "text", text: `x402 payment failed: ${x402Err instanceof Error ? x402Err.message : String(x402Err)}. Check Solana wallet balance.` },
706
+ ],
408
707
  };
409
708
  }
410
709
  }
411
710
  }
412
711
  else if (wwwAuth && PRIVATE_KEY) {
413
- const challenge = parseWwwAuthenticate(wwwAuth);
414
- const tempoToken = await completeTempoPayment(challenge.params, PRIVATE_KEY);
415
- paymentHeaders["Authorization"] = `Payment ${tempoToken}`;
416
- }
417
- else {
418
- return {
419
- content: [{ type: "text", text: "No compatible payment key configured for available protocols." }],
420
- };
712
+ try {
713
+ const parsed = parseWwwAuthenticate(wwwAuth);
714
+ const tempoToken = await completeTempoPayment(parsed.params, PRIVATE_KEY);
715
+ paymentHeaders["Authorization"] = `Payment ${tempoToken}`;
716
+ usedProtocol = "pathUSD (Tempo)";
717
+ }
718
+ catch (tempoErr) {
719
+ return {
720
+ content: [
721
+ { type: "text", text: `Tempo payment failed: ${tempoErr instanceof Error ? tempoErr.message : String(tempoErr)}` },
722
+ ],
723
+ };
724
+ }
421
725
  }
422
726
  const paidRes = await fetch(`${API_URL}/api/intelligence`, {
423
727
  method: "POST",
424
- headers: { ...headers, ...paymentHeaders },
728
+ headers: { ...reqHeaders, ...paymentHeaders },
425
729
  body: JSON.stringify({ token }),
426
730
  });
427
- const text = await paidRes.text();
731
+ const paidText = await paidRes.text();
428
732
  let formatted;
429
733
  try {
430
- formatted = JSON.stringify(JSON.parse(text), null, 2);
734
+ formatted = JSON.stringify(JSON.parse(paidText), null, 2);
431
735
  }
432
736
  catch {
433
- formatted = text;
737
+ formatted = paidText;
434
738
  }
435
739
  const discount = paidRes.headers.get("X-M32-Discount");
436
- const discountNote = discount && discount !== "0" ? ` (${discount}% M32 holder discount applied)` : "";
740
+ const discountNote = discount && discount !== "0" ? ` (${discount}% M32 discount)` : "";
437
741
  return {
438
742
  content: [
439
743
  {
440
744
  type: "text",
441
- text: `**Solana Token Intelligence** for \`${token}\`${discountNote}:\n\n\`\`\`json\n${formatted}\n\`\`\``,
745
+ text: `**Solana Token Intelligence** \`${token}\` via ${usedProtocol}${discountNote}\n\n\`\`\`json\n${formatted}\n\`\`\``,
442
746
  },
443
747
  ],
444
748
  };
@@ -448,12 +752,12 @@ server.tool("get_solana_token_intelligence", "Get real-time Solana token intelli
448
752
  content: [
449
753
  {
450
754
  type: "text",
451
- text: `Failed to get token intelligence: ${err instanceof Error ? err.message : String(err)}`,
755
+ text: `Network error reaching ${API_URL}: ${err instanceof Error ? err.message : String(err)}`,
452
756
  },
453
757
  ],
454
758
  };
455
759
  }
456
- });
760
+ }
457
761
  function parseWwwAuthenticate(header) {
458
762
  const match = header.match(/^(\w+)\s+(.+)$/);
459
763
  if (!match)
@@ -469,11 +773,6 @@ function parseWwwAuthenticate(header) {
469
773
  return { scheme, params };
470
774
  }
471
775
  async function completeTempoPayment(challengeParams, privateKey) {
472
- // The Tempo payment flow:
473
- // 1. Parse amount, currency, recipient, nonce from challenge
474
- // 2. Sign a payment authorization with the private key
475
- // 3. Return the signed token for the Authorization header
476
- // Dynamic import — mppx and viem are optional peer dependencies
477
776
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
478
777
  let mppxClient;
479
778
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -485,18 +784,17 @@ async function completeTempoPayment(challengeParams, privateKey) {
485
784
  viemAccounts = await import(viemPkg);
486
785
  }
487
786
  catch {
488
- throw new Error("Payment client not available. Install mppx and viem as peer dependencies:\n npm install mppx viem");
787
+ throw new Error("Tempo payment client not available. Install: npm install mppx viem");
489
788
  }
490
789
  try {
491
790
  const account = viemAccounts.privateKeyToAccount(privateKey.startsWith("0x") ? privateKey : `0x${privateKey}`);
492
791
  const client = mppxClient.Mppx.create({
493
792
  methods: [mppxClient.tempo({ account })],
494
793
  });
495
- const token = await client.pay(challengeParams);
496
- return token;
794
+ return (await client.pay(challengeParams));
497
795
  }
498
796
  catch (payErr) {
499
- throw new Error(`Payment failed: ${payErr instanceof Error ? payErr.message : String(payErr)}`);
797
+ throw new Error(`Tempo payment failed: ${payErr instanceof Error ? payErr.message : String(payErr)}`);
500
798
  }
501
799
  }
502
800
  async function completeX402Payment(paymentRequiredHeader, solanaPrivateKey) {
@@ -507,22 +805,22 @@ async function completeX402Payment(paymentRequiredHeader, solanaPrivateKey) {
507
805
  catch {
508
806
  throw new Error("Could not decode Payment-Required header");
509
807
  }
510
- // Build x402 payment payload with the Solana key
511
- // Dynamic import for optional dependency
808
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
512
809
  let solanaWeb3;
513
810
  try {
514
811
  const pkg = "@solana/web3.js";
515
812
  solanaWeb3 = await import(pkg);
516
813
  }
517
814
  catch {
518
- throw new Error("x402 payment requires @solana/web3.js:\n npm install @solana/web3.js");
815
+ throw new Error("x402 payment requires @solana/web3.js: npm install @solana/web3.js");
519
816
  }
817
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
520
818
  let keypair;
521
819
  try {
522
820
  if (solanaPrivateKey.startsWith("[")) {
523
821
  keypair = solanaWeb3.Keypair.fromSecretKey(new Uint8Array(JSON.parse(solanaPrivateKey)));
524
822
  }
525
- else if (/^[0-9a-fA-F]+$/.test(solanaPrivateKey)) {
823
+ else if (/^[0-9a-fA-F]+$/.test(solanaPrivateKey) && solanaPrivateKey.length % 2 === 0) {
526
824
  keypair = solanaWeb3.Keypair.fromSecretKey(new Uint8Array(Buffer.from(solanaPrivateKey, "hex")));
527
825
  }
528
826
  else {
@@ -549,24 +847,26 @@ async function completeX402Payment(paymentRequiredHeader, solanaPrivateKey) {
549
847
  };
550
848
  const message = JSON.stringify(payload.payload);
551
849
  const messageBytes = new TextEncoder().encode(message);
850
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
552
851
  let tweetnacl;
553
852
  try {
554
853
  const pkg = "tweetnacl";
555
854
  tweetnacl = await import(pkg);
556
855
  }
557
856
  catch {
558
- throw new Error("x402 signing requires tweetnacl:\n npm install tweetnacl");
857
+ throw new Error("x402 signing requires tweetnacl: npm install tweetnacl");
559
858
  }
560
859
  const naclSign = tweetnacl.default?.sign ?? tweetnacl.sign;
561
860
  const signed = naclSign.detached(messageBytes, keypair.secretKey);
562
861
  payload.payload.signature = Buffer.from(signed).toString("base64");
563
862
  return Buffer.from(JSON.stringify(payload)).toString("base64");
564
863
  }
565
- // ── Start ──
864
+ // ── Start ───────────────────────────────────────────────────────────────────
566
865
  async function main() {
567
866
  const transport = new StdioServerTransport();
568
867
  await server.connect(transport);
569
- console.error("MPP32 MCP server running on stdio");
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}`);
570
870
  }
571
871
  main().catch((err) => {
572
872
  console.error("Fatal:", err);