mpp32-mcp-server 1.1.4 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +70 -0
- package/dist/index.js +61 -101
- package/dist/x402-signers.d.ts +42 -0
- package/dist/x402-signers.js +306 -0
- package/package.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,76 @@ 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.1] - 2026-05-11
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
* **"Server disconnected" on startup under Claude Desktop's bundled Node.**
|
|
12
|
+
1.2.0 imported `@solana/kit` (and `@solana-program/*`) at the top of the
|
|
13
|
+
payment-signer module. Those packages declare `engines.node: ">=20.18.0"`
|
|
14
|
+
and use Node-20-only WebCrypto Ed25519 APIs at load time. Claude Desktop
|
|
15
|
+
ships a bundled Node that on many installs is still 18.x, so the import
|
|
16
|
+
threw before the MCP server could answer the initialize handshake — the
|
|
17
|
+
process exited and Claude Desktop reported only "Server disconnected"
|
|
18
|
+
with no further diagnostics. All Solana and EVM crypto deps are now
|
|
19
|
+
loaded lazily inside the signer functions. The server boots on any Node
|
|
20
|
+
that supports MCP; only a `solana:*` payment attempt fails on too-old
|
|
21
|
+
Node, and now with a clear actionable error.
|
|
22
|
+
|
|
23
|
+
## [1.2.0] - 2026-05-11
|
|
24
|
+
|
|
25
|
+
This release makes x402 payments actually work end-to-end. Prior versions
|
|
26
|
+
shipped a non-spec-compliant signing path that the official x402.org
|
|
27
|
+
facilitator rejected with HTTP 400 on every paid call, so no settlement ever
|
|
28
|
+
occurred. The signing path has been rewritten from scratch against the
|
|
29
|
+
[Coinbase x402 reference implementation](https://github.com/coinbase/x402).
|
|
30
|
+
|
|
31
|
+
### Fixed (the headline)
|
|
32
|
+
|
|
33
|
+
* **Real Solana x402 payments.** When a server returns a `Payment-Required`
|
|
34
|
+
challenge on a `solana:*` network, the MCP client now builds a real Solana
|
|
35
|
+
`VersionedTransaction` with the three instructions the `exact` SVM scheme
|
|
36
|
+
requires — `SetComputeUnitLimit`, `SetComputeUnitPrice`, and SPL-Token
|
|
37
|
+
`TransferChecked` between the payer's and recipient's Associated Token
|
|
38
|
+
Accounts. The transaction is partially signed by the payer (the fee-payer
|
|
39
|
+
slot is left empty for the facilitator to fill at `/settle` time, per spec)
|
|
40
|
+
and base64-encoded into the `payload.transaction` field. The official
|
|
41
|
+
`x402.org/facilitator` now accepts and settles these payments.
|
|
42
|
+
|
|
43
|
+
* **Real EVM x402 payments on Base.** For challenges with `network: "base"`
|
|
44
|
+
or `network: "base-sepolia"` (and the `eip155:*` aliases), the client now
|
|
45
|
+
signs an EIP-3009 `transferWithAuthorization` typed-data message using
|
|
46
|
+
`viem` and the EVM private key in `MPP32_PRIVATE_KEY`. This unblocks the
|
|
47
|
+
~85% of the federated catalog (~3,900 of 4,581 entries) that lives on
|
|
48
|
+
Base — Exa Search, Firecrawl, OpenAI's x402 gateway, Anthropic's,
|
|
49
|
+
Alchemy RPC, CoinGecko Pro, Nansen, Cloudflare Workers AI, and the rest.
|
|
50
|
+
|
|
51
|
+
### Added
|
|
52
|
+
|
|
53
|
+
* **`path` argument to `call_mpp32_endpoint`.** Many curated catalog
|
|
54
|
+
entries store only an upstream base URL (e.g. `https://api.exa.ai`). Pass
|
|
55
|
+
the upstream path (e.g. `/search`) via the new `path` parameter to hit a
|
|
56
|
+
real endpoint instead of `POST /` (which returned 404). The agent server
|
|
57
|
+
forwards the path and appends it safely to the catalog base URL.
|
|
58
|
+
|
|
59
|
+
* **`MPP32_SOLANA_RPC_URL` env var.** Override the Solana RPC used to fetch
|
|
60
|
+
recent blockhashes when building x402 transactions. Defaults to
|
|
61
|
+
`https://api.mainnet-beta.solana.com`. Set this if you hit public-endpoint
|
|
62
|
+
rate limits.
|
|
63
|
+
|
|
64
|
+
* **`@solana/kit`, `@solana-program/token`, `@solana-program/compute-budget`,
|
|
65
|
+
`viem` as real dependencies.** Tree-shakeable, no `rpc-websockets`
|
|
66
|
+
transitive dependency, and no ESM/CJS landmines on Node 20+. `viem` was
|
|
67
|
+
previously an optional peer; it is now required because the EVM x402
|
|
68
|
+
signer cannot work without it.
|
|
69
|
+
|
|
70
|
+
### Migration
|
|
71
|
+
|
|
72
|
+
* No config changes required if you only use the Solana intelligence oracle
|
|
73
|
+
(it still uses `MPP32_SOLANA_PRIVATE_KEY`). To pay for Base-network
|
|
74
|
+
services like Exa Search, set `MPP32_PRIVATE_KEY` to your EVM private key
|
|
75
|
+
and ensure that wallet holds USDC on Base.
|
|
76
|
+
|
|
7
77
|
## [1.1.4] - 2026-05-11
|
|
8
78
|
|
|
9
79
|
### Added
|
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.1";
|
|
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
|
|
@@ -342,14 +343,18 @@ server.tool("list_mpp32_services", "Browse the MPP32 federated catalog of 4,500+
|
|
|
342
343
|
}
|
|
343
344
|
});
|
|
344
345
|
// ── Tool 2: call_mpp32_endpoint ─────────────────────────────────────────────
|
|
345
|
-
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.", {
|
|
346
347
|
slug: z
|
|
347
348
|
.string()
|
|
348
|
-
.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')."),
|
|
349
350
|
method: z
|
|
350
351
|
.enum(["GET", "POST", "PUT", "DELETE"])
|
|
351
352
|
.default("POST")
|
|
352
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 '/'."),
|
|
353
358
|
body: z
|
|
354
359
|
.union([z.string(), z.record(z.unknown())])
|
|
355
360
|
.optional()
|
|
@@ -358,7 +363,7 @@ server.tool("call_mpp32_endpoint", "Call any HTTP-callable service in the MPP32
|
|
|
358
363
|
.record(z.string())
|
|
359
364
|
.optional()
|
|
360
365
|
.describe("URL query parameters as key-value pairs."),
|
|
361
|
-
}, async ({ slug, method, body, query }) => {
|
|
366
|
+
}, async ({ slug, method, path, body, query }) => {
|
|
362
367
|
// Normalize body to an object so it can be JSON.stringified by the upstream call
|
|
363
368
|
let parsedBody = body;
|
|
364
369
|
if (typeof body === "string") {
|
|
@@ -370,10 +375,10 @@ server.tool("call_mpp32_endpoint", "Call any HTTP-callable service in the MPP32
|
|
|
370
375
|
}
|
|
371
376
|
}
|
|
372
377
|
if (AGENT_KEY) {
|
|
373
|
-
return await callViaAgentExecute(slug, method, parsedBody, query);
|
|
378
|
+
return await callViaAgentExecute(slug, method, parsedBody, query, path);
|
|
374
379
|
}
|
|
375
380
|
// Legacy path — only works for native services with payment keys
|
|
376
|
-
return await callViaLegacyProxy(slug, method, parsedBody, query);
|
|
381
|
+
return await callViaLegacyProxy(slug, method, parsedBody, query, path);
|
|
377
382
|
});
|
|
378
383
|
// ── Tool 3: get_solana_token_intelligence ───────────────────────────────────
|
|
379
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.", {
|
|
@@ -393,7 +398,7 @@ server.tool("get_solana_token_intelligence", "Get real-time Solana token intelli
|
|
|
393
398
|
return await legacyIntelligenceCall(token, walletAddress);
|
|
394
399
|
});
|
|
395
400
|
// ── Core: agent/execute path with 402 sign-and-retry ────────────────────────
|
|
396
|
-
async function callViaAgentExecute(service, method, body, query) {
|
|
401
|
+
async function callViaAgentExecute(service, method, body, query, path) {
|
|
397
402
|
try {
|
|
398
403
|
const execUrl = new URL("/api/agent/execute", API_URL).toString();
|
|
399
404
|
const reqBody = JSON.stringify({
|
|
@@ -401,6 +406,7 @@ async function callViaAgentExecute(service, method, body, query) {
|
|
|
401
406
|
method,
|
|
402
407
|
...(body !== undefined ? { body } : {}),
|
|
403
408
|
...(query ? { query } : {}),
|
|
409
|
+
...(path ? { path } : {}),
|
|
404
410
|
});
|
|
405
411
|
// Round 1: no payment headers
|
|
406
412
|
const firstRes = await fetchWithTimeout(execUrl, {
|
|
@@ -452,11 +458,18 @@ function detectPaymentRequired(resp) {
|
|
|
452
458
|
async function signAndRetry(execUrl, reqBody, challenge) {
|
|
453
459
|
const paymentHeaders = {};
|
|
454
460
|
let usedProtocol = "";
|
|
455
|
-
// Prefer x402 if
|
|
456
|
-
|
|
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)) {
|
|
457
466
|
try {
|
|
458
|
-
|
|
459
|
-
|
|
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)";
|
|
460
473
|
}
|
|
461
474
|
catch (err) {
|
|
462
475
|
// Fall through to Tempo if available
|
|
@@ -683,9 +696,12 @@ function paymentFailedMessage(challenge, proto, err) {
|
|
|
683
696
|
};
|
|
684
697
|
}
|
|
685
698
|
// ── Legacy path (no MPP32_AGENT_KEY) ────────────────────────────────────────
|
|
686
|
-
async function callViaLegacyProxy(slug, method, body, query) {
|
|
699
|
+
async function callViaLegacyProxy(slug, method, body, query, path) {
|
|
687
700
|
try {
|
|
688
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;
|
|
689
705
|
// We fetch /info first to detect that the slug exists as a native service.
|
|
690
706
|
const infoUrl = new URL(`/api/proxy/${encodeURIComponent(slug)}/info`, API_URL).toString();
|
|
691
707
|
const infoRes = await fetchWithTimeout(infoUrl);
|
|
@@ -760,10 +776,14 @@ async function callViaLegacyProxy(slug, method, body, query) {
|
|
|
760
776
|
}
|
|
761
777
|
const paymentHeaders = {};
|
|
762
778
|
let usedProtocol = "";
|
|
763
|
-
if (paymentRequired && SOLANA_PRIVATE_KEY) {
|
|
779
|
+
if (paymentRequired && (SOLANA_PRIVATE_KEY || PRIVATE_KEY)) {
|
|
764
780
|
try {
|
|
765
|
-
|
|
766
|
-
|
|
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)";
|
|
767
787
|
}
|
|
768
788
|
catch (err) {
|
|
769
789
|
if (wwwAuth && PRIVATE_KEY) {
|
|
@@ -901,10 +921,14 @@ async function legacyIntelligenceCall(token, walletAddress) {
|
|
|
901
921
|
const paymentRequired = res.headers.get("payment-required") ?? undefined;
|
|
902
922
|
const paymentHeaders = {};
|
|
903
923
|
let usedProtocol = "";
|
|
904
|
-
if (paymentRequired && SOLANA_PRIVATE_KEY) {
|
|
924
|
+
if (paymentRequired && (SOLANA_PRIVATE_KEY || PRIVATE_KEY)) {
|
|
905
925
|
try {
|
|
906
|
-
|
|
907
|
-
|
|
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)";
|
|
908
932
|
}
|
|
909
933
|
catch (x402Err) {
|
|
910
934
|
if (wwwAuth && PRIVATE_KEY) {
|
|
@@ -925,7 +949,7 @@ async function legacyIntelligenceCall(token, walletAddress) {
|
|
|
925
949
|
else {
|
|
926
950
|
return {
|
|
927
951
|
content: [
|
|
928
|
-
{ 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.` },
|
|
929
953
|
],
|
|
930
954
|
};
|
|
931
955
|
}
|
|
@@ -1026,89 +1050,25 @@ async function completeTempoPayment(challengeParams, privateKey) {
|
|
|
1026
1050
|
throw new Error(`Tempo payment failed: ${payErr instanceof Error ? payErr.message : String(payErr)}`);
|
|
1027
1051
|
}
|
|
1028
1052
|
}
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
throw new Error(`x402 signing requires tweetnacl, which ships with mpp32-mcp-server. ` +
|
|
1048
|
-
`If you're seeing this on a clean npx install, upgrade to mpp32-mcp-server@latest. ` +
|
|
1049
|
-
`Underlying error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1050
|
-
}
|
|
1051
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1052
|
-
let bs58;
|
|
1053
|
-
try {
|
|
1054
|
-
const pkg = "bs58";
|
|
1055
|
-
bs58 = await import(pkg);
|
|
1056
|
-
}
|
|
1057
|
-
catch (err) {
|
|
1058
|
-
throw new Error(`x402 signing requires bs58, which ships with mpp32-mcp-server. ` +
|
|
1059
|
-
`Upgrade to mpp32-mcp-server@latest. ` +
|
|
1060
|
-
`Underlying error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1061
|
-
}
|
|
1062
|
-
const bs58Decode = bs58.default?.decode ?? bs58.decode;
|
|
1063
|
-
const bs58Encode = bs58.default?.encode ?? bs58.encode;
|
|
1064
|
-
const naclSign = tweetnacl.default?.sign ?? tweetnacl.sign;
|
|
1065
|
-
let rawKey;
|
|
1066
|
-
try {
|
|
1067
|
-
if (solanaPrivateKey.startsWith("[")) {
|
|
1068
|
-
rawKey = new Uint8Array(JSON.parse(solanaPrivateKey));
|
|
1069
|
-
}
|
|
1070
|
-
else if (/^[0-9a-fA-F]+$/.test(solanaPrivateKey) && solanaPrivateKey.length % 2 === 0) {
|
|
1071
|
-
rawKey = new Uint8Array(Buffer.from(solanaPrivateKey, "hex"));
|
|
1072
|
-
}
|
|
1073
|
-
else {
|
|
1074
|
-
rawKey = bs58Decode(solanaPrivateKey);
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
|
-
catch (err) {
|
|
1078
|
-
throw new Error(`Could not decode Solana private key: ${err instanceof Error ? err.message : String(err)}`);
|
|
1079
|
-
}
|
|
1080
|
-
let secretKey;
|
|
1081
|
-
let publicKey;
|
|
1082
|
-
if (rawKey.length === 64) {
|
|
1083
|
-
secretKey = rawKey;
|
|
1084
|
-
publicKey = rawKey.slice(32);
|
|
1085
|
-
}
|
|
1086
|
-
else if (rawKey.length === 32) {
|
|
1087
|
-
const kp = naclSign.keyPair.fromSeed(rawKey);
|
|
1088
|
-
secretKey = kp.secretKey;
|
|
1089
|
-
publicKey = kp.publicKey;
|
|
1090
|
-
}
|
|
1091
|
-
else {
|
|
1092
|
-
throw new Error(`Solana private key must be a 32-byte seed or 64-byte expanded key; got ${rawKey.length} bytes.`);
|
|
1093
|
-
}
|
|
1094
|
-
const payload = {
|
|
1095
|
-
x402Version: 1,
|
|
1096
|
-
scheme: requirements.scheme ?? "exact",
|
|
1097
|
-
network: requirements.network ?? "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
|
|
1098
|
-
payload: {
|
|
1099
|
-
signature: "",
|
|
1100
|
-
from: bs58Encode(publicKey),
|
|
1101
|
-
amount: requirements.maxAmountRequired,
|
|
1102
|
-
asset: requirements.asset,
|
|
1103
|
-
payTo: requirements.payTo,
|
|
1104
|
-
nonce: Date.now().toString(),
|
|
1105
|
-
},
|
|
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,
|
|
1106
1071
|
};
|
|
1107
|
-
const message = JSON.stringify(payload.payload);
|
|
1108
|
-
const messageBytes = new TextEncoder().encode(message);
|
|
1109
|
-
const signed = naclSign.detached(messageBytes, secretKey);
|
|
1110
|
-
payload.payload.signature = Buffer.from(signed).toString("base64");
|
|
1111
|
-
return Buffer.from(JSON.stringify(payload)).toString("base64");
|
|
1112
1072
|
}
|
|
1113
1073
|
// ── Start ───────────────────────────────────────────────────────────────────
|
|
1114
1074
|
async function main() {
|
|
@@ -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,306 @@
|
|
|
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
|
+
async function loadSvmDeps() {
|
|
21
|
+
const [kit, tokenProgram, computeBudgetProgram, bs58Mod, naclMod] = await Promise.all([
|
|
22
|
+
import("@solana/kit"),
|
|
23
|
+
import("@solana-program/token"),
|
|
24
|
+
import("@solana-program/compute-budget"),
|
|
25
|
+
import("bs58"),
|
|
26
|
+
import("tweetnacl"),
|
|
27
|
+
]).catch((err) => {
|
|
28
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
29
|
+
throw new Error(`Could not load Solana signing libraries: ${msg}. ` +
|
|
30
|
+
`Solana x402 payments require Node 20.18 or newer (Claude Desktop's bundled Node may be older). ` +
|
|
31
|
+
`Upgrade Node, or run mpp32-mcp-server under a system Node 20+ via your MCP config's "command".`);
|
|
32
|
+
});
|
|
33
|
+
return {
|
|
34
|
+
address: kit.address,
|
|
35
|
+
createKeyPairSignerFromBytes: kit.createKeyPairSignerFromBytes,
|
|
36
|
+
createSolanaRpc: kit.createSolanaRpc,
|
|
37
|
+
createTransactionMessage: kit.createTransactionMessage,
|
|
38
|
+
setTransactionMessageFeePayer: kit.setTransactionMessageFeePayer,
|
|
39
|
+
setTransactionMessageLifetimeUsingBlockhash: kit.setTransactionMessageLifetimeUsingBlockhash,
|
|
40
|
+
appendTransactionMessageInstructions: kit.appendTransactionMessageInstructions,
|
|
41
|
+
partiallySignTransactionMessageWithSigners: kit.partiallySignTransactionMessageWithSigners,
|
|
42
|
+
getBase64EncodedWireTransaction: kit.getBase64EncodedWireTransaction,
|
|
43
|
+
pipe: kit.pipe,
|
|
44
|
+
getTransferCheckedInstruction: tokenProgram.getTransferCheckedInstruction,
|
|
45
|
+
findAssociatedTokenPda: tokenProgram.findAssociatedTokenPda,
|
|
46
|
+
TOKEN_PROGRAM_ADDRESS: tokenProgram.TOKEN_PROGRAM_ADDRESS,
|
|
47
|
+
getSetComputeUnitLimitInstruction: computeBudgetProgram.getSetComputeUnitLimitInstruction,
|
|
48
|
+
getSetComputeUnitPriceInstruction: computeBudgetProgram.getSetComputeUnitPriceInstruction,
|
|
49
|
+
bs58: bs58Mod.default ?? bs58Mod,
|
|
50
|
+
nacl: naclMod.default ?? naclMod,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
async function loadEvmDeps() {
|
|
54
|
+
try {
|
|
55
|
+
const viemAccounts = await import("viem/accounts");
|
|
56
|
+
return { privateKeyToAccount: viemAccounts.privateKeyToAccount };
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
60
|
+
throw new Error(`Could not load EVM signing libraries: ${msg}. ` +
|
|
61
|
+
`Base / Ethereum x402 payments require Node 18 or newer. Upgrade Node and retry.`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// ── Network classification ──────────────────────────────────────────────────
|
|
65
|
+
const SOLANA_MAINNET = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
|
|
66
|
+
const SOLANA_DEVNET = "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1";
|
|
67
|
+
export function isSvmNetwork(network) {
|
|
68
|
+
return network.startsWith("solana") || network === "solana-mainnet" || network === "solana-devnet";
|
|
69
|
+
}
|
|
70
|
+
export function isEvmNetwork(network) {
|
|
71
|
+
if (network.startsWith("eip155:"))
|
|
72
|
+
return true;
|
|
73
|
+
return ["base", "base-sepolia", "ethereum", "ethereum-sepolia"].includes(network);
|
|
74
|
+
}
|
|
75
|
+
function chainSpecFor(network) {
|
|
76
|
+
if (network === "base" || network === "eip155:8453") {
|
|
77
|
+
return { chainId: 8453, name: "Base", rpcUrl: "https://mainnet.base.org" };
|
|
78
|
+
}
|
|
79
|
+
if (network === "base-sepolia" || network === "eip155:84532") {
|
|
80
|
+
return { chainId: 84532, name: "Base Sepolia", rpcUrl: "https://sepolia.base.org" };
|
|
81
|
+
}
|
|
82
|
+
if (network === "ethereum" || network === "eip155:1") {
|
|
83
|
+
return { chainId: 1, name: "Ethereum", rpcUrl: "https://eth.llamarpc.com" };
|
|
84
|
+
}
|
|
85
|
+
throw new Error(`Unsupported EVM network "${network}". x402 EVM payments currently support: base, base-sepolia, ethereum.`);
|
|
86
|
+
}
|
|
87
|
+
function decodeSolanaSecret(raw, deps) {
|
|
88
|
+
if (raw.startsWith("[")) {
|
|
89
|
+
const arr = JSON.parse(raw);
|
|
90
|
+
if (!Array.isArray(arr))
|
|
91
|
+
throw new Error("Solana secret JSON array malformed");
|
|
92
|
+
return new Uint8Array(arr);
|
|
93
|
+
}
|
|
94
|
+
if (/^[0-9a-fA-F]+$/.test(raw) && raw.length % 2 === 0) {
|
|
95
|
+
return new Uint8Array(Buffer.from(raw, "hex"));
|
|
96
|
+
}
|
|
97
|
+
return deps.bs58.decode(raw);
|
|
98
|
+
}
|
|
99
|
+
async function buildSolanaSigner(rawKey, deps) {
|
|
100
|
+
let bytes = decodeSolanaSecret(rawKey, deps);
|
|
101
|
+
if (bytes.length === 32) {
|
|
102
|
+
// 32-byte seed — kit's createKeyPairSignerFromBytes wants the 64-byte
|
|
103
|
+
// expanded key. Derive via tweetnacl.
|
|
104
|
+
const kp = deps.nacl.sign.keyPair.fromSeed(bytes);
|
|
105
|
+
bytes = kp.secretKey;
|
|
106
|
+
}
|
|
107
|
+
else if (bytes.length !== 64) {
|
|
108
|
+
throw new Error(`Solana private key must be a 32-byte seed or a 64-byte expanded key; got ${bytes.length} bytes.`);
|
|
109
|
+
}
|
|
110
|
+
return await deps.createKeyPairSignerFromBytes(bytes);
|
|
111
|
+
}
|
|
112
|
+
// ── SVM signer ──────────────────────────────────────────────────────────────
|
|
113
|
+
const DEFAULT_SOLANA_RPC = "https://api.mainnet-beta.solana.com";
|
|
114
|
+
export async function signX402PaymentSvm(requirements, rawKey, rpcUrlOverride) {
|
|
115
|
+
if (requirements.scheme !== "exact") {
|
|
116
|
+
throw new Error(`SVM x402 scheme "${requirements.scheme}" not implemented; only "exact" is supported.`);
|
|
117
|
+
}
|
|
118
|
+
if (!requirements.extra?.feePayer) {
|
|
119
|
+
throw new Error(`SVM x402 challenge is missing extra.feePayer. The facilitator must advertise a fee-payer address per the x402 spec. ` +
|
|
120
|
+
`If you are calling MPP32 itself, upgrade the backend; if a third-party service, ask them to fix their challenge.`);
|
|
121
|
+
}
|
|
122
|
+
const decimals = requirements.extra?.decimals ?? 6;
|
|
123
|
+
const amount = BigInt(requirements.maxAmountRequired);
|
|
124
|
+
if (amount <= 0n)
|
|
125
|
+
throw new Error(`Invalid maxAmountRequired: ${requirements.maxAmountRequired}`);
|
|
126
|
+
const deps = await loadSvmDeps();
|
|
127
|
+
const signer = await buildSolanaSigner(rawKey, deps);
|
|
128
|
+
const payerAddress = signer.address;
|
|
129
|
+
const mintAddress = deps.address(requirements.asset);
|
|
130
|
+
const recipientAddress = deps.address(requirements.payTo);
|
|
131
|
+
const feePayerAddress = deps.address(requirements.extra.feePayer);
|
|
132
|
+
// Derive both sides' associated token accounts (classic SPL Token program).
|
|
133
|
+
const [sourceAtaTuple, destinationAtaTuple] = await Promise.all([
|
|
134
|
+
deps.findAssociatedTokenPda({
|
|
135
|
+
owner: payerAddress,
|
|
136
|
+
mint: mintAddress,
|
|
137
|
+
tokenProgram: deps.TOKEN_PROGRAM_ADDRESS,
|
|
138
|
+
}),
|
|
139
|
+
deps.findAssociatedTokenPda({
|
|
140
|
+
owner: recipientAddress,
|
|
141
|
+
mint: mintAddress,
|
|
142
|
+
tokenProgram: deps.TOKEN_PROGRAM_ADDRESS,
|
|
143
|
+
}),
|
|
144
|
+
]);
|
|
145
|
+
const sourceAta = sourceAtaTuple[0];
|
|
146
|
+
const destinationAta = destinationAtaTuple[0];
|
|
147
|
+
const rpcUrl = rpcUrlOverride && rpcUrlOverride.length > 0 ? rpcUrlOverride : DEFAULT_SOLANA_RPC;
|
|
148
|
+
const rpc = deps.createSolanaRpc(rpcUrl);
|
|
149
|
+
const { value: latestBlockhash } = await rpc.getLatestBlockhash({ commitment: "confirmed" }).send();
|
|
150
|
+
const instructions = [
|
|
151
|
+
deps.getSetComputeUnitLimitInstruction({ units: 150_000 }),
|
|
152
|
+
deps.getSetComputeUnitPriceInstruction({ microLamports: 1000n }),
|
|
153
|
+
deps.getTransferCheckedInstruction({
|
|
154
|
+
source: sourceAta,
|
|
155
|
+
mint: mintAddress,
|
|
156
|
+
destination: destinationAta,
|
|
157
|
+
authority: signer,
|
|
158
|
+
amount,
|
|
159
|
+
decimals,
|
|
160
|
+
}),
|
|
161
|
+
];
|
|
162
|
+
const message = deps.pipe(deps.createTransactionMessage({ version: 0 }), (m) => deps.setTransactionMessageFeePayer(feePayerAddress, m), (m) => deps.setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m), (m) => deps.appendTransactionMessageInstructions(instructions, m));
|
|
163
|
+
// Partially sign — fills the payer's signature slot, leaves the fee payer's
|
|
164
|
+
// slot empty for the facilitator to fill in at /settle time.
|
|
165
|
+
const partiallySigned = await deps.partiallySignTransactionMessageWithSigners(message);
|
|
166
|
+
const base64Tx = deps.getBase64EncodedWireTransaction(partiallySigned);
|
|
167
|
+
const envelope = {
|
|
168
|
+
x402Version: 1,
|
|
169
|
+
scheme: "exact",
|
|
170
|
+
network: requirements.network,
|
|
171
|
+
payload: { transaction: base64Tx },
|
|
172
|
+
};
|
|
173
|
+
return Buffer.from(JSON.stringify(envelope)).toString("base64");
|
|
174
|
+
}
|
|
175
|
+
// ── EVM signer (EIP-3009 transferWithAuthorization) ─────────────────────────
|
|
176
|
+
function randomHex32() {
|
|
177
|
+
const buf = Buffer.alloc(32);
|
|
178
|
+
for (let i = 0; i < 32; i++)
|
|
179
|
+
buf[i] = Math.floor(Math.random() * 256);
|
|
180
|
+
return ("0x" + buf.toString("hex"));
|
|
181
|
+
}
|
|
182
|
+
export async function signX402PaymentEvm(requirements, rawKey) {
|
|
183
|
+
if (requirements.scheme !== "exact") {
|
|
184
|
+
throw new Error(`EVM x402 scheme "${requirements.scheme}" not implemented; only "exact" is supported.`);
|
|
185
|
+
}
|
|
186
|
+
const chain = chainSpecFor(requirements.network);
|
|
187
|
+
const tokenName = requirements.extra?.name ?? "USD Coin";
|
|
188
|
+
const tokenVersion = requirements.extra?.version ?? "2";
|
|
189
|
+
const assetAddr = requirements.asset;
|
|
190
|
+
if (!/^0x[0-9a-fA-F]{40}$/.test(assetAddr)) {
|
|
191
|
+
throw new Error(`EVM x402 challenge asset is not a valid 0x address: ${requirements.asset}`);
|
|
192
|
+
}
|
|
193
|
+
const recipientAddr = requirements.payTo;
|
|
194
|
+
if (!/^0x[0-9a-fA-F]{40}$/.test(recipientAddr)) {
|
|
195
|
+
throw new Error(`EVM x402 challenge payTo is not a valid 0x address: ${requirements.payTo}`);
|
|
196
|
+
}
|
|
197
|
+
const value = BigInt(requirements.maxAmountRequired);
|
|
198
|
+
if (value <= 0n)
|
|
199
|
+
throw new Error(`Invalid maxAmountRequired: ${requirements.maxAmountRequired}`);
|
|
200
|
+
const keyHex = rawKey.startsWith("0x") ? rawKey : `0x${rawKey}`;
|
|
201
|
+
if (!/^0x[0-9a-fA-F]{64}$/.test(keyHex)) {
|
|
202
|
+
throw new Error("MPP32_PRIVATE_KEY must be a 64-character hex EVM private key (0x-prefixed or bare).");
|
|
203
|
+
}
|
|
204
|
+
const { privateKeyToAccount } = await loadEvmDeps();
|
|
205
|
+
const account = privateKeyToAccount(keyHex);
|
|
206
|
+
const now = Math.floor(Date.now() / 1000);
|
|
207
|
+
const validAfter = BigInt(0);
|
|
208
|
+
const validBefore = BigInt(now + (requirements.maxTimeoutSeconds ?? 600));
|
|
209
|
+
const nonce = randomHex32();
|
|
210
|
+
const domain = {
|
|
211
|
+
name: tokenName,
|
|
212
|
+
version: tokenVersion,
|
|
213
|
+
chainId: chain.chainId,
|
|
214
|
+
verifyingContract: assetAddr,
|
|
215
|
+
};
|
|
216
|
+
const types = {
|
|
217
|
+
TransferWithAuthorization: [
|
|
218
|
+
{ name: "from", type: "address" },
|
|
219
|
+
{ name: "to", type: "address" },
|
|
220
|
+
{ name: "value", type: "uint256" },
|
|
221
|
+
{ name: "validAfter", type: "uint256" },
|
|
222
|
+
{ name: "validBefore", type: "uint256" },
|
|
223
|
+
{ name: "nonce", type: "bytes32" },
|
|
224
|
+
],
|
|
225
|
+
};
|
|
226
|
+
const messageObj = {
|
|
227
|
+
from: account.address,
|
|
228
|
+
to: recipientAddr,
|
|
229
|
+
value,
|
|
230
|
+
validAfter,
|
|
231
|
+
validBefore,
|
|
232
|
+
nonce,
|
|
233
|
+
};
|
|
234
|
+
const signature = await account.signTypedData({
|
|
235
|
+
domain,
|
|
236
|
+
types,
|
|
237
|
+
primaryType: "TransferWithAuthorization",
|
|
238
|
+
message: messageObj,
|
|
239
|
+
});
|
|
240
|
+
const envelope = {
|
|
241
|
+
x402Version: 1,
|
|
242
|
+
scheme: "exact",
|
|
243
|
+
network: requirements.network,
|
|
244
|
+
payload: {
|
|
245
|
+
signature,
|
|
246
|
+
authorization: {
|
|
247
|
+
from: messageObj.from,
|
|
248
|
+
to: messageObj.to,
|
|
249
|
+
value: messageObj.value.toString(),
|
|
250
|
+
validAfter: messageObj.validAfter.toString(),
|
|
251
|
+
validBefore: messageObj.validBefore.toString(),
|
|
252
|
+
nonce: messageObj.nonce,
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
return Buffer.from(JSON.stringify(envelope)).toString("base64");
|
|
257
|
+
}
|
|
258
|
+
export async function signX402Payment(args) {
|
|
259
|
+
let requirements;
|
|
260
|
+
try {
|
|
261
|
+
const json = Buffer.from(args.paymentRequiredHeader, "base64").toString("utf-8");
|
|
262
|
+
requirements = JSON.parse(json);
|
|
263
|
+
}
|
|
264
|
+
catch (err) {
|
|
265
|
+
throw new Error(`Could not decode Payment-Required header as base64 JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
266
|
+
}
|
|
267
|
+
if (!requirements.network)
|
|
268
|
+
throw new Error("x402 payment requirements missing 'network'");
|
|
269
|
+
if (!requirements.asset)
|
|
270
|
+
throw new Error("x402 payment requirements missing 'asset'");
|
|
271
|
+
if (!requirements.payTo)
|
|
272
|
+
throw new Error("x402 payment requirements missing 'payTo'");
|
|
273
|
+
if (!requirements.maxAmountRequired)
|
|
274
|
+
throw new Error("x402 payment requirements missing 'maxAmountRequired'");
|
|
275
|
+
if (isSvmNetwork(requirements.network)) {
|
|
276
|
+
if (!args.solanaKey) {
|
|
277
|
+
throw new Error(`Provider requires SVM payment on ${requirements.network}, but MPP32_SOLANA_PRIVATE_KEY is not configured. ` +
|
|
278
|
+
`Set it in your MCP config to enable USDC-on-Solana payments.`);
|
|
279
|
+
}
|
|
280
|
+
const header = await signX402PaymentSvm(requirements, args.solanaKey, args.solanaRpcUrl);
|
|
281
|
+
return {
|
|
282
|
+
xPaymentHeader: header,
|
|
283
|
+
network: requirements.network,
|
|
284
|
+
scheme: requirements.scheme,
|
|
285
|
+
protocolUsed: "x402-svm",
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
if (isEvmNetwork(requirements.network)) {
|
|
289
|
+
if (!args.evmKey) {
|
|
290
|
+
throw new Error(`Provider requires EVM payment on ${requirements.network}, but MPP32_PRIVATE_KEY is not configured. ` +
|
|
291
|
+
`Set it in your MCP config to enable USDC-on-Base payments.`);
|
|
292
|
+
}
|
|
293
|
+
const header = await signX402PaymentEvm(requirements, args.evmKey);
|
|
294
|
+
return {
|
|
295
|
+
xPaymentHeader: header,
|
|
296
|
+
network: requirements.network,
|
|
297
|
+
scheme: requirements.scheme,
|
|
298
|
+
protocolUsed: "x402-evm",
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
if (requirements.network === "" || requirements.network === undefined) {
|
|
302
|
+
throw new Error(`Provider's x402 challenge does not specify a network. We cannot pay it. Ask the provider to fix their challenge.`);
|
|
303
|
+
}
|
|
304
|
+
throw new Error(`Unsupported x402 network "${requirements.network}". Supported: solana:*, base, base-sepolia, ethereum (and their eip155:* aliases). ` +
|
|
305
|
+
`If this network is real and we should support it, file an issue.`);
|
|
306
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mpp32-mcp-server",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"mcpName": "io.github.MPP32/mpp32-mcp-server",
|
|
5
5
|
"description": "Payment layer for AI agents. One MCP, five protocols, thousands of paid APIs your agent can call.",
|
|
6
6
|
"type": "module",
|
|
@@ -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": {
|