mercury402-mcp 0.1.2 → 0.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/dist/client.js +11 -2
- package/dist/http.js +35 -14
- package/dist/server.js +14 -7
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -17,6 +17,15 @@ const HOW_TO_PAY = [
|
|
|
17
17
|
'Sign an EIP-3009 transferWithAuthorization for the quoted amount to pay_to, base64-encode an x402 PaymentPayload, and resend the same request with a PAYMENT-SIGNATURE header. Any x402-compatible client/wallet can do this.',
|
|
18
18
|
'Or enable paid mode in this MCP server: set MERCURY402_PAYER_PRIVATE_KEY to a funded Base hot wallet (and optionally MERCURY402_MAX_PRICE_USD), then call get_endpoint_data again.',
|
|
19
19
|
];
|
|
20
|
+
// A hosted (publicly exposed) server refuses to boot with a payer key, and a remote caller
|
|
21
|
+
// cannot set its env vars, so it must not suggest enabling paid mode on itself.
|
|
22
|
+
export const HOSTED_HOW_TO_PAY = [
|
|
23
|
+
HOW_TO_PAY[0],
|
|
24
|
+
HOW_TO_PAY[1],
|
|
25
|
+
'To pay automatically, run mercury402-mcp locally (npx -y mercury402-mcp) with MERCURY402_PAYER_PRIVATE_KEY set to a funded Base hot wallet you control.',
|
|
26
|
+
];
|
|
27
|
+
export const LOCAL_NO_PAYER_REASON = 'Payment required. Paid mode is disabled (MERCURY402_PAYER_PRIVATE_KEY not set).';
|
|
28
|
+
export const HOSTED_NO_PAYER_REASON = 'Payment required. This hosted endpoint is discovery-only and never pays on your behalf.';
|
|
20
29
|
async function readBody(res) {
|
|
21
30
|
const text = await res.text();
|
|
22
31
|
if (!text)
|
|
@@ -63,7 +72,7 @@ export class MercuryClient {
|
|
|
63
72
|
reason,
|
|
64
73
|
quote: payable ? toQuote(payable) : options[0],
|
|
65
74
|
all_options: options,
|
|
66
|
-
how_to_pay: HOW_TO_PAY,
|
|
75
|
+
how_to_pay: this.opts.hosted ? HOSTED_HOW_TO_PAY : HOW_TO_PAY,
|
|
67
76
|
raw_body: body,
|
|
68
77
|
};
|
|
69
78
|
}
|
|
@@ -84,7 +93,7 @@ export class MercuryClient {
|
|
|
84
93
|
const reqs = parsePaymentRequired(first.headers, firstBody);
|
|
85
94
|
const payer = this.opts.payer;
|
|
86
95
|
if (!payer) {
|
|
87
|
-
return this.paymentRequired(url,
|
|
96
|
+
return this.paymentRequired(url, this.opts.hosted ? HOSTED_NO_PAYER_REASON : LOCAL_NO_PAYER_REASON, reqs, firstBody);
|
|
88
97
|
}
|
|
89
98
|
if (!pay) {
|
|
90
99
|
return this.paymentRequired(url, 'Payment required. Not paying because pay=false was requested.', reqs, firstBody);
|
package/dist/http.js
CHANGED
|
@@ -28,6 +28,35 @@ export function hostnameOf(hostHeader) {
|
|
|
28
28
|
}
|
|
29
29
|
return h.split(':')[0];
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* The first setting that makes the HTTP server publicly reachable, or undefined when it is
|
|
33
|
+
* loopback-only. Single source of truth for "public exposure": the paid-mode boot guard
|
|
34
|
+
* refuses to start on it, and the caller-facing payment text switches to hosted wording.
|
|
35
|
+
*/
|
|
36
|
+
export function publicExposure(config) {
|
|
37
|
+
if (!LOOPBACK_BINDS.has(config.httpHost))
|
|
38
|
+
return { kind: 'bind', host: config.httpHost };
|
|
39
|
+
const publicHost = config.httpAllowedHosts.map(hostnameOf).find((h) => !LOOPBACK_HOSTNAMES.includes(h));
|
|
40
|
+
if (publicHost)
|
|
41
|
+
return { kind: 'allowed_host', host: publicHost };
|
|
42
|
+
if (config.httpTrustProxy)
|
|
43
|
+
return { kind: 'trust_proxy' };
|
|
44
|
+
if (config.publicUrl)
|
|
45
|
+
return { kind: 'public_url' };
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
function paidModeRefusal(exposure) {
|
|
49
|
+
switch (exposure.kind) {
|
|
50
|
+
case 'bind':
|
|
51
|
+
return 'Refusing to serve paid mode over HTTP on a non-loopback host (MCP_HTTP_HOST). Use stdio or 127.0.0.1.';
|
|
52
|
+
case 'allowed_host':
|
|
53
|
+
return `Refusing to serve paid mode with a public MCP_HTTP_ALLOWED_HOSTS entry (${exposure.host}): anyone who can reach it could spend the payer wallet.`;
|
|
54
|
+
case 'trust_proxy':
|
|
55
|
+
return 'Refusing to serve paid mode with MCP_HTTP_TRUST_PROXY: a proxy in front of the server means others can reach it.';
|
|
56
|
+
case 'public_url':
|
|
57
|
+
return 'Refusing to serve paid mode with MCP_PUBLIC_URL set: a public endpoint must run in free mode.';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
31
60
|
/**
|
|
32
61
|
* Paid mode signs payments from a hot wallet, and HTTP has no auth: anyone who can
|
|
33
62
|
* reach the endpoint could spend it. So paid mode is allowed only on a loopback bind
|
|
@@ -36,19 +65,9 @@ export function hostnameOf(hostHeader) {
|
|
|
36
65
|
export function assertSafeHttpConfig(config, paid = !!config.payerPrivateKey) {
|
|
37
66
|
if (!paid)
|
|
38
67
|
return;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const publicHost = config.httpAllowedHosts.map(hostnameOf).find((h) => !LOOPBACK_HOSTNAMES.includes(h));
|
|
43
|
-
if (publicHost) {
|
|
44
|
-
throw new Error(`Refusing to serve paid mode with a public MCP_HTTP_ALLOWED_HOSTS entry (${publicHost}): anyone who can reach it could spend the payer wallet.`);
|
|
45
|
-
}
|
|
46
|
-
if (config.httpTrustProxy) {
|
|
47
|
-
throw new Error('Refusing to serve paid mode with MCP_HTTP_TRUST_PROXY: a proxy in front of the server means others can reach it.');
|
|
48
|
-
}
|
|
49
|
-
if (config.publicUrl) {
|
|
50
|
-
throw new Error('Refusing to serve paid mode with MCP_PUBLIC_URL set: a public endpoint must run in free mode.');
|
|
51
|
-
}
|
|
68
|
+
const exposure = publicExposure(config);
|
|
69
|
+
if (exposure)
|
|
70
|
+
throw new Error(paidModeRefusal(exposure));
|
|
52
71
|
}
|
|
53
72
|
/** Fixed-window counter per key. take() returns 0 when allowed, else seconds until the window resets. */
|
|
54
73
|
export class FixedWindowLimiter {
|
|
@@ -128,6 +147,8 @@ export function createHttpServer(config, options = {}) {
|
|
|
128
147
|
const deps = options.deps ?? {};
|
|
129
148
|
const paid = !!(deps.payer ?? config.payerPrivateKey);
|
|
130
149
|
assertSafeHttpConfig(config, paid);
|
|
150
|
+
// Same predicate as the boot guard: a publicly reachable server tells callers it never pays.
|
|
151
|
+
const hosted = publicExposure(config) !== undefined;
|
|
131
152
|
const log = options.log ?? (() => { });
|
|
132
153
|
const allowedHosts = new Set([...LOOPBACK_HOSTNAMES, ...config.httpAllowedHosts.map(hostnameOf)]);
|
|
133
154
|
const perIp = new FixedWindowLimiter(config.httpRateLimitPerMin, 60_000, options.now);
|
|
@@ -167,7 +188,7 @@ export function createHttpServer(config, options = {}) {
|
|
|
167
188
|
return send(res, 400, rpcError(-32700, 'Parse error'));
|
|
168
189
|
}
|
|
169
190
|
stats.mcp_requests++;
|
|
170
|
-
const server = createMercuryServer(config, deps);
|
|
191
|
+
const server = createMercuryServer(config, deps, { hosted });
|
|
171
192
|
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, enableJsonResponse: true });
|
|
172
193
|
res.on('close', () => {
|
|
173
194
|
void transport.close();
|
package/dist/server.js
CHANGED
|
@@ -4,7 +4,17 @@ import { CATALOG, resolveRequest } from './catalog.js';
|
|
|
4
4
|
import { MercuryClient } from './client.js';
|
|
5
5
|
import { createEvmPayer } from './payment.js';
|
|
6
6
|
export const SERVER_NAME = 'mercury402';
|
|
7
|
-
export const SERVER_VERSION = '0.1.
|
|
7
|
+
export const SERVER_VERSION = '0.1.3';
|
|
8
|
+
const GET_ENDPOINT_DATA_USAGE = 'Use list_endpoints first to find paths and parameters. Path params can be inline ("/v1/fred/UNRATE") or passed in params ' +
|
|
9
|
+
'("/v1/fred/{series_id}" + {"series_id":"UNRATE"}). Other params go to the query string (GET) or JSON body (POST).';
|
|
10
|
+
export const LOCAL_GET_ENDPOINT_DATA_DESCRIPTION = 'Call a Mercury402 endpoint on the live API. Without paid mode, endpoints return HTTP 402 and this tool returns ' +
|
|
11
|
+
'the price and x402 payment instructions instead of data. With paid mode enabled (MERCURY402_PAYER_PRIVATE_KEY), ' +
|
|
12
|
+
'it pays in USDC on Base (capped by MERCURY402_MAX_PRICE_USD) and returns the data. ' +
|
|
13
|
+
GET_ENDPOINT_DATA_USAGE;
|
|
14
|
+
export const HOSTED_GET_ENDPOINT_DATA_DESCRIPTION = 'Call a Mercury402 endpoint on the live API. This hosted endpoint is discovery-only: endpoints return HTTP 402 and this ' +
|
|
15
|
+
'tool returns the price and x402 payment instructions instead of data. Pay with your own x402 client, or run ' +
|
|
16
|
+
'mercury402-mcp locally (npx -y mercury402-mcp) with your own wallet. ' +
|
|
17
|
+
GET_ENDPOINT_DATA_USAGE;
|
|
8
18
|
function json(value) {
|
|
9
19
|
return JSON.stringify(value, null, 2);
|
|
10
20
|
}
|
|
@@ -26,7 +36,7 @@ function formatCallResult(result) {
|
|
|
26
36
|
return textResult(`ERROR (HTTP ${result.http_status}) ${result.url}\n\n${json(result)}`, true);
|
|
27
37
|
}
|
|
28
38
|
}
|
|
29
|
-
export function createMercuryServer(config, deps = {}) {
|
|
39
|
+
export function createMercuryServer(config, deps = {}, options = {}) {
|
|
30
40
|
const catalog = deps.catalog ?? CATALOG;
|
|
31
41
|
const payer = deps.payer ?? (config.payerPrivateKey ? createEvmPayer(config.payerPrivateKey) : undefined);
|
|
32
42
|
const client = new MercuryClient({
|
|
@@ -35,6 +45,7 @@ export function createMercuryServer(config, deps = {}) {
|
|
|
35
45
|
payer,
|
|
36
46
|
maxPriceUsd: config.maxPriceUsd,
|
|
37
47
|
timeoutMs: config.timeoutMs,
|
|
48
|
+
hosted: options.hosted,
|
|
38
49
|
});
|
|
39
50
|
const categories = [...new Set(catalog.endpoints.map((e) => e.category))].sort();
|
|
40
51
|
const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION });
|
|
@@ -64,11 +75,7 @@ export function createMercuryServer(config, deps = {}) {
|
|
|
64
75
|
});
|
|
65
76
|
server.registerTool('get_endpoint_data', {
|
|
66
77
|
title: 'Call a Mercury402 endpoint',
|
|
67
|
-
description:
|
|
68
|
-
'the price and x402 payment instructions instead of data. With paid mode enabled (MERCURY402_PAYER_PRIVATE_KEY), ' +
|
|
69
|
-
'it pays in USDC on Base (capped by MERCURY402_MAX_PRICE_USD) and returns the data. ' +
|
|
70
|
-
'Use list_endpoints first to find paths and parameters. Path params can be inline ("/v1/fred/UNRATE") or passed in params ' +
|
|
71
|
-
'("/v1/fred/{series_id}" + {"series_id":"UNRATE"}). Other params go to the query string (GET) or JSON body (POST).',
|
|
78
|
+
description: options.hosted ? HOSTED_GET_ENDPOINT_DATA_DESCRIPTION : LOCAL_GET_ENDPOINT_DATA_DESCRIPTION,
|
|
72
79
|
inputSchema: {
|
|
73
80
|
path: z.string().describe('Endpoint path, e.g. "/v1/treasury/yield-curve/daily-snapshot" or "/v1/fred/UNRATE"'),
|
|
74
81
|
params: z
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mercury402-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "MCP server for Mercury402: discover and call 78 pay-per-call (x402, USDC on Base) financial data endpoints — Treasury, FRED, forex, yield spreads, breakeven inflation, macro composites",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Mercury402",
|