mpp32-mcp-server 1.1.3 → 1.2.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/CHANGELOG.md +71 -0
- package/dist/index.js +125 -101
- package/dist/x402-signers.d.ts +42 -0
- package/dist/x402-signers.js +267 -0
- package/package.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,77 @@ All notable changes to `mpp32-mcp-server` are documented here. The format
|
|
|
4
4
|
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the
|
|
5
5
|
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [1.2.0] - 2026-05-11
|
|
8
|
+
|
|
9
|
+
This release makes x402 payments actually work end-to-end. Prior versions
|
|
10
|
+
shipped a non-spec-compliant signing path that the official x402.org
|
|
11
|
+
facilitator rejected with HTTP 400 on every paid call, so no settlement ever
|
|
12
|
+
occurred. The signing path has been rewritten from scratch against the
|
|
13
|
+
[Coinbase x402 reference implementation](https://github.com/coinbase/x402).
|
|
14
|
+
|
|
15
|
+
### Fixed (the headline)
|
|
16
|
+
|
|
17
|
+
* **Real Solana x402 payments.** When a server returns a `Payment-Required`
|
|
18
|
+
challenge on a `solana:*` network, the MCP client now builds a real Solana
|
|
19
|
+
`VersionedTransaction` with the three instructions the `exact` SVM scheme
|
|
20
|
+
requires — `SetComputeUnitLimit`, `SetComputeUnitPrice`, and SPL-Token
|
|
21
|
+
`TransferChecked` between the payer's and recipient's Associated Token
|
|
22
|
+
Accounts. The transaction is partially signed by the payer (the fee-payer
|
|
23
|
+
slot is left empty for the facilitator to fill at `/settle` time, per spec)
|
|
24
|
+
and base64-encoded into the `payload.transaction` field. The official
|
|
25
|
+
`x402.org/facilitator` now accepts and settles these payments.
|
|
26
|
+
|
|
27
|
+
* **Real EVM x402 payments on Base.** For challenges with `network: "base"`
|
|
28
|
+
or `network: "base-sepolia"` (and the `eip155:*` aliases), the client now
|
|
29
|
+
signs an EIP-3009 `transferWithAuthorization` typed-data message using
|
|
30
|
+
`viem` and the EVM private key in `MPP32_PRIVATE_KEY`. This unblocks the
|
|
31
|
+
~85% of the federated catalog (~3,900 of 4,581 entries) that lives on
|
|
32
|
+
Base — Exa Search, Firecrawl, OpenAI's x402 gateway, Anthropic's,
|
|
33
|
+
Alchemy RPC, CoinGecko Pro, Nansen, Cloudflare Workers AI, and the rest.
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
* **`path` argument to `call_mpp32_endpoint`.** Many curated catalog
|
|
38
|
+
entries store only an upstream base URL (e.g. `https://api.exa.ai`). Pass
|
|
39
|
+
the upstream path (e.g. `/search`) via the new `path` parameter to hit a
|
|
40
|
+
real endpoint instead of `POST /` (which returned 404). The agent server
|
|
41
|
+
forwards the path and appends it safely to the catalog base URL.
|
|
42
|
+
|
|
43
|
+
* **`MPP32_SOLANA_RPC_URL` env var.** Override the Solana RPC used to fetch
|
|
44
|
+
recent blockhashes when building x402 transactions. Defaults to
|
|
45
|
+
`https://api.mainnet-beta.solana.com`. Set this if you hit public-endpoint
|
|
46
|
+
rate limits.
|
|
47
|
+
|
|
48
|
+
* **`@solana/kit`, `@solana-program/token`, `@solana-program/compute-budget`,
|
|
49
|
+
`viem` as real dependencies.** Tree-shakeable, no `rpc-websockets`
|
|
50
|
+
transitive dependency, and no ESM/CJS landmines on Node 20+. `viem` was
|
|
51
|
+
previously an optional peer; it is now required because the EVM x402
|
|
52
|
+
signer cannot work without it.
|
|
53
|
+
|
|
54
|
+
### Migration
|
|
55
|
+
|
|
56
|
+
* No config changes required if you only use the Solana intelligence oracle
|
|
57
|
+
(it still uses `MPP32_SOLANA_PRIVATE_KEY`). To pay for Base-network
|
|
58
|
+
services like Exa Search, set `MPP32_PRIVATE_KEY` to your EVM private key
|
|
59
|
+
and ensure that wallet holds USDC on Base.
|
|
60
|
+
|
|
61
|
+
## [1.1.4] - 2026-05-11
|
|
62
|
+
|
|
63
|
+
### Added
|
|
64
|
+
|
|
65
|
+
* **`get_mpp32_diagnostics` MCP tool.** Reports server version, API URL,
|
|
66
|
+
and per-variable detection state for `MPP32_AGENT_KEY`,
|
|
67
|
+
`MPP32_SOLANA_PRIVATE_KEY`, and `MPP32_PRIVATE_KEY` — without ever
|
|
68
|
+
echoing the secret. The single most common payment failure has been
|
|
69
|
+
"I set the key but the MCP server doesn't see it" (wrong
|
|
70
|
+
`claude_desktop_config.json` file, `env` block at the wrong level in
|
|
71
|
+
the JSON, typo, or stale process from an incomplete restart). This
|
|
72
|
+
tool turns that into a one-call diagnosis.
|
|
73
|
+
* **Per-variable startup banner lines.** The stderr banner now prints
|
|
74
|
+
one `[mpp32] MPP32_X: SET (fingerprint) / NOT SET` line per managed
|
|
75
|
+
env var, so users who can find their MCP log file can see the same
|
|
76
|
+
diagnosis without calling a tool.
|
|
77
|
+
|
|
7
78
|
## [1.1.3] - 2026-05-11
|
|
8
79
|
|
|
9
80
|
### Fixed
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
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.0";
|
|
6
7
|
// ── Env loading: trim and sanitize aggressively ─────────────────────────────
|
|
7
8
|
// Copy-paste from Claude Desktop / Cursor / Windsurf JSON config UIs frequently
|
|
8
9
|
// adds trailing \n, \r, NBSP, BOM, or wraps the value in literal quotes. Any
|
|
@@ -173,6 +174,63 @@ function isHttpCallable(svc) {
|
|
|
173
174
|
return false;
|
|
174
175
|
return /^https?:\/\//.test(url);
|
|
175
176
|
}
|
|
177
|
+
// ── Tool 0: get_mpp32_diagnostics ───────────────────────────────────────────
|
|
178
|
+
// Lets the user (and Claude) see exactly what the MCP process detected at
|
|
179
|
+
// startup. The single most common failure mode is "I set the env var but it
|
|
180
|
+
// didn't reach the server" — wrong claude_desktop_config.json file edited,
|
|
181
|
+
// `env` block at the wrong level, typo in the variable name, stale process
|
|
182
|
+
// from an incomplete restart. This tool answers all of those without
|
|
183
|
+
// asking the user to dig through MCP log files.
|
|
184
|
+
function describeEnvVarStatus(name, value) {
|
|
185
|
+
const raw = process.env[name];
|
|
186
|
+
if (raw === undefined)
|
|
187
|
+
return `${name}: NOT SET (variable absent from MCP process env)`;
|
|
188
|
+
if (raw.length === 0)
|
|
189
|
+
return `${name}: EMPTY (set but blank)`;
|
|
190
|
+
if (value === undefined) {
|
|
191
|
+
return `${name}: REJECTED (raw length ${raw.length}, but failed validation — check startup log for reason)`;
|
|
192
|
+
}
|
|
193
|
+
// Show a short, non-secret fingerprint so the user can confirm it's the
|
|
194
|
+
// right value without us exfiltrating the key.
|
|
195
|
+
const fingerprint = value.length <= 12
|
|
196
|
+
? `${value.length} chars`
|
|
197
|
+
: `${value.slice(0, 6)}…${value.slice(-4)} (${value.length} chars)`;
|
|
198
|
+
return `${name}: SET (${fingerprint})`;
|
|
199
|
+
}
|
|
200
|
+
server.tool("get_mpp32_diagnostics", "Report what the mpp32-mcp-server detected at startup: version, API URL, and which env vars (MPP32_AGENT_KEY, MPP32_SOLANA_PRIVATE_KEY, MPP32_PRIVATE_KEY) were loaded into this process. Use this FIRST if payments fail with 'no key configured' even though you set one in claude_desktop_config.json — it confirms whether your env vars actually reached the MCP process or got dropped by a typo / wrong file / stale restart.", {}, async () => {
|
|
201
|
+
const lines = [
|
|
202
|
+
`**mpp32-mcp-server diagnostics**`,
|
|
203
|
+
``,
|
|
204
|
+
`Version: ${SERVER_VERSION}`,
|
|
205
|
+
`API URL: ${API_URL}`,
|
|
206
|
+
`Timeout: ${TIMEOUT_MS}ms`,
|
|
207
|
+
`Node: ${process.version} on ${process.platform}/${process.arch}`,
|
|
208
|
+
``,
|
|
209
|
+
`**Environment variable detection** (values are fingerprinted, never returned in full):`,
|
|
210
|
+
``,
|
|
211
|
+
describeEnvVarStatus("MPP32_AGENT_KEY", AGENT_KEY),
|
|
212
|
+
describeEnvVarStatus("MPP32_SOLANA_PRIVATE_KEY", SOLANA_PRIVATE_KEY),
|
|
213
|
+
describeEnvVarStatus("MPP32_PRIVATE_KEY", PRIVATE_KEY),
|
|
214
|
+
``,
|
|
215
|
+
`**Capabilities:**`,
|
|
216
|
+
`- Catalog browsing: yes (always available)`,
|
|
217
|
+
`- Federated service execution: ${AGENT_KEY ? "yes" : "no — set MPP32_AGENT_KEY"}`,
|
|
218
|
+
`- x402 (USDC on Solana) payment: ${SOLANA_PRIVATE_KEY ? "yes" : "no — set MPP32_SOLANA_PRIVATE_KEY"}`,
|
|
219
|
+
`- Tempo (pathUSD on Eth L2) payment: ${PRIVATE_KEY ? "yes" : "no — set MPP32_PRIVATE_KEY"}`,
|
|
220
|
+
``,
|
|
221
|
+
`If a variable shows NOT SET but you put it in claude_desktop_config.json:`,
|
|
222
|
+
`1. Confirm the file path Claude Desktop actually reads from:`,
|
|
223
|
+
` - macOS: ~/Library/Application Support/Claude/claude_desktop_config.json`,
|
|
224
|
+
` - Windows: %APPDATA%\\Claude\\claude_desktop_config.json`,
|
|
225
|
+
`2. The 'env' block must sit INSIDE the server entry, alongside 'command' and 'args' — not at the top level.`,
|
|
226
|
+
`3. Fully quit Claude Desktop (Cmd+Q on Mac, right-click tray → Quit on Windows). Closing the window is not enough.`,
|
|
227
|
+
`4. Re-open Claude Desktop. The new MCP process inherits the env from the JSON.`,
|
|
228
|
+
`5. Call get_mpp32_diagnostics again — if it still shows NOT SET, the JSON did not load (check for a syntax error).`,
|
|
229
|
+
];
|
|
230
|
+
return {
|
|
231
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
232
|
+
};
|
|
233
|
+
});
|
|
176
234
|
// ── Tool 1: list_mpp32_services ─────────────────────────────────────────────
|
|
177
235
|
server.tool("list_mpp32_services", "Browse the MPP32 federated catalog of 4,500+ machine-payable APIs and data services. Includes native MPP32 services (callable end-to-end through this MCP), the x402 Bazaar (USDC on Solana), curated free APIs (DexScreener, Jupiter, CoinGecko health, httpbin, etc.), and the public MCP Registry (npx-installable servers; listing-only). Each result indicates whether it is callable through `call_mpp32_endpoint` or listing-only. The catalog is large (~4,500 entries) — by default a single call returns up to 100 results and the response will tell you the true total and whether the page was truncated. Use `q` (free-text search), `category`, `source`, or `protocol` to narrow down, or raise `limit` (max 500) for broader pages.", {
|
|
178
236
|
category: z
|
|
@@ -285,14 +343,18 @@ server.tool("list_mpp32_services", "Browse the MPP32 federated catalog of 4,500+
|
|
|
285
343
|
}
|
|
286
344
|
});
|
|
287
345
|
// ── Tool 2: call_mpp32_endpoint ─────────────────────────────────────────────
|
|
288
|
-
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
|
|
346
|
+
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-on-Solana, MPP32_PRIVATE_KEY for x402-on-Base/Ethereum and Tempo pathUSD) is configured. Set MPP32_AGENT_KEY for dashboard tracking. Use `list_mpp32_services` first to find a slug. Many catalog entries store only the upstream BASE URL (e.g. `https://api.exa.ai`) — pass the upstream path (e.g. `/search`) via the `path` argument when calling those. Listing-only entries (npx-installable MCP servers, etc.) cannot be called through this tool.", {
|
|
289
347
|
slug: z
|
|
290
348
|
.string()
|
|
291
|
-
.describe("Service slug from `list_mpp32_services` (e.g. 'mpp32-intelligence')."),
|
|
349
|
+
.describe("Service slug from `list_mpp32_services` (e.g. 'curated:exa', 'mpp32-intelligence')."),
|
|
292
350
|
method: z
|
|
293
351
|
.enum(["GET", "POST", "PUT", "DELETE"])
|
|
294
352
|
.default("POST")
|
|
295
353
|
.describe("HTTP method."),
|
|
354
|
+
path: z
|
|
355
|
+
.string()
|
|
356
|
+
.optional()
|
|
357
|
+
.describe("Upstream path appended to the service's base URL (e.g. '/search' for Exa, '/v1/chat/completions' for OpenAI). Leave empty for catalog entries that already store a full path, or for native MPP32 services. Always begins with '/'."),
|
|
296
358
|
body: z
|
|
297
359
|
.union([z.string(), z.record(z.unknown())])
|
|
298
360
|
.optional()
|
|
@@ -301,7 +363,7 @@ server.tool("call_mpp32_endpoint", "Call any HTTP-callable service in the MPP32
|
|
|
301
363
|
.record(z.string())
|
|
302
364
|
.optional()
|
|
303
365
|
.describe("URL query parameters as key-value pairs."),
|
|
304
|
-
}, async ({ slug, method, body, query }) => {
|
|
366
|
+
}, async ({ slug, method, path, body, query }) => {
|
|
305
367
|
// Normalize body to an object so it can be JSON.stringified by the upstream call
|
|
306
368
|
let parsedBody = body;
|
|
307
369
|
if (typeof body === "string") {
|
|
@@ -313,10 +375,10 @@ server.tool("call_mpp32_endpoint", "Call any HTTP-callable service in the MPP32
|
|
|
313
375
|
}
|
|
314
376
|
}
|
|
315
377
|
if (AGENT_KEY) {
|
|
316
|
-
return await callViaAgentExecute(slug, method, parsedBody, query);
|
|
378
|
+
return await callViaAgentExecute(slug, method, parsedBody, query, path);
|
|
317
379
|
}
|
|
318
380
|
// Legacy path — only works for native services with payment keys
|
|
319
|
-
return await callViaLegacyProxy(slug, method, parsedBody, query);
|
|
381
|
+
return await callViaLegacyProxy(slug, method, parsedBody, query, path);
|
|
320
382
|
});
|
|
321
383
|
// ── Tool 3: get_solana_token_intelligence ───────────────────────────────────
|
|
322
384
|
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.", {
|
|
@@ -336,7 +398,7 @@ server.tool("get_solana_token_intelligence", "Get real-time Solana token intelli
|
|
|
336
398
|
return await legacyIntelligenceCall(token, walletAddress);
|
|
337
399
|
});
|
|
338
400
|
// ── Core: agent/execute path with 402 sign-and-retry ────────────────────────
|
|
339
|
-
async function callViaAgentExecute(service, method, body, query) {
|
|
401
|
+
async function callViaAgentExecute(service, method, body, query, path) {
|
|
340
402
|
try {
|
|
341
403
|
const execUrl = new URL("/api/agent/execute", API_URL).toString();
|
|
342
404
|
const reqBody = JSON.stringify({
|
|
@@ -344,6 +406,7 @@ async function callViaAgentExecute(service, method, body, query) {
|
|
|
344
406
|
method,
|
|
345
407
|
...(body !== undefined ? { body } : {}),
|
|
346
408
|
...(query ? { query } : {}),
|
|
409
|
+
...(path ? { path } : {}),
|
|
347
410
|
});
|
|
348
411
|
// Round 1: no payment headers
|
|
349
412
|
const firstRes = await fetchWithTimeout(execUrl, {
|
|
@@ -395,11 +458,18 @@ function detectPaymentRequired(resp) {
|
|
|
395
458
|
async function signAndRetry(execUrl, reqBody, challenge) {
|
|
396
459
|
const paymentHeaders = {};
|
|
397
460
|
let usedProtocol = "";
|
|
398
|
-
// Prefer x402 if
|
|
399
|
-
|
|
461
|
+
// Prefer x402 if a payment-required challenge is present AND we hold a key
|
|
462
|
+
// for *either* the SVM or EVM side. The signer module inspects the
|
|
463
|
+
// challenge's `network` field and routes to the right signer; we just need
|
|
464
|
+
// to pass it whichever keys we have.
|
|
465
|
+
if (challenge.paymentRequired && (SOLANA_PRIVATE_KEY || PRIVATE_KEY)) {
|
|
400
466
|
try {
|
|
401
|
-
|
|
402
|
-
|
|
467
|
+
const completed = await completeX402Payment(challenge.paymentRequired, {
|
|
468
|
+
solana: SOLANA_PRIVATE_KEY,
|
|
469
|
+
evm: PRIVATE_KEY,
|
|
470
|
+
});
|
|
471
|
+
paymentHeaders["X-Payment"] = completed.xPaymentHeader;
|
|
472
|
+
usedProtocol = completed.protocolUsed === "x402-evm" ? "USDC (x402, Base)" : "USDC (x402, Solana)";
|
|
403
473
|
}
|
|
404
474
|
catch (err) {
|
|
405
475
|
// Fall through to Tempo if available
|
|
@@ -626,9 +696,12 @@ function paymentFailedMessage(challenge, proto, err) {
|
|
|
626
696
|
};
|
|
627
697
|
}
|
|
628
698
|
// ── Legacy path (no MPP32_AGENT_KEY) ────────────────────────────────────────
|
|
629
|
-
async function callViaLegacyProxy(slug, method, body, query) {
|
|
699
|
+
async function callViaLegacyProxy(slug, method, body, query, path) {
|
|
630
700
|
try {
|
|
631
701
|
// Without an agent key, only native /api/proxy/<slug> is reachable.
|
|
702
|
+
// Native services do not need a `path` argument; if one is passed, we
|
|
703
|
+
// ignore it here. (The agent-execute path forwards it for external entries.)
|
|
704
|
+
void path;
|
|
632
705
|
// We fetch /info first to detect that the slug exists as a native service.
|
|
633
706
|
const infoUrl = new URL(`/api/proxy/${encodeURIComponent(slug)}/info`, API_URL).toString();
|
|
634
707
|
const infoRes = await fetchWithTimeout(infoUrl);
|
|
@@ -703,10 +776,14 @@ async function callViaLegacyProxy(slug, method, body, query) {
|
|
|
703
776
|
}
|
|
704
777
|
const paymentHeaders = {};
|
|
705
778
|
let usedProtocol = "";
|
|
706
|
-
if (paymentRequired && SOLANA_PRIVATE_KEY) {
|
|
779
|
+
if (paymentRequired && (SOLANA_PRIVATE_KEY || PRIVATE_KEY)) {
|
|
707
780
|
try {
|
|
708
|
-
|
|
709
|
-
|
|
781
|
+
const completed = await completeX402Payment(paymentRequired, {
|
|
782
|
+
solana: SOLANA_PRIVATE_KEY,
|
|
783
|
+
evm: PRIVATE_KEY,
|
|
784
|
+
});
|
|
785
|
+
paymentHeaders["X-Payment"] = completed.xPaymentHeader;
|
|
786
|
+
usedProtocol = completed.protocolUsed === "x402-evm" ? "USDC (x402, Base)" : "USDC (x402, Solana)";
|
|
710
787
|
}
|
|
711
788
|
catch (err) {
|
|
712
789
|
if (wwwAuth && PRIVATE_KEY) {
|
|
@@ -844,10 +921,14 @@ async function legacyIntelligenceCall(token, walletAddress) {
|
|
|
844
921
|
const paymentRequired = res.headers.get("payment-required") ?? undefined;
|
|
845
922
|
const paymentHeaders = {};
|
|
846
923
|
let usedProtocol = "";
|
|
847
|
-
if (paymentRequired && SOLANA_PRIVATE_KEY) {
|
|
924
|
+
if (paymentRequired && (SOLANA_PRIVATE_KEY || PRIVATE_KEY)) {
|
|
848
925
|
try {
|
|
849
|
-
|
|
850
|
-
|
|
926
|
+
const completed = await completeX402Payment(paymentRequired, {
|
|
927
|
+
solana: SOLANA_PRIVATE_KEY,
|
|
928
|
+
evm: PRIVATE_KEY,
|
|
929
|
+
});
|
|
930
|
+
paymentHeaders["X-Payment"] = completed.xPaymentHeader;
|
|
931
|
+
usedProtocol = completed.protocolUsed === "x402-evm" ? "USDC (x402, Base)" : "USDC (x402, Solana)";
|
|
851
932
|
}
|
|
852
933
|
catch (x402Err) {
|
|
853
934
|
if (wwwAuth && PRIVATE_KEY) {
|
|
@@ -868,7 +949,7 @@ async function legacyIntelligenceCall(token, walletAddress) {
|
|
|
868
949
|
else {
|
|
869
950
|
return {
|
|
870
951
|
content: [
|
|
871
|
-
{ type: "text", text: `x402 payment failed: ${x402Err instanceof Error ? x402Err.message : String(x402Err)}. Check
|
|
952
|
+
{ type: "text", text: `x402 payment failed: ${x402Err instanceof Error ? x402Err.message : String(x402Err)}. Check that the wallet for the challenge network has sufficient USDC balance.` },
|
|
872
953
|
],
|
|
873
954
|
};
|
|
874
955
|
}
|
|
@@ -969,89 +1050,25 @@ async function completeTempoPayment(challengeParams, privateKey) {
|
|
|
969
1050
|
throw new Error(`Tempo payment failed: ${payErr instanceof Error ? payErr.message : String(payErr)}`);
|
|
970
1051
|
}
|
|
971
1052
|
}
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
throw new Error(`x402 signing requires tweetnacl, which ships with mpp32-mcp-server. ` +
|
|
991
|
-
`If you're seeing this on a clean npx install, upgrade to mpp32-mcp-server@latest. ` +
|
|
992
|
-
`Underlying error: ${err instanceof Error ? err.message : String(err)}`);
|
|
993
|
-
}
|
|
994
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
995
|
-
let bs58;
|
|
996
|
-
try {
|
|
997
|
-
const pkg = "bs58";
|
|
998
|
-
bs58 = await import(pkg);
|
|
999
|
-
}
|
|
1000
|
-
catch (err) {
|
|
1001
|
-
throw new Error(`x402 signing requires bs58, which ships with mpp32-mcp-server. ` +
|
|
1002
|
-
`Upgrade to mpp32-mcp-server@latest. ` +
|
|
1003
|
-
`Underlying error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1004
|
-
}
|
|
1005
|
-
const bs58Decode = bs58.default?.decode ?? bs58.decode;
|
|
1006
|
-
const bs58Encode = bs58.default?.encode ?? bs58.encode;
|
|
1007
|
-
const naclSign = tweetnacl.default?.sign ?? tweetnacl.sign;
|
|
1008
|
-
let rawKey;
|
|
1009
|
-
try {
|
|
1010
|
-
if (solanaPrivateKey.startsWith("[")) {
|
|
1011
|
-
rawKey = new Uint8Array(JSON.parse(solanaPrivateKey));
|
|
1012
|
-
}
|
|
1013
|
-
else if (/^[0-9a-fA-F]+$/.test(solanaPrivateKey) && solanaPrivateKey.length % 2 === 0) {
|
|
1014
|
-
rawKey = new Uint8Array(Buffer.from(solanaPrivateKey, "hex"));
|
|
1015
|
-
}
|
|
1016
|
-
else {
|
|
1017
|
-
rawKey = bs58Decode(solanaPrivateKey);
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
|
-
catch (err) {
|
|
1021
|
-
throw new Error(`Could not decode Solana private key: ${err instanceof Error ? err.message : String(err)}`);
|
|
1022
|
-
}
|
|
1023
|
-
let secretKey;
|
|
1024
|
-
let publicKey;
|
|
1025
|
-
if (rawKey.length === 64) {
|
|
1026
|
-
secretKey = rawKey;
|
|
1027
|
-
publicKey = rawKey.slice(32);
|
|
1028
|
-
}
|
|
1029
|
-
else if (rawKey.length === 32) {
|
|
1030
|
-
const kp = naclSign.keyPair.fromSeed(rawKey);
|
|
1031
|
-
secretKey = kp.secretKey;
|
|
1032
|
-
publicKey = kp.publicKey;
|
|
1033
|
-
}
|
|
1034
|
-
else {
|
|
1035
|
-
throw new Error(`Solana private key must be a 32-byte seed or 64-byte expanded key; got ${rawKey.length} bytes.`);
|
|
1036
|
-
}
|
|
1037
|
-
const payload = {
|
|
1038
|
-
x402Version: 1,
|
|
1039
|
-
scheme: requirements.scheme ?? "exact",
|
|
1040
|
-
network: requirements.network ?? "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
|
|
1041
|
-
payload: {
|
|
1042
|
-
signature: "",
|
|
1043
|
-
from: bs58Encode(publicKey),
|
|
1044
|
-
amount: requirements.maxAmountRequired,
|
|
1045
|
-
asset: requirements.asset,
|
|
1046
|
-
payTo: requirements.payTo,
|
|
1047
|
-
nonce: Date.now().toString(),
|
|
1048
|
-
},
|
|
1053
|
+
// Build a real, x402-spec-compliant payment payload from the server's
|
|
1054
|
+
// Payment-Required challenge. For Solana-family networks, this produces a
|
|
1055
|
+
// base64 partially-signed VersionedTransaction (3 instructions, fee-payer
|
|
1056
|
+
// slot reserved for the facilitator). For Base/Base-Sepolia/Ethereum, it
|
|
1057
|
+
// produces an EIP-3009 transferWithAuthorization signature. Returns the
|
|
1058
|
+
// envelope ready to drop into the `X-Payment` HTTP header.
|
|
1059
|
+
async function completeX402Payment(paymentRequiredHeader, keys) {
|
|
1060
|
+
const solanaRpcUrl = readEnv("MPP32_SOLANA_RPC_URL");
|
|
1061
|
+
const result = await signX402Payment({
|
|
1062
|
+
paymentRequiredHeader,
|
|
1063
|
+
solanaKey: keys.solana,
|
|
1064
|
+
evmKey: keys.evm,
|
|
1065
|
+
solanaRpcUrl,
|
|
1066
|
+
});
|
|
1067
|
+
return {
|
|
1068
|
+
xPaymentHeader: result.xPaymentHeader,
|
|
1069
|
+
network: result.network,
|
|
1070
|
+
protocolUsed: result.protocolUsed,
|
|
1049
1071
|
};
|
|
1050
|
-
const message = JSON.stringify(payload.payload);
|
|
1051
|
-
const messageBytes = new TextEncoder().encode(message);
|
|
1052
|
-
const signed = naclSign.detached(messageBytes, secretKey);
|
|
1053
|
-
payload.payload.signature = Buffer.from(signed).toString("base64");
|
|
1054
|
-
return Buffer.from(JSON.stringify(payload)).toString("base64");
|
|
1055
1072
|
}
|
|
1056
1073
|
// ── Start ───────────────────────────────────────────────────────────────────
|
|
1057
1074
|
async function main() {
|
|
@@ -1065,6 +1082,13 @@ async function main() {
|
|
|
1065
1082
|
.filter(Boolean)
|
|
1066
1083
|
.join(", ") || "no keys (catalog-only legacy mode)";
|
|
1067
1084
|
console.error(`[mpp32] MCP server v${SERVER_VERSION} on stdio. API ${API_URL}. Configured: ${features}. Timeout ${TIMEOUT_MS}ms.`);
|
|
1085
|
+
// Per-variable status so a user staring at this log can immediately see
|
|
1086
|
+
// whether their env vars made it through. Values are fingerprinted.
|
|
1087
|
+
const fp = (v) => !v ? "NOT SET" : v.length <= 12 ? `SET (${v.length}c)` : `SET (${v.slice(0, 6)}…${v.slice(-4)}, ${v.length}c)`;
|
|
1088
|
+
console.error(`[mpp32] MPP32_AGENT_KEY: ${fp(AGENT_KEY)}`);
|
|
1089
|
+
console.error(`[mpp32] MPP32_SOLANA_PRIVATE_KEY: ${fp(SOLANA_PRIVATE_KEY)}`);
|
|
1090
|
+
console.error(`[mpp32] MPP32_PRIVATE_KEY: ${fp(PRIVATE_KEY)}`);
|
|
1091
|
+
console.error(`[mpp32] If a key shows NOT SET but you set it in claude_desktop_config.json, call the get_mpp32_diagnostics tool for help, or fully quit Claude Desktop and reopen.`);
|
|
1068
1092
|
}
|
|
1069
1093
|
main().catch((err) => {
|
|
1070
1094
|
console.error("Fatal:", err);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export interface X402PaymentRequirements {
|
|
2
|
+
scheme: string;
|
|
3
|
+
network: string;
|
|
4
|
+
maxAmountRequired: string;
|
|
5
|
+
resource: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
mimeType?: string;
|
|
8
|
+
payTo: string;
|
|
9
|
+
maxTimeoutSeconds?: number;
|
|
10
|
+
asset: string;
|
|
11
|
+
outputSchema?: unknown;
|
|
12
|
+
extra?: {
|
|
13
|
+
feePayer?: string;
|
|
14
|
+
name?: string;
|
|
15
|
+
version?: string;
|
|
16
|
+
decimals?: number;
|
|
17
|
+
[k: string]: unknown;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export interface X402PaymentEnvelope {
|
|
21
|
+
x402Version: number;
|
|
22
|
+
scheme: string;
|
|
23
|
+
network: string;
|
|
24
|
+
payload: unknown;
|
|
25
|
+
}
|
|
26
|
+
export declare function isSvmNetwork(network: string): boolean;
|
|
27
|
+
export declare function isEvmNetwork(network: string): boolean;
|
|
28
|
+
export declare function signX402PaymentSvm(requirements: X402PaymentRequirements, rawKey: string, rpcUrlOverride?: string): Promise<string>;
|
|
29
|
+
export declare function signX402PaymentEvm(requirements: X402PaymentRequirements, rawKey: string): Promise<string>;
|
|
30
|
+
export interface SignX402Args {
|
|
31
|
+
paymentRequiredHeader: string;
|
|
32
|
+
solanaKey?: string;
|
|
33
|
+
evmKey?: string;
|
|
34
|
+
solanaRpcUrl?: string;
|
|
35
|
+
}
|
|
36
|
+
export interface SignX402Result {
|
|
37
|
+
xPaymentHeader: string;
|
|
38
|
+
network: string;
|
|
39
|
+
scheme: string;
|
|
40
|
+
protocolUsed: "x402-svm" | "x402-evm";
|
|
41
|
+
}
|
|
42
|
+
export declare function signX402Payment(args: SignX402Args): Promise<SignX402Result>;
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
// x402 protocol-compliant payment signers.
|
|
2
|
+
//
|
|
3
|
+
// Two schemes are implemented end-to-end here. Both follow the official
|
|
4
|
+
// `exact` scheme from https://x402.org and the reference implementation at
|
|
5
|
+
// https://github.com/coinbase/x402.
|
|
6
|
+
//
|
|
7
|
+
// • SVM (Solana): build a 3-instruction Solana VersionedTransaction
|
|
8
|
+
// (SetComputeUnitLimit, SetComputeUnitPrice, SPL-Token TransferChecked
|
|
9
|
+
// between Associated Token Accounts), set the facilitator-advertised
|
|
10
|
+
// fee payer, partially sign with the payer's Ed25519 keypair, and
|
|
11
|
+
// base64-encode the wire transaction. The fee-payer signature slot is
|
|
12
|
+
// left empty — the facilitator fills it during /settle.
|
|
13
|
+
// • EVM (Base / Base-Sepolia): sign an EIP-3009 `transferWithAuthorization`
|
|
14
|
+
// typed data message with the payer's secp256k1 key using viem. The
|
|
15
|
+
// resulting signature plus authorization parameters form the payload.
|
|
16
|
+
//
|
|
17
|
+
// In both cases the outer envelope is
|
|
18
|
+
// { x402Version: 1, scheme: "exact", network, payload: <scheme payload> }
|
|
19
|
+
// base64-encoded into the `X-Payment` HTTP header.
|
|
20
|
+
import { address, createKeyPairSignerFromBytes, createSolanaRpc, createTransactionMessage, setTransactionMessageFeePayer, setTransactionMessageLifetimeUsingBlockhash, appendTransactionMessageInstructions, partiallySignTransactionMessageWithSigners, getBase64EncodedWireTransaction, pipe, } from "@solana/kit";
|
|
21
|
+
import { getTransferCheckedInstruction, findAssociatedTokenPda, TOKEN_PROGRAM_ADDRESS, } from "@solana-program/token";
|
|
22
|
+
import { getSetComputeUnitLimitInstruction, getSetComputeUnitPriceInstruction, } from "@solana-program/compute-budget";
|
|
23
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
24
|
+
import bs58 from "bs58";
|
|
25
|
+
import nacl from "tweetnacl";
|
|
26
|
+
// ── Network classification ──────────────────────────────────────────────────
|
|
27
|
+
const SOLANA_MAINNET = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
|
|
28
|
+
const SOLANA_DEVNET = "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1";
|
|
29
|
+
export function isSvmNetwork(network) {
|
|
30
|
+
return network.startsWith("solana") || network === "solana-mainnet" || network === "solana-devnet";
|
|
31
|
+
}
|
|
32
|
+
export function isEvmNetwork(network) {
|
|
33
|
+
if (network.startsWith("eip155:"))
|
|
34
|
+
return true;
|
|
35
|
+
return ["base", "base-sepolia", "ethereum", "ethereum-sepolia"].includes(network);
|
|
36
|
+
}
|
|
37
|
+
function chainSpecFor(network) {
|
|
38
|
+
if (network === "base" || network === "eip155:8453") {
|
|
39
|
+
return { chainId: 8453, name: "Base", rpcUrl: "https://mainnet.base.org" };
|
|
40
|
+
}
|
|
41
|
+
if (network === "base-sepolia" || network === "eip155:84532") {
|
|
42
|
+
return { chainId: 84532, name: "Base Sepolia", rpcUrl: "https://sepolia.base.org" };
|
|
43
|
+
}
|
|
44
|
+
if (network === "ethereum" || network === "eip155:1") {
|
|
45
|
+
return { chainId: 1, name: "Ethereum", rpcUrl: "https://eth.llamarpc.com" };
|
|
46
|
+
}
|
|
47
|
+
throw new Error(`Unsupported EVM network "${network}". x402 EVM payments currently support: base, base-sepolia, ethereum.`);
|
|
48
|
+
}
|
|
49
|
+
// ── Key decoding ────────────────────────────────────────────────────────────
|
|
50
|
+
function decodeSolanaSecret(raw) {
|
|
51
|
+
if (raw.startsWith("[")) {
|
|
52
|
+
const arr = JSON.parse(raw);
|
|
53
|
+
if (!Array.isArray(arr))
|
|
54
|
+
throw new Error("Solana secret JSON array malformed");
|
|
55
|
+
return new Uint8Array(arr);
|
|
56
|
+
}
|
|
57
|
+
if (/^[0-9a-fA-F]+$/.test(raw) && raw.length % 2 === 0) {
|
|
58
|
+
return new Uint8Array(Buffer.from(raw, "hex"));
|
|
59
|
+
}
|
|
60
|
+
return bs58.decode(raw);
|
|
61
|
+
}
|
|
62
|
+
async function buildSolanaSigner(rawKey) {
|
|
63
|
+
let bytes = decodeSolanaSecret(rawKey);
|
|
64
|
+
if (bytes.length === 32) {
|
|
65
|
+
// 32-byte seed — kit's createKeyPairSignerFromBytes wants the 64-byte
|
|
66
|
+
// expanded key. Derive via tweetnacl.
|
|
67
|
+
const kp = nacl.sign.keyPair.fromSeed(bytes);
|
|
68
|
+
bytes = kp.secretKey;
|
|
69
|
+
}
|
|
70
|
+
else if (bytes.length !== 64) {
|
|
71
|
+
throw new Error(`Solana private key must be a 32-byte seed or a 64-byte expanded key; got ${bytes.length} bytes.`);
|
|
72
|
+
}
|
|
73
|
+
return await createKeyPairSignerFromBytes(bytes);
|
|
74
|
+
}
|
|
75
|
+
// ── SVM signer ──────────────────────────────────────────────────────────────
|
|
76
|
+
const DEFAULT_SOLANA_RPC = "https://api.mainnet-beta.solana.com";
|
|
77
|
+
export async function signX402PaymentSvm(requirements, rawKey, rpcUrlOverride) {
|
|
78
|
+
if (requirements.scheme !== "exact") {
|
|
79
|
+
throw new Error(`SVM x402 scheme "${requirements.scheme}" not implemented; only "exact" is supported.`);
|
|
80
|
+
}
|
|
81
|
+
if (!requirements.extra?.feePayer) {
|
|
82
|
+
throw new Error(`SVM x402 challenge is missing extra.feePayer. The facilitator must advertise a fee-payer address per the x402 spec. ` +
|
|
83
|
+
`If you are calling MPP32 itself, upgrade the backend; if a third-party service, ask them to fix their challenge.`);
|
|
84
|
+
}
|
|
85
|
+
const decimals = requirements.extra?.decimals ?? 6;
|
|
86
|
+
const amount = BigInt(requirements.maxAmountRequired);
|
|
87
|
+
if (amount <= 0n)
|
|
88
|
+
throw new Error(`Invalid maxAmountRequired: ${requirements.maxAmountRequired}`);
|
|
89
|
+
const signer = await buildSolanaSigner(rawKey);
|
|
90
|
+
const payerAddress = signer.address;
|
|
91
|
+
const mintAddress = address(requirements.asset);
|
|
92
|
+
const recipientAddress = address(requirements.payTo);
|
|
93
|
+
const feePayerAddress = address(requirements.extra.feePayer);
|
|
94
|
+
// Derive both sides' associated token accounts (classic SPL Token program).
|
|
95
|
+
const [sourceAtaTuple, destinationAtaTuple] = await Promise.all([
|
|
96
|
+
findAssociatedTokenPda({
|
|
97
|
+
owner: payerAddress,
|
|
98
|
+
mint: mintAddress,
|
|
99
|
+
tokenProgram: TOKEN_PROGRAM_ADDRESS,
|
|
100
|
+
}),
|
|
101
|
+
findAssociatedTokenPda({
|
|
102
|
+
owner: recipientAddress,
|
|
103
|
+
mint: mintAddress,
|
|
104
|
+
tokenProgram: TOKEN_PROGRAM_ADDRESS,
|
|
105
|
+
}),
|
|
106
|
+
]);
|
|
107
|
+
const sourceAta = sourceAtaTuple[0];
|
|
108
|
+
const destinationAta = destinationAtaTuple[0];
|
|
109
|
+
const rpcUrl = rpcUrlOverride && rpcUrlOverride.length > 0 ? rpcUrlOverride : DEFAULT_SOLANA_RPC;
|
|
110
|
+
const rpc = createSolanaRpc(rpcUrl);
|
|
111
|
+
const { value: latestBlockhash } = await rpc.getLatestBlockhash({ commitment: "confirmed" }).send();
|
|
112
|
+
const instructions = [
|
|
113
|
+
getSetComputeUnitLimitInstruction({ units: 150_000 }),
|
|
114
|
+
getSetComputeUnitPriceInstruction({ microLamports: 1000n }),
|
|
115
|
+
getTransferCheckedInstruction({
|
|
116
|
+
source: sourceAta,
|
|
117
|
+
mint: mintAddress,
|
|
118
|
+
destination: destinationAta,
|
|
119
|
+
authority: signer,
|
|
120
|
+
amount,
|
|
121
|
+
decimals,
|
|
122
|
+
}),
|
|
123
|
+
];
|
|
124
|
+
const message = pipe(createTransactionMessage({ version: 0 }), (m) => setTransactionMessageFeePayer(feePayerAddress, m), (m) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m), (m) => appendTransactionMessageInstructions(instructions, m));
|
|
125
|
+
// Partially sign — fills the payer's signature slot, leaves the fee payer's
|
|
126
|
+
// slot empty for the facilitator to fill in at /settle time.
|
|
127
|
+
const partiallySigned = await partiallySignTransactionMessageWithSigners(message);
|
|
128
|
+
const base64Tx = getBase64EncodedWireTransaction(partiallySigned);
|
|
129
|
+
const envelope = {
|
|
130
|
+
x402Version: 1,
|
|
131
|
+
scheme: "exact",
|
|
132
|
+
network: requirements.network,
|
|
133
|
+
payload: { transaction: base64Tx },
|
|
134
|
+
};
|
|
135
|
+
return Buffer.from(JSON.stringify(envelope)).toString("base64");
|
|
136
|
+
}
|
|
137
|
+
// ── EVM signer (EIP-3009 transferWithAuthorization) ─────────────────────────
|
|
138
|
+
function randomHex32() {
|
|
139
|
+
const buf = Buffer.alloc(32);
|
|
140
|
+
for (let i = 0; i < 32; i++)
|
|
141
|
+
buf[i] = Math.floor(Math.random() * 256);
|
|
142
|
+
return ("0x" + buf.toString("hex"));
|
|
143
|
+
}
|
|
144
|
+
export async function signX402PaymentEvm(requirements, rawKey) {
|
|
145
|
+
if (requirements.scheme !== "exact") {
|
|
146
|
+
throw new Error(`EVM x402 scheme "${requirements.scheme}" not implemented; only "exact" is supported.`);
|
|
147
|
+
}
|
|
148
|
+
const chain = chainSpecFor(requirements.network);
|
|
149
|
+
const tokenName = requirements.extra?.name ?? "USD Coin";
|
|
150
|
+
const tokenVersion = requirements.extra?.version ?? "2";
|
|
151
|
+
const assetAddr = requirements.asset;
|
|
152
|
+
if (!/^0x[0-9a-fA-F]{40}$/.test(assetAddr)) {
|
|
153
|
+
throw new Error(`EVM x402 challenge asset is not a valid 0x address: ${requirements.asset}`);
|
|
154
|
+
}
|
|
155
|
+
const recipientAddr = requirements.payTo;
|
|
156
|
+
if (!/^0x[0-9a-fA-F]{40}$/.test(recipientAddr)) {
|
|
157
|
+
throw new Error(`EVM x402 challenge payTo is not a valid 0x address: ${requirements.payTo}`);
|
|
158
|
+
}
|
|
159
|
+
const value = BigInt(requirements.maxAmountRequired);
|
|
160
|
+
if (value <= 0n)
|
|
161
|
+
throw new Error(`Invalid maxAmountRequired: ${requirements.maxAmountRequired}`);
|
|
162
|
+
const keyHex = rawKey.startsWith("0x") ? rawKey : `0x${rawKey}`;
|
|
163
|
+
if (!/^0x[0-9a-fA-F]{64}$/.test(keyHex)) {
|
|
164
|
+
throw new Error("MPP32_PRIVATE_KEY must be a 64-character hex EVM private key (0x-prefixed or bare).");
|
|
165
|
+
}
|
|
166
|
+
const account = privateKeyToAccount(keyHex);
|
|
167
|
+
const now = Math.floor(Date.now() / 1000);
|
|
168
|
+
const validAfter = BigInt(0);
|
|
169
|
+
const validBefore = BigInt(now + (requirements.maxTimeoutSeconds ?? 600));
|
|
170
|
+
const nonce = randomHex32();
|
|
171
|
+
const domain = {
|
|
172
|
+
name: tokenName,
|
|
173
|
+
version: tokenVersion,
|
|
174
|
+
chainId: chain.chainId,
|
|
175
|
+
verifyingContract: assetAddr,
|
|
176
|
+
};
|
|
177
|
+
const types = {
|
|
178
|
+
TransferWithAuthorization: [
|
|
179
|
+
{ name: "from", type: "address" },
|
|
180
|
+
{ name: "to", type: "address" },
|
|
181
|
+
{ name: "value", type: "uint256" },
|
|
182
|
+
{ name: "validAfter", type: "uint256" },
|
|
183
|
+
{ name: "validBefore", type: "uint256" },
|
|
184
|
+
{ name: "nonce", type: "bytes32" },
|
|
185
|
+
],
|
|
186
|
+
};
|
|
187
|
+
const messageObj = {
|
|
188
|
+
from: account.address,
|
|
189
|
+
to: recipientAddr,
|
|
190
|
+
value,
|
|
191
|
+
validAfter,
|
|
192
|
+
validBefore,
|
|
193
|
+
nonce,
|
|
194
|
+
};
|
|
195
|
+
const signature = await account.signTypedData({
|
|
196
|
+
domain,
|
|
197
|
+
types,
|
|
198
|
+
primaryType: "TransferWithAuthorization",
|
|
199
|
+
message: messageObj,
|
|
200
|
+
});
|
|
201
|
+
const envelope = {
|
|
202
|
+
x402Version: 1,
|
|
203
|
+
scheme: "exact",
|
|
204
|
+
network: requirements.network,
|
|
205
|
+
payload: {
|
|
206
|
+
signature,
|
|
207
|
+
authorization: {
|
|
208
|
+
from: messageObj.from,
|
|
209
|
+
to: messageObj.to,
|
|
210
|
+
value: messageObj.value.toString(),
|
|
211
|
+
validAfter: messageObj.validAfter.toString(),
|
|
212
|
+
validBefore: messageObj.validBefore.toString(),
|
|
213
|
+
nonce: messageObj.nonce,
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
return Buffer.from(JSON.stringify(envelope)).toString("base64");
|
|
218
|
+
}
|
|
219
|
+
export async function signX402Payment(args) {
|
|
220
|
+
let requirements;
|
|
221
|
+
try {
|
|
222
|
+
const json = Buffer.from(args.paymentRequiredHeader, "base64").toString("utf-8");
|
|
223
|
+
requirements = JSON.parse(json);
|
|
224
|
+
}
|
|
225
|
+
catch (err) {
|
|
226
|
+
throw new Error(`Could not decode Payment-Required header as base64 JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
227
|
+
}
|
|
228
|
+
if (!requirements.network)
|
|
229
|
+
throw new Error("x402 payment requirements missing 'network'");
|
|
230
|
+
if (!requirements.asset)
|
|
231
|
+
throw new Error("x402 payment requirements missing 'asset'");
|
|
232
|
+
if (!requirements.payTo)
|
|
233
|
+
throw new Error("x402 payment requirements missing 'payTo'");
|
|
234
|
+
if (!requirements.maxAmountRequired)
|
|
235
|
+
throw new Error("x402 payment requirements missing 'maxAmountRequired'");
|
|
236
|
+
if (isSvmNetwork(requirements.network)) {
|
|
237
|
+
if (!args.solanaKey) {
|
|
238
|
+
throw new Error(`Provider requires SVM payment on ${requirements.network}, but MPP32_SOLANA_PRIVATE_KEY is not configured. ` +
|
|
239
|
+
`Set it in your MCP config to enable USDC-on-Solana payments.`);
|
|
240
|
+
}
|
|
241
|
+
const header = await signX402PaymentSvm(requirements, args.solanaKey, args.solanaRpcUrl);
|
|
242
|
+
return {
|
|
243
|
+
xPaymentHeader: header,
|
|
244
|
+
network: requirements.network,
|
|
245
|
+
scheme: requirements.scheme,
|
|
246
|
+
protocolUsed: "x402-svm",
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
if (isEvmNetwork(requirements.network)) {
|
|
250
|
+
if (!args.evmKey) {
|
|
251
|
+
throw new Error(`Provider requires EVM payment on ${requirements.network}, but MPP32_PRIVATE_KEY is not configured. ` +
|
|
252
|
+
`Set it in your MCP config to enable USDC-on-Base payments.`);
|
|
253
|
+
}
|
|
254
|
+
const header = await signX402PaymentEvm(requirements, args.evmKey);
|
|
255
|
+
return {
|
|
256
|
+
xPaymentHeader: header,
|
|
257
|
+
network: requirements.network,
|
|
258
|
+
scheme: requirements.scheme,
|
|
259
|
+
protocolUsed: "x402-evm",
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
if (requirements.network === "" || requirements.network === undefined) {
|
|
263
|
+
throw new Error(`Provider's x402 challenge does not specify a network. We cannot pay it. Ask the provider to fix their challenge.`);
|
|
264
|
+
}
|
|
265
|
+
throw new Error(`Unsupported x402 network "${requirements.network}". Supported: solana:*, base, base-sepolia, ethereum (and their eip155:* aliases). ` +
|
|
266
|
+
`If this network is real and we should support it, file an issue.`);
|
|
267
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mpp32-mcp-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
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",
|
|
@@ -69,20 +69,20 @@
|
|
|
69
69
|
},
|
|
70
70
|
"dependencies": {
|
|
71
71
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
72
|
+
"@solana-program/compute-budget": "^0.15.0",
|
|
73
|
+
"@solana-program/token": "^0.13.0",
|
|
74
|
+
"@solana/kit": "^6.9.0",
|
|
72
75
|
"bs58": "^6.0.0",
|
|
73
76
|
"tweetnacl": "^1.0.3",
|
|
77
|
+
"viem": "^2.48.11",
|
|
74
78
|
"zod": "^3.23.0"
|
|
75
79
|
},
|
|
76
80
|
"peerDependencies": {
|
|
77
|
-
"mppx": ">=0.4.0"
|
|
78
|
-
"viem": ">=2.0.0"
|
|
81
|
+
"mppx": ">=0.4.0"
|
|
79
82
|
},
|
|
80
83
|
"peerDependenciesMeta": {
|
|
81
84
|
"mppx": {
|
|
82
85
|
"optional": true
|
|
83
|
-
},
|
|
84
|
-
"viem": {
|
|
85
|
-
"optional": true
|
|
86
86
|
}
|
|
87
87
|
},
|
|
88
88
|
"devDependencies": {
|