mpp32-mcp-server 1.1.1 → 1.1.3
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 +112 -0
- package/dist/index.js +252 -54
- package/package.json +4 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `mpp32-mcp-server` are documented here. The format
|
|
4
|
+
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the
|
|
5
|
+
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [1.1.3] - 2026-05-11
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
* **x402 payments actually work now.** `completeX402Payment` previously
|
|
12
|
+
dynamically imported `@solana/web3.js`, `bs58`, and `tweetnacl`, but
|
|
13
|
+
none of those packages were declared in `dependencies`. On a clean
|
|
14
|
+
`npx mpp32-mcp-server` install they were missing from `node_modules`,
|
|
15
|
+
the import threw, the catch block returned a misleading "check wallet
|
|
16
|
+
balance" message, and Claude paraphrased that as "no funded Solana
|
|
17
|
+
wallet configured" even when `MPP32_SOLANA_PRIVATE_KEY` was set.
|
|
18
|
+
* **Dropped `@solana/web3.js` entirely.** Adding it to `dependencies`
|
|
19
|
+
uncovered a second, deeper bug: its transitive `rpc-websockets`
|
|
20
|
+
bundle is CJS and `require()`s a now ESM-only `uuid`, which throws
|
|
21
|
+
`ERR_REQUIRE_ESM` on Node 20+ the first time the signer loads. We
|
|
22
|
+
only ever used `Keypair.fromSecretKey` and `publicKey.toBase58()`,
|
|
23
|
+
both of which are trivial Ed25519/base58 operations. Signing is now
|
|
24
|
+
done directly with `tweetnacl` + `bs58`, so the failure mode is gone
|
|
25
|
+
and the install footprint is ~30 MB smaller.
|
|
26
|
+
* **Properly declared signing dependencies.** `bs58` and `tweetnacl`
|
|
27
|
+
are now real `dependencies` rather than implicit assumptions.
|
|
28
|
+
* **Better error when a payment dependency genuinely fails to load.**
|
|
29
|
+
The catch around each dynamic import now tells the user the package
|
|
30
|
+
ships with `mpp32-mcp-server` and directs them to upgrade to
|
|
31
|
+
`@latest` instead of suggesting a manual `npm install` that won't
|
|
32
|
+
stick across `npx` runs.
|
|
33
|
+
* **32-byte seed Solana keys now accepted** in addition to 64-byte
|
|
34
|
+
expanded keys (previously `Keypair.fromSecretKey` rejected seeds).
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
|
|
38
|
+
* **Catalog total surfaced in `list_mpp32_services`.** The federated
|
|
39
|
+
catalog has 4,500+ external services but the API returns at most 500
|
|
40
|
+
per call (default 100). The response now includes
|
|
41
|
+
`data.totalAvailable` and a `truncated`/`hint` pair so the model
|
|
42
|
+
knows results were paginated and how to drill in with `q`,
|
|
43
|
+
`category`, `source`, or `protocol`. The MCP client formats the
|
|
44
|
+
header as `Found N services (of M total available in catalog)` and
|
|
45
|
+
prints the hint when applicable.
|
|
46
|
+
|
|
47
|
+
## [1.1.2] - 2026-05-10
|
|
48
|
+
|
|
49
|
+
### Fixed
|
|
50
|
+
|
|
51
|
+
* **Configuration robustness.** Every environment variable is now trimmed,
|
|
52
|
+
stripped of a leading byte-order mark, and unwrapped if the value was
|
|
53
|
+
pasted with literal surrounding quotes. A trailing newline or stray
|
|
54
|
+
whitespace in `MPP32_AGENT_KEY` used to make Node's HTTP client throw
|
|
55
|
+
`ERR_INVALID_CHAR` on every catalog call. That failure mode is now
|
|
56
|
+
impossible.
|
|
57
|
+
* **ASCII-only header guard.** Any non-ASCII byte in a value that would
|
|
58
|
+
end up in an HTTP header is rejected at startup with a clear message
|
|
59
|
+
pointing at the offending variable, instead of a low-level fetch error
|
|
60
|
+
at call time.
|
|
61
|
+
* **Format validation.** `MPP32_AGENT_KEY` must look like
|
|
62
|
+
`mpp32_agent_*`. `MPP32_PRIVATE_KEY` must be a 0x-prefixed 64-character
|
|
63
|
+
EVM hex key. `MPP32_SOLANA_PRIVATE_KEY` is recognized as base58, hex,
|
|
64
|
+
or a JSON byte-array. Malformed values warn at startup with the first
|
|
65
|
+
hex bytes so invisible characters are easy to spot.
|
|
66
|
+
* **Case-insensitive payment challenge parsing.** `Payment-Required` and
|
|
67
|
+
`payment-required` headers are now treated identically.
|
|
68
|
+
* **Liberal `WWW-Authenticate` parser.** Token regex broadened to RFC
|
|
69
|
+
7235 token68 so valid challenges with `+`, `/`, `:`, and `,` no longer
|
|
70
|
+
get silently dropped.
|
|
71
|
+
* **`walletAddress` sanitization.** The optional `walletAddress` argument
|
|
72
|
+
to `get_solana_token_intelligence` is trimmed and ASCII-validated
|
|
73
|
+
before going into the `X-Wallet-Address` header.
|
|
74
|
+
|
|
75
|
+
### Added
|
|
76
|
+
|
|
77
|
+
* **Request timeouts.** Every outbound fetch is wrapped with an
|
|
78
|
+
`AbortController`. Default 30 seconds, configurable through
|
|
79
|
+
`MPP32_TIMEOUT_MS` (1000-300000).
|
|
80
|
+
* **Startup banner.** The server prints the version, API base URL,
|
|
81
|
+
configured keys, and timeout to stderr on start, so misconfigurations
|
|
82
|
+
are visible without making a call.
|
|
83
|
+
|
|
84
|
+
### Internal
|
|
85
|
+
|
|
86
|
+
* `SERVER_VERSION` is now a single constant, so the protocol handshake,
|
|
87
|
+
banner, and `package.json` cannot drift apart.
|
|
88
|
+
|
|
89
|
+
## [1.1.1] - 2026-05-09
|
|
90
|
+
|
|
91
|
+
### Added
|
|
92
|
+
|
|
93
|
+
* `mcpName` field added to `package.json` to satisfy the MCP registry
|
|
94
|
+
publishing requirement.
|
|
95
|
+
* First registry listing under `io.github.MPP32/mpp32-mcp-server` at
|
|
96
|
+
`registry.modelcontextprotocol.io`.
|
|
97
|
+
|
|
98
|
+
## [1.1.0] - 2026-05-09
|
|
99
|
+
|
|
100
|
+
### Added
|
|
101
|
+
|
|
102
|
+
* Three tools: `list_mpp32_services`, `call_mpp32_endpoint`,
|
|
103
|
+
`get_solana_token_intelligence`.
|
|
104
|
+
* End to end support for five payment rails: x402, Tempo, ACP, AP2,
|
|
105
|
+
AGTP. Server picks the rail the wallet is funded for and falls back
|
|
106
|
+
if the first attempt does not settle.
|
|
107
|
+
* Federated catalog (native, curated free, x402 Bazaar, MCP registry).
|
|
108
|
+
* Automatic 402 sign-and-retry through `/api/agent/execute`.
|
|
109
|
+
|
|
110
|
+
## [1.0.0] - 2026-05-08
|
|
111
|
+
|
|
112
|
+
* Initial public release on npm.
|
package/dist/index.js
CHANGED
|
@@ -2,20 +2,165 @@
|
|
|
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
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
|
|
5
|
+
const SERVER_VERSION = "1.1.3";
|
|
6
|
+
// ── Env loading: trim and sanitize aggressively ─────────────────────────────
|
|
7
|
+
// Copy-paste from Claude Desktop / Cursor / Windsurf JSON config UIs frequently
|
|
8
|
+
// adds trailing \n, \r, NBSP, BOM, or wraps the value in literal quotes. Any
|
|
9
|
+
// non-ASCII byte (including a stray newline) in a value that ends up in an
|
|
10
|
+
// HTTP header makes Node's undici fetch throw ERR_INVALID_CHAR, which used to
|
|
11
|
+
// surface as a confusing "invalid byte character" error on every catalog call.
|
|
12
|
+
function readEnv(name) {
|
|
13
|
+
const raw = process.env[name];
|
|
14
|
+
if (raw === undefined)
|
|
15
|
+
return undefined;
|
|
16
|
+
let v = raw;
|
|
17
|
+
if (v.charCodeAt(0) === 0xfeff)
|
|
18
|
+
v = v.slice(1);
|
|
19
|
+
v = v.trim();
|
|
20
|
+
if (v.length >= 2 &&
|
|
21
|
+
((v.startsWith('"') && v.endsWith('"')) ||
|
|
22
|
+
(v.startsWith("'") && v.endsWith("'")))) {
|
|
23
|
+
v = v.slice(1, -1).trim();
|
|
24
|
+
}
|
|
25
|
+
if (v.length === 0)
|
|
26
|
+
return undefined;
|
|
27
|
+
return v;
|
|
28
|
+
}
|
|
29
|
+
function isPrintableAscii(v) {
|
|
30
|
+
for (let i = 0; i < v.length; i++) {
|
|
31
|
+
const c = v.charCodeAt(i);
|
|
32
|
+
if (c < 0x20 || c > 0x7e)
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
function safeHeaderValue(name, value) {
|
|
38
|
+
if (!isPrintableAscii(value)) {
|
|
39
|
+
throw new Error(`Environment variable contains non-printable or non-ASCII characters that cannot be sent as an HTTP header (${name}). ` +
|
|
40
|
+
`Re-copy the value from https://mpp32.org/agent-console without any surrounding whitespace, quotes, or newlines.`);
|
|
41
|
+
}
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
function describeEnvProblem(name, raw, expected) {
|
|
45
|
+
const hex = Array.from(raw.slice(0, 4))
|
|
46
|
+
.map((c) => c.charCodeAt(0).toString(16).padStart(2, "0"))
|
|
47
|
+
.join(" ");
|
|
48
|
+
return `${name} looks malformed. Expected ${expected}. First bytes: 0x${hex}. Re-copy from ${API_URL}/agent-console.`;
|
|
49
|
+
}
|
|
50
|
+
const RAW_API_URL = readEnv("MPP32_API_URL") ?? "https://mpp32.org";
|
|
51
|
+
const API_URL = (() => {
|
|
52
|
+
try {
|
|
53
|
+
const u = new URL(RAW_API_URL.replace(/\/+$/, ""));
|
|
54
|
+
if (u.protocol !== "https:" && u.protocol !== "http:") {
|
|
55
|
+
throw new Error(`MPP32_API_URL must be http(s), got ${u.protocol}`);
|
|
56
|
+
}
|
|
57
|
+
return u.toString().replace(/\/+$/, "");
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
console.error(`[mpp32] MPP32_API_URL is not a valid URL: ${err instanceof Error ? err.message : String(err)}. ` +
|
|
61
|
+
`Falling back to https://mpp32.org.`);
|
|
62
|
+
return "https://mpp32.org";
|
|
63
|
+
}
|
|
64
|
+
})();
|
|
65
|
+
// Default request timeout. Configurable via MPP32_TIMEOUT_MS.
|
|
66
|
+
const TIMEOUT_MS = (() => {
|
|
67
|
+
const raw = readEnv("MPP32_TIMEOUT_MS");
|
|
68
|
+
if (!raw)
|
|
69
|
+
return 30_000;
|
|
70
|
+
const n = Number.parseInt(raw, 10);
|
|
71
|
+
if (!Number.isFinite(n) || n < 1_000 || n > 300_000) {
|
|
72
|
+
console.error(`[mpp32] MPP32_TIMEOUT_MS=${raw} out of range. Using 30000ms.`);
|
|
73
|
+
return 30_000;
|
|
74
|
+
}
|
|
75
|
+
return n;
|
|
76
|
+
})();
|
|
77
|
+
// MPP32_AGENT_KEY is canonical; MPP32_API_KEY is an accepted alias from older docs.
|
|
78
|
+
const AGENT_KEY = (() => {
|
|
79
|
+
const v = readEnv("MPP32_AGENT_KEY") ?? readEnv("MPP32_API_KEY");
|
|
80
|
+
if (!v)
|
|
81
|
+
return undefined;
|
|
82
|
+
if (!isPrintableAscii(v)) {
|
|
83
|
+
console.error(`[mpp32] MPP32_AGENT_KEY contains non-ASCII characters and will be ignored. ` +
|
|
84
|
+
`Re-copy the key from ${API_URL}/agent-console.`);
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
if (!/^mpp32_agent_[A-Za-z0-9_-]+$/.test(v)) {
|
|
88
|
+
console.error(`[mpp32] ${describeEnvProblem("MPP32_AGENT_KEY", v, "a value starting with 'mpp32_agent_'")}`);
|
|
89
|
+
}
|
|
90
|
+
return v;
|
|
91
|
+
})();
|
|
92
|
+
const PRIVATE_KEY = (() => {
|
|
93
|
+
const v = readEnv("MPP32_PRIVATE_KEY");
|
|
94
|
+
if (!v)
|
|
95
|
+
return undefined;
|
|
96
|
+
if (!isPrintableAscii(v)) {
|
|
97
|
+
console.error(`[mpp32] MPP32_PRIVATE_KEY contains non-ASCII characters and will be ignored. ` +
|
|
98
|
+
`Re-paste the hex key (0x-prefixed or 64 hex chars).`);
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
if (!/^(0x)?[0-9a-fA-F]{64}$/.test(v)) {
|
|
102
|
+
console.error(`[mpp32] ${describeEnvProblem("MPP32_PRIVATE_KEY", v, "0x-prefixed 64-hex-char EVM private key")}`);
|
|
103
|
+
}
|
|
104
|
+
return v;
|
|
105
|
+
})();
|
|
106
|
+
const SOLANA_PRIVATE_KEY = (() => {
|
|
107
|
+
const v = readEnv("MPP32_SOLANA_PRIVATE_KEY");
|
|
108
|
+
if (!v)
|
|
109
|
+
return undefined;
|
|
110
|
+
if (!isPrintableAscii(v)) {
|
|
111
|
+
console.error(`[mpp32] MPP32_SOLANA_PRIVATE_KEY contains non-ASCII characters and will be ignored. ` +
|
|
112
|
+
`Re-paste the base58 (or [byte,byte,...] array, or hex) key.`);
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
const looksValid = v.startsWith("[") ||
|
|
116
|
+
/^[0-9a-fA-F]+$/.test(v) ||
|
|
117
|
+
/^[1-9A-HJ-NP-Za-km-z]{43,90}$/.test(v); // base58
|
|
118
|
+
if (!looksValid) {
|
|
119
|
+
console.error(`[mpp32] ${describeEnvProblem("MPP32_SOLANA_PRIVATE_KEY", v, "base58 string, hex string, or [byte,byte,...] array")}`);
|
|
120
|
+
}
|
|
121
|
+
return v;
|
|
122
|
+
})();
|
|
123
|
+
// Wrap fetch with a default timeout. AbortSignal.timeout exists in Node 20+,
|
|
124
|
+
// but we ship for Node 18+, so we build the signal ourselves.
|
|
125
|
+
async function fetchWithTimeout(url, init) {
|
|
126
|
+
const controller = new AbortController();
|
|
127
|
+
const timer = setTimeout(() => controller.abort(), init?.timeoutMs ?? TIMEOUT_MS);
|
|
128
|
+
try {
|
|
129
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
133
|
+
throw new Error(`Request to ${url} timed out after ${init?.timeoutMs ?? TIMEOUT_MS}ms. ` +
|
|
134
|
+
`Set MPP32_TIMEOUT_MS in your MCP config to extend.`);
|
|
135
|
+
}
|
|
136
|
+
throw err;
|
|
137
|
+
}
|
|
138
|
+
finally {
|
|
139
|
+
clearTimeout(timer);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Lowercase all keys in a headers-like object. The backend may emit
|
|
143
|
+
// "Payment-Required" or "payment-required"; downstream code must not care.
|
|
144
|
+
function lowercaseHeaderKeys(obj) {
|
|
145
|
+
if (!obj)
|
|
146
|
+
return {};
|
|
147
|
+
const out = {};
|
|
148
|
+
for (const [k, v] of Object.entries(obj))
|
|
149
|
+
out[k.toLowerCase()] = v;
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
11
152
|
const server = new McpServer({
|
|
12
153
|
name: "mpp32",
|
|
13
|
-
version:
|
|
154
|
+
version: SERVER_VERSION,
|
|
14
155
|
});
|
|
15
156
|
function buildHeaders(extra = {}) {
|
|
16
|
-
const headers = {
|
|
17
|
-
|
|
18
|
-
headers[
|
|
157
|
+
const headers = {};
|
|
158
|
+
for (const [k, v] of Object.entries(extra)) {
|
|
159
|
+
headers[k] = safeHeaderValue(k, v);
|
|
160
|
+
}
|
|
161
|
+
if (AGENT_KEY) {
|
|
162
|
+
headers["X-Agent-Key"] = safeHeaderValue("MPP32_AGENT_KEY", AGENT_KEY);
|
|
163
|
+
}
|
|
19
164
|
return headers;
|
|
20
165
|
}
|
|
21
166
|
function isHttpCallable(svc) {
|
|
@@ -29,7 +174,7 @@ function isHttpCallable(svc) {
|
|
|
29
174
|
return /^https?:\/\//.test(url);
|
|
30
175
|
}
|
|
31
176
|
// ── 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.
|
|
177
|
+
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.", {
|
|
33
178
|
category: z
|
|
34
179
|
.string()
|
|
35
180
|
.optional()
|
|
@@ -59,7 +204,7 @@ server.tool("list_mpp32_services", "Browse the MPP32 federated catalog of machin
|
|
|
59
204
|
if (source)
|
|
60
205
|
url.searchParams.set("source", source);
|
|
61
206
|
url.searchParams.set("limit", String(limit ?? 100));
|
|
62
|
-
const res = await
|
|
207
|
+
const res = await fetchWithTimeout(url.toString(), { headers: buildHeaders() });
|
|
63
208
|
if (!res.ok) {
|
|
64
209
|
return {
|
|
65
210
|
content: [
|
|
@@ -106,17 +251,24 @@ server.tool("list_mpp32_services", "Browse the MPP32 federated catalog of machin
|
|
|
106
251
|
.join("\n");
|
|
107
252
|
});
|
|
108
253
|
const counts = json.data.counts;
|
|
254
|
+
const totalAvailable = json.data.totalAvailable;
|
|
109
255
|
const callableCount = services.filter(isHttpCallable).length;
|
|
256
|
+
const sourcesLine = totalAvailable
|
|
257
|
+
? `**Sources:** ${counts.native} native + ${counts.external} external (of ${totalAvailable.combined} total available in catalog). **Callable through this MCP:** ${callableCount}.`
|
|
258
|
+
: `**Sources:** ${counts.native} native + ${counts.external} external. **Callable through this MCP:** ${callableCount}.`;
|
|
110
259
|
const header = [
|
|
111
260
|
`# MPP32 Federated Catalog — ${services.length} result${services.length !== 1 ? "s" : ""}`,
|
|
112
261
|
``,
|
|
113
|
-
|
|
262
|
+
sourcesLine,
|
|
263
|
+
json.data.truncated && json.data.hint ? `\n> ⚠️ ${json.data.hint}` : ``,
|
|
114
264
|
``,
|
|
115
265
|
AGENT_KEY
|
|
116
266
|
? `Calls through \`call_mpp32_endpoint\` are tracked in your dashboard at ${API_URL}/agent-console (your X-Agent-Key is set).`
|
|
117
267
|
: `**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
268
|
``,
|
|
119
|
-
]
|
|
269
|
+
]
|
|
270
|
+
.filter((l) => l !== ``)
|
|
271
|
+
.join("\n");
|
|
120
272
|
return {
|
|
121
273
|
content: [{ type: "text", text: header + "\n" + lines.join("\n\n") }],
|
|
122
274
|
};
|
|
@@ -194,7 +346,7 @@ async function callViaAgentExecute(service, method, body, query) {
|
|
|
194
346
|
...(query ? { query } : {}),
|
|
195
347
|
});
|
|
196
348
|
// Round 1: no payment headers
|
|
197
|
-
const firstRes = await
|
|
349
|
+
const firstRes = await fetchWithTimeout(execUrl, {
|
|
198
350
|
method: "POST",
|
|
199
351
|
headers: buildHeaders({ "Content-Type": "application/json" }),
|
|
200
352
|
body: reqBody,
|
|
@@ -231,7 +383,7 @@ function detectPaymentRequired(resp) {
|
|
|
231
383
|
const result = resp?.data?.result;
|
|
232
384
|
if (!result?.error || result.error.code !== "PAYMENT_REQUIRED")
|
|
233
385
|
return null;
|
|
234
|
-
const headers = result.error.challenge?.headers
|
|
386
|
+
const headers = lowercaseHeaderKeys(result.error.challenge?.headers);
|
|
235
387
|
return {
|
|
236
388
|
wwwAuthenticate: headers["www-authenticate"],
|
|
237
389
|
paymentRequired: headers["payment-required"],
|
|
@@ -308,7 +460,7 @@ async function signAndRetry(execUrl, reqBody, challenge) {
|
|
|
308
460
|
};
|
|
309
461
|
}
|
|
310
462
|
// Round 2: with payment headers
|
|
311
|
-
const secondRes = await
|
|
463
|
+
const secondRes = await fetchWithTimeout(execUrl, {
|
|
312
464
|
method: "POST",
|
|
313
465
|
headers: buildHeaders({ "Content-Type": "application/json", ...paymentHeaders }),
|
|
314
466
|
body: reqBody,
|
|
@@ -437,7 +589,9 @@ function paymentKeyMissingMessage(resp, challenge) {
|
|
|
437
589
|
' "command": "npx",',
|
|
438
590
|
' "args": ["mpp32-mcp-server"],',
|
|
439
591
|
' "env": {',
|
|
440
|
-
AGENT_KEY
|
|
592
|
+
AGENT_KEY
|
|
593
|
+
? ` "MPP32_AGENT_KEY": "${AGENT_KEY.slice(0, 12).replace(/[^A-Za-z0-9_-]/g, "?")}…",`
|
|
594
|
+
: "",
|
|
441
595
|
' "MPP32_SOLANA_PRIVATE_KEY": "<solana-base58-key for USDC>",',
|
|
442
596
|
' "MPP32_PRIVATE_KEY": "<EVM-hex-key for pathUSD>"',
|
|
443
597
|
" }",
|
|
@@ -477,7 +631,7 @@ async function callViaLegacyProxy(slug, method, body, query) {
|
|
|
477
631
|
// Without an agent key, only native /api/proxy/<slug> is reachable.
|
|
478
632
|
// We fetch /info first to detect that the slug exists as a native service.
|
|
479
633
|
const infoUrl = new URL(`/api/proxy/${encodeURIComponent(slug)}/info`, API_URL).toString();
|
|
480
|
-
const infoRes = await
|
|
634
|
+
const infoRes = await fetchWithTimeout(infoUrl);
|
|
481
635
|
if (!infoRes.ok) {
|
|
482
636
|
return {
|
|
483
637
|
content: [
|
|
@@ -500,7 +654,7 @@ async function callViaLegacyProxy(slug, method, body, query) {
|
|
|
500
654
|
const baseHeaders = { Accept: "application/json" };
|
|
501
655
|
if (body !== undefined)
|
|
502
656
|
baseHeaders["Content-Type"] = "application/json";
|
|
503
|
-
const challengeRes = await
|
|
657
|
+
const challengeRes = await fetchWithTimeout(proxyUrl.toString(), {
|
|
504
658
|
method,
|
|
505
659
|
headers: baseHeaders,
|
|
506
660
|
body: method !== "GET" && body !== undefined ? JSON.stringify(body) : undefined,
|
|
@@ -592,7 +746,7 @@ async function callViaLegacyProxy(slug, method, body, query) {
|
|
|
592
746
|
],
|
|
593
747
|
};
|
|
594
748
|
}
|
|
595
|
-
const paidRes = await
|
|
749
|
+
const paidRes = await fetchWithTimeout(proxyUrl.toString(), {
|
|
596
750
|
method,
|
|
597
751
|
headers: { ...baseHeaders, ...paymentHeaders },
|
|
598
752
|
body: method !== "GET" && body !== undefined ? JSON.stringify(body) : undefined,
|
|
@@ -628,9 +782,21 @@ async function callViaLegacyProxy(slug, method, body, query) {
|
|
|
628
782
|
async function legacyIntelligenceCall(token, walletAddress) {
|
|
629
783
|
try {
|
|
630
784
|
const reqHeaders = { "Content-Type": "application/json" };
|
|
631
|
-
if (walletAddress)
|
|
632
|
-
|
|
633
|
-
|
|
785
|
+
if (walletAddress) {
|
|
786
|
+
const trimmed = walletAddress.trim();
|
|
787
|
+
if (!isPrintableAscii(trimmed)) {
|
|
788
|
+
return {
|
|
789
|
+
content: [
|
|
790
|
+
{
|
|
791
|
+
type: "text",
|
|
792
|
+
text: `walletAddress contains non-ASCII characters. Pass a Solana base58 address only.`,
|
|
793
|
+
},
|
|
794
|
+
],
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
reqHeaders["X-Wallet-Address"] = trimmed;
|
|
798
|
+
}
|
|
799
|
+
const res = await fetchWithTimeout(`${API_URL}/api/intelligence`, {
|
|
634
800
|
method: "POST",
|
|
635
801
|
headers: reqHeaders,
|
|
636
802
|
body: JSON.stringify({ token }),
|
|
@@ -723,7 +889,7 @@ async function legacyIntelligenceCall(token, walletAddress) {
|
|
|
723
889
|
};
|
|
724
890
|
}
|
|
725
891
|
}
|
|
726
|
-
const paidRes = await
|
|
892
|
+
const paidRes = await fetchWithTimeout(`${API_URL}/api/intelligence`, {
|
|
727
893
|
method: "POST",
|
|
728
894
|
headers: { ...reqHeaders, ...paymentHeaders },
|
|
729
895
|
body: JSON.stringify({ token }),
|
|
@@ -762,13 +928,19 @@ function parseWwwAuthenticate(header) {
|
|
|
762
928
|
const match = header.match(/^(\w+)\s+(.+)$/);
|
|
763
929
|
if (!match)
|
|
764
930
|
return { scheme: null, params: {} };
|
|
765
|
-
const scheme = match[1];
|
|
766
|
-
const rest = match[2];
|
|
931
|
+
const scheme = match[1] ?? null;
|
|
932
|
+
const rest = match[2] ?? "";
|
|
767
933
|
const params = {};
|
|
768
|
-
|
|
934
|
+
// Tokens per RFC 7235: quoted-string OR a token68-ish value covering all
|
|
935
|
+
// base64url, base58, hex, JSON-pointers, etc. Liberal on purpose so we do
|
|
936
|
+
// not silently drop valid challenges.
|
|
937
|
+
const paramRegex = /([A-Za-z0-9_-]+)=(?:"((?:[^"\\]|\\.)*)"|([^\s,]+))/g;
|
|
769
938
|
let m;
|
|
770
939
|
while ((m = paramRegex.exec(rest)) !== null) {
|
|
771
|
-
|
|
940
|
+
const key = m[1];
|
|
941
|
+
const val = m[2] ?? m[3];
|
|
942
|
+
if (key && val !== undefined)
|
|
943
|
+
params[key] = val;
|
|
772
944
|
}
|
|
773
945
|
return { scheme, params };
|
|
774
946
|
}
|
|
@@ -805,40 +977,70 @@ async function completeX402Payment(paymentRequiredHeader, solanaPrivateKey) {
|
|
|
805
977
|
catch {
|
|
806
978
|
throw new Error("Could not decode Payment-Required header");
|
|
807
979
|
}
|
|
980
|
+
// We sign x402 challenges with raw Ed25519 (Solana keys are Ed25519). No
|
|
981
|
+
// @solana/web3.js needed — it pulls in rpc-websockets which has a
|
|
982
|
+
// CJS/ESM uuid incompat on Node 20+ that breaks every paid call.
|
|
808
983
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
809
|
-
let
|
|
984
|
+
let tweetnacl;
|
|
810
985
|
try {
|
|
811
|
-
const pkg = "
|
|
812
|
-
|
|
986
|
+
const pkg = "tweetnacl";
|
|
987
|
+
tweetnacl = await import(pkg);
|
|
813
988
|
}
|
|
814
|
-
catch {
|
|
815
|
-
throw new Error(
|
|
989
|
+
catch (err) {
|
|
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)}`);
|
|
816
993
|
}
|
|
817
994
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
818
|
-
let
|
|
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;
|
|
819
1009
|
try {
|
|
820
1010
|
if (solanaPrivateKey.startsWith("[")) {
|
|
821
|
-
|
|
1011
|
+
rawKey = new Uint8Array(JSON.parse(solanaPrivateKey));
|
|
822
1012
|
}
|
|
823
1013
|
else if (/^[0-9a-fA-F]+$/.test(solanaPrivateKey) && solanaPrivateKey.length % 2 === 0) {
|
|
824
|
-
|
|
1014
|
+
rawKey = new Uint8Array(Buffer.from(solanaPrivateKey, "hex"));
|
|
825
1015
|
}
|
|
826
1016
|
else {
|
|
827
|
-
|
|
828
|
-
const bs58 = await import(bs58Pkg);
|
|
829
|
-
keypair = solanaWeb3.Keypair.fromSecretKey(bs58.default.decode(solanaPrivateKey));
|
|
1017
|
+
rawKey = bs58Decode(solanaPrivateKey);
|
|
830
1018
|
}
|
|
831
1019
|
}
|
|
832
1020
|
catch (err) {
|
|
833
1021
|
throw new Error(`Could not decode Solana private key: ${err instanceof Error ? err.message : String(err)}`);
|
|
834
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
|
+
}
|
|
835
1037
|
const payload = {
|
|
836
1038
|
x402Version: 1,
|
|
837
1039
|
scheme: requirements.scheme ?? "exact",
|
|
838
1040
|
network: requirements.network ?? "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
|
|
839
1041
|
payload: {
|
|
840
1042
|
signature: "",
|
|
841
|
-
from:
|
|
1043
|
+
from: bs58Encode(publicKey),
|
|
842
1044
|
amount: requirements.maxAmountRequired,
|
|
843
1045
|
asset: requirements.asset,
|
|
844
1046
|
payTo: requirements.payTo,
|
|
@@ -847,17 +1049,7 @@ async function completeX402Payment(paymentRequiredHeader, solanaPrivateKey) {
|
|
|
847
1049
|
};
|
|
848
1050
|
const message = JSON.stringify(payload.payload);
|
|
849
1051
|
const messageBytes = new TextEncoder().encode(message);
|
|
850
|
-
|
|
851
|
-
let tweetnacl;
|
|
852
|
-
try {
|
|
853
|
-
const pkg = "tweetnacl";
|
|
854
|
-
tweetnacl = await import(pkg);
|
|
855
|
-
}
|
|
856
|
-
catch {
|
|
857
|
-
throw new Error("x402 signing requires tweetnacl: npm install tweetnacl");
|
|
858
|
-
}
|
|
859
|
-
const naclSign = tweetnacl.default?.sign ?? tweetnacl.sign;
|
|
860
|
-
const signed = naclSign.detached(messageBytes, keypair.secretKey);
|
|
1052
|
+
const signed = naclSign.detached(messageBytes, secretKey);
|
|
861
1053
|
payload.payload.signature = Buffer.from(signed).toString("base64");
|
|
862
1054
|
return Buffer.from(JSON.stringify(payload)).toString("base64");
|
|
863
1055
|
}
|
|
@@ -865,8 +1057,14 @@ async function completeX402Payment(paymentRequiredHeader, solanaPrivateKey) {
|
|
|
865
1057
|
async function main() {
|
|
866
1058
|
const transport = new StdioServerTransport();
|
|
867
1059
|
await server.connect(transport);
|
|
868
|
-
const
|
|
869
|
-
|
|
1060
|
+
const features = [
|
|
1061
|
+
AGENT_KEY ? "agent-key" : null,
|
|
1062
|
+
SOLANA_PRIVATE_KEY ? "x402-key" : null,
|
|
1063
|
+
PRIVATE_KEY ? "tempo-key" : null,
|
|
1064
|
+
]
|
|
1065
|
+
.filter(Boolean)
|
|
1066
|
+
.join(", ") || "no keys (catalog-only legacy mode)";
|
|
1067
|
+
console.error(`[mpp32] MCP server v${SERVER_VERSION} on stdio. API ${API_URL}. Configured: ${features}. Timeout ${TIMEOUT_MS}ms.`);
|
|
870
1068
|
}
|
|
871
1069
|
main().catch((err) => {
|
|
872
1070
|
console.error("Fatal:", err);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mpp32-mcp-server",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.3",
|
|
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",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"dist/**/*.js",
|
|
20
20
|
"dist/**/*.d.ts",
|
|
21
21
|
"README.md",
|
|
22
|
+
"CHANGELOG.md",
|
|
22
23
|
"LICENSE"
|
|
23
24
|
],
|
|
24
25
|
"sideEffects": false,
|
|
@@ -68,6 +69,8 @@
|
|
|
68
69
|
},
|
|
69
70
|
"dependencies": {
|
|
70
71
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
72
|
+
"bs58": "^6.0.0",
|
|
73
|
+
"tweetnacl": "^1.0.3",
|
|
71
74
|
"zod": "^3.23.0"
|
|
72
75
|
},
|
|
73
76
|
"peerDependencies": {
|