mpp32-mcp-server 1.0.2 → 1.1.0
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/README.md +84 -164
- package/dist/index.js +544 -244
- package/package.json +2 -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.
|
|
13
|
+
version: "1.1.0",
|
|
11
14
|
});
|
|
12
|
-
|
|
13
|
-
|
|
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').
|
|
18
|
-
|
|
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/
|
|
21
|
-
|
|
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", "Browse the MPP32 ecosystem of machine-payabl
|
|
|
30
71
|
};
|
|
31
72
|
}
|
|
32
73
|
const json = (await res.json());
|
|
33
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
`- **
|
|
51
|
-
`- **
|
|
52
|
-
`- **
|
|
53
|
-
`- **
|
|
54
|
-
`- **
|
|
55
|
-
s.
|
|
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
|
|
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", "Browse the MPP32 ecosystem of machine-payabl
|
|
|
73
132
|
};
|
|
74
133
|
}
|
|
75
134
|
});
|
|
76
|
-
// ── Tool 2: call_mpp32_endpoint
|
|
77
|
-
server.tool("call_mpp32_endpoint", "Call
|
|
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("
|
|
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("
|
|
84
|
-
.describe("HTTP method
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
"",
|
|
104
|
-
"
|
|
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
|
-
//
|
|
129
|
-
|
|
130
|
-
const
|
|
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:
|
|
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 [
|
|
157
|
-
proxyUrl.searchParams.set(
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
|
509
|
+
const text = await challengeRes.text();
|
|
175
510
|
let formatted;
|
|
176
511
|
try {
|
|
177
|
-
formatted = JSON.stringify(JSON.parse(
|
|
512
|
+
formatted = JSON.stringify(JSON.parse(text), null, 2);
|
|
178
513
|
}
|
|
179
514
|
catch {
|
|
180
|
-
formatted =
|
|
515
|
+
formatted = text;
|
|
181
516
|
}
|
|
182
517
|
return {
|
|
183
518
|
content: [
|
|
184
519
|
{
|
|
185
520
|
type: "text",
|
|
186
|
-
text: `**${info.data.name}**
|
|
521
|
+
text: `**${info.data.name}** — HTTP ${challengeRes.status}\n\n\`\`\`json\n${formatted}\n\`\`\``,
|
|
187
522
|
},
|
|
188
523
|
],
|
|
189
524
|
};
|
|
190
525
|
}
|
|
191
|
-
//
|
|
192
|
-
const wwwAuth = challengeRes.headers.get("www-authenticate");
|
|
193
|
-
const
|
|
194
|
-
|
|
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:
|
|
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
|
-
|
|
205
|
-
let
|
|
206
|
-
|
|
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
|
-
|
|
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
|
|
220
|
-
|
|
221
|
-
|
|
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
|
|
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(
|
|
253
|
-
|
|
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
|
|
590
|
+
text: `No compatible payment method.`,
|
|
282
591
|
},
|
|
283
592
|
],
|
|
284
593
|
};
|
|
285
594
|
}
|
|
286
|
-
|
|
287
|
-
const authedRes = await fetch(proxyUrl.toString(), {
|
|
595
|
+
const paidRes = await fetch(proxyUrl.toString(), {
|
|
288
596
|
method,
|
|
289
|
-
headers: {
|
|
290
|
-
|
|
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
|
|
600
|
+
const paidText = await paidRes.text();
|
|
296
601
|
let formatted;
|
|
297
602
|
try {
|
|
298
|
-
formatted = JSON.stringify(JSON.parse(
|
|
603
|
+
formatted = JSON.stringify(JSON.parse(paidText), null, 2);
|
|
299
604
|
}
|
|
300
605
|
catch {
|
|
301
|
-
formatted =
|
|
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 a machine-payable API endpoint on MPP32
|
|
|
320
619
|
content: [
|
|
321
620
|
{
|
|
322
621
|
type: "text",
|
|
323
|
-
text: `
|
|
622
|
+
text: `Network error reaching ${API_URL}: ${err instanceof Error ? err.message : String(err)}`,
|
|
324
623
|
},
|
|
325
624
|
],
|
|
326
625
|
};
|
|
327
626
|
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
server.tool("get_solana_token_intelligence", "Get comprehensive Solana token intelligence including alpha score (0-100), rug risk assessment, whale activity tracking, smart money signals, 24h pump probability, projected ROI ranges, and real-time market data from DexScreener, Jupiter, and CoinGecko. Accepts any token address or ticker symbol. 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
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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 (
|
|
353
|
-
const text = await
|
|
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 comprehensive Solana token int
|
|
|
358
644
|
catch {
|
|
359
645
|
formatted = text;
|
|
360
646
|
}
|
|
361
|
-
if (
|
|
647
|
+
if (res.ok) {
|
|
362
648
|
return {
|
|
363
649
|
content: [
|
|
364
650
|
{
|
|
365
651
|
type: "text",
|
|
366
|
-
text: `**Solana Token Intelligence**
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
397
|
-
|
|
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
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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: [
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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: { ...
|
|
728
|
+
headers: { ...reqHeaders, ...paymentHeaders },
|
|
425
729
|
body: JSON.stringify({ token }),
|
|
426
730
|
});
|
|
427
|
-
const
|
|
731
|
+
const paidText = await paidRes.text();
|
|
428
732
|
let formatted;
|
|
429
733
|
try {
|
|
430
|
-
formatted = JSON.stringify(JSON.parse(
|
|
734
|
+
formatted = JSON.stringify(JSON.parse(paidText), null, 2);
|
|
431
735
|
}
|
|
432
736
|
catch {
|
|
433
|
-
formatted =
|
|
737
|
+
formatted = paidText;
|
|
434
738
|
}
|
|
435
739
|
const discount = paidRes.headers.get("X-M32-Discount");
|
|
436
|
-
const discountNote = discount && discount !== "0" ? ` (${discount}% M32
|
|
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**
|
|
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 comprehensive Solana token int
|
|
|
448
752
|
content: [
|
|
449
753
|
{
|
|
450
754
|
type: "text",
|
|
451
|
-
text: `
|
|
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("
|
|
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
|
-
|
|
496
|
-
return token;
|
|
794
|
+
return (await client.pay(challengeParams));
|
|
497
795
|
}
|
|
498
796
|
catch (payErr) {
|
|
499
|
-
throw new Error(`
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
-
|
|
868
|
+
const keyHint = AGENT_KEY ? "agent-key configured" : "no agent-key (legacy mode)";
|
|
869
|
+
console.error(`MPP32 MCP server v1.1.0 running on stdio — ${keyHint}`);
|
|
570
870
|
}
|
|
571
871
|
main().catch((err) => {
|
|
572
872
|
console.error("Fatal:", err);
|