crypto-market-data-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/README.md +4 -1
- package/index.js +45 -8
- package/package.json +1 -1
- package/server.json +3 -3
package/README.md
CHANGED
|
@@ -15,6 +15,9 @@ subscription, and a call that fails is never charged.
|
|
|
15
15
|
| `sentiment` | Fear and Greed, trending coins, breadth, mood | $0.003 |
|
|
16
16
|
| `coin_snapshot` | All of the above for one coin | $0.005 |
|
|
17
17
|
| `hyperliquid_wallet` | Positions, liquidation distance, risk label, PnL, volume for any address | $0.01 |
|
|
18
|
+
| `create_watch` | Up to 10 conditions (price, % change, funding, open interest, liquidations, order book, Fear and Greed, whale orders, news impact) checked every minute for 24 hours; a signed webhook when one happens | $0.02 |
|
|
19
|
+
| `watch_status` | A watch's status and its last 20 fires | free |
|
|
20
|
+
| `stop_watch` | Stop a watch | free |
|
|
18
21
|
| `service_info` | What is offered and what this session has spent | free |
|
|
19
22
|
|
|
20
23
|
Data refreshes every minute from 24/7 monitoring of Binance spot and futures, Hyperliquid and 31 news sources.
|
|
@@ -58,7 +61,7 @@ gasless. Use a dedicated wallet with a small balance.
|
|
|
58
61
|
|
|
59
62
|
## Spending limits
|
|
60
63
|
|
|
61
|
-
- `X402_MAX_PER_CALL_USD` (default `0.
|
|
64
|
+
- `X402_MAX_PER_CALL_USD` (default `0.02`, the price of a watch): no single payment above this.
|
|
62
65
|
- `X402_MAX_TOTAL_USD` (default `1.00`): no more payments after this much in a session.
|
|
63
66
|
|
|
64
67
|
Without a key, `prices` and `sentiment` work free, with data refreshed every 15 minutes. `service_info` works too,
|
package/index.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// wallet in X402_PRIVATE_KEY, within hard spending caps. No account or API key is needed.
|
|
5
5
|
//
|
|
6
6
|
// X402_PRIVATE_KEY 0x... key of a wallet holding a little USDC on Base (required for paid tools)
|
|
7
|
-
// X402_MAX_PER_CALL_USD refuse any single payment above this (default 0.
|
|
7
|
+
// X402_MAX_PER_CALL_USD refuse any single payment above this (default 0.02, the price of a watch)
|
|
8
8
|
// X402_MAX_TOTAL_USD stop paying after this much per session (default 1.00)
|
|
9
9
|
// CMD_API_URL API base (default https://crypto-market-data.zafermemon87.workers.dev)
|
|
10
10
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -21,7 +21,7 @@ function cap(name, dflt) {
|
|
|
21
21
|
const v = Number((process.env[name] || "").trim() || dflt);
|
|
22
22
|
return Number.isFinite(v) && v >= 0 ? Math.round(v * USDC_UNITS) : 0; // an unreadable cap pays nothing, never "no limit"
|
|
23
23
|
}
|
|
24
|
-
const maxPerCall = cap("X402_MAX_PER_CALL_USD", "0.
|
|
24
|
+
const maxPerCall = cap("X402_MAX_PER_CALL_USD", "0.02");
|
|
25
25
|
const maxTotal = cap("X402_MAX_TOTAL_USD", "1.00");
|
|
26
26
|
let spent = 0;
|
|
27
27
|
|
|
@@ -37,13 +37,14 @@ function client() {
|
|
|
37
37
|
const text = (obj) => ({ content: [{ type: "text", text: typeof obj === "string" ? obj : JSON.stringify(obj, null, 1) }] });
|
|
38
38
|
const fail = (msg) => ({ ...text(msg), isError: true });
|
|
39
39
|
|
|
40
|
-
async function call(path, query) {
|
|
40
|
+
async function call(path, query, { method = "GET", data, headers = {} } = {}) {
|
|
41
41
|
const url = new URL(API + path);
|
|
42
42
|
for (const [k, v] of Object.entries(query || {})) if (v !== undefined && v !== null && v !== "") url.searchParams.set(k, String(v));
|
|
43
|
-
const
|
|
43
|
+
const init = data === undefined ? { method, headers } : { method, headers: { ...headers, "Content-Type": "application/json" }, body: JSON.stringify(data) };
|
|
44
|
+
const first = await fetch(url, init);
|
|
44
45
|
if (first.status !== 402) return first.ok ? text(await first.json()) : fail(`API answered ${first.status}: ${await first.text()}`);
|
|
45
46
|
const pay = client();
|
|
46
|
-
if (!pay && FREE[path]) {
|
|
47
|
+
if (!pay && method === "GET" && FREE[path]) {
|
|
47
48
|
// no wallet: the free copy of this data, refreshed every 15 minutes
|
|
48
49
|
const free = await fetch(API + FREE[path] + url.search);
|
|
49
50
|
const body = await free.json().catch(() => ({}));
|
|
@@ -51,21 +52,21 @@ async function call(path, query) {
|
|
|
51
52
|
return text({ ...body, note: "Free data refreshed every 15 minutes. Set X402_PRIVATE_KEY (a wallet with a little USDC on Base) " +
|
|
52
53
|
"for live data every minute." });
|
|
53
54
|
}
|
|
54
|
-
if (!pay) return fail("This tool
|
|
55
|
+
if (!pay) return fail("This tool is paid per call (USD 0.003 to 0.02) in USDC on Base. Set X402_PRIVATE_KEY to a wallet " +
|
|
55
56
|
"holding a little USDC on Base (no ETH needed), or use the same data on Apify: https://apify.com/zaffy_93/crypto-market-data");
|
|
56
57
|
const required = pay.getPaymentRequiredResponse((n) => first.headers.get(n), await first.json().catch(() => undefined));
|
|
57
58
|
const amount = Number(required.accepts[0].amount);
|
|
58
59
|
if (!(amount > 0) || amount > maxPerCall) return fail(`Price ${amount / USDC_UNITS} USDC is above X402_MAX_PER_CALL_USD (${maxPerCall / USDC_UNITS}); not paid.`);
|
|
59
60
|
if (spent + amount > maxTotal) return fail(`Session budget X402_MAX_TOTAL_USD (${maxTotal / USDC_UNITS}) reached (${spent / USDC_UNITS} USDC spent); not paid.`);
|
|
60
61
|
const payload = await pay.createPaymentPayload(required);
|
|
61
|
-
const paid = await fetch(url, { headers: pay.encodePaymentSignatureHeader(payload) });
|
|
62
|
+
const paid = await fetch(url, { ...init, headers: { ...init.headers, ...pay.encodePaymentSignatureHeader(payload) } });
|
|
62
63
|
const body = await paid.json().catch(() => ({}));
|
|
63
64
|
if (paid.status === 200 && paid.headers.get("PAYMENT-RESPONSE")) spent += amount;
|
|
64
65
|
// 400/503 answers are not settled by the API, so they cost nothing
|
|
65
66
|
return paid.ok ? text(body) : fail(`Not answered (${paid.status}), not charged: ${body.error || JSON.stringify(body)}`);
|
|
66
67
|
}
|
|
67
68
|
|
|
68
|
-
const server = new McpServer({ name: "crypto-market-data", version: "0.1.
|
|
69
|
+
const server = new McpServer({ name: "crypto-market-data", version: "0.1.3" });
|
|
69
70
|
const tokens = z.string().optional().describe("Comma-separated tickers such as BTC,ETH,SOL (up to 50). Empty means the top 50 coins.");
|
|
70
71
|
const token = z.string().optional().describe("One ticker such as BTC");
|
|
71
72
|
const limit = z.number().int().min(1).max(200).optional().describe("How many rows");
|
|
@@ -92,6 +93,42 @@ for (const [name, path, description, shape] of tools) {
|
|
|
92
93
|
}
|
|
93
94
|
});
|
|
94
95
|
}
|
|
96
|
+
// Watches: conditions checked every minute for 24 hours, with a signed webhook (or status polling) when one happens.
|
|
97
|
+
const METRICS = ["price", "change_5m_pct", "change_1h_pct", "change_4h_pct", "change_24h_pct", "funding_8h_pct", "futures_premium_pct",
|
|
98
|
+
"open_interest_change_1h_pct", "open_interest_change_24h_pct", "long_short_account_ratio", "spread_bps", "orderbook_imbalance",
|
|
99
|
+
"buyer_share_15m", "volume_15m_vs_normal", "liquidations_1h_usd", "liquidations_long_1h_usd", "liquidations_short_1h_usd", "fear_greed",
|
|
100
|
+
"whale_order_usd", "news_impact"];
|
|
101
|
+
const condition = z.object({
|
|
102
|
+
token: z.string().optional().describe("Ticker such as BTC; \"*\" (any coin) for whale_order_usd and news_impact; omit for fear_greed"),
|
|
103
|
+
metric: z.enum(METRICS),
|
|
104
|
+
op: z.enum(["above", "below"]).describe("above: >= value; below: <= value (events use above)"),
|
|
105
|
+
value: z.number().describe("Threshold in the metric's unit: percent for *_pct, USD for *_usd"),
|
|
106
|
+
});
|
|
107
|
+
server.registerTool("create_watch", {
|
|
108
|
+
description: "Watch the crypto market for 24 hours: up to 10 conditions (price, % change, funding, open interest, liquidations, " +
|
|
109
|
+
"order book, Fear and Greed, whale orders, news impact) checked every minute; when one happens, a signed webhook goes to " +
|
|
110
|
+
"callback_url and it shows in watch_status. Level conditions fire when they turn true. Keep the returned secret (USD 0.02).",
|
|
111
|
+
inputSchema: { conditions: z.array(condition).min(1).max(10), callback_url: z.string().optional().describe("https URL for the webhook (optional)"),
|
|
112
|
+
label: z.string().max(80).optional() },
|
|
113
|
+
}, async (args) => {
|
|
114
|
+
try {
|
|
115
|
+
return await call("/v1/watch", null, { method: "POST", data: args });
|
|
116
|
+
} catch (e) {
|
|
117
|
+
return fail(`Request failed: ${e.message}`);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
const watchRef = { watch_id: z.string().describe("watch_id from create_watch"), secret: z.string().describe("secret from create_watch") };
|
|
121
|
+
for (const [name, method, description] of [["watch_status", "GET", "Free: a watch's status, conditions and its last 20 fires."],
|
|
122
|
+
["stop_watch", "DELETE", "Free: stop a watch before it expires."]]) {
|
|
123
|
+
server.registerTool(name, { description, inputSchema: watchRef }, async ({ watch_id, secret }) => {
|
|
124
|
+
try {
|
|
125
|
+
return await call(`/watch/${encodeURIComponent(watch_id)}`, null, { method, headers: { "X-Watch-Secret": secret } });
|
|
126
|
+
} catch (e) {
|
|
127
|
+
return fail(`Request failed: ${e.message}`);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
95
132
|
server.registerTool("service_info", { description: "Free: what the service offers, prices, and session spending so far.", inputSchema: {} },
|
|
96
133
|
async () => {
|
|
97
134
|
const info = await (await fetch(API + "/")).json();
|
package/package.json
CHANGED
package/server.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"url": "https://github.com/zaffy18/crypto-market-data-mcp",
|
|
8
8
|
"source": "github"
|
|
9
9
|
},
|
|
10
|
-
"version": "0.1.
|
|
10
|
+
"version": "0.1.3",
|
|
11
11
|
"remotes": [
|
|
12
12
|
{
|
|
13
13
|
"type": "streamable-http",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
{
|
|
19
19
|
"registryType": "npm",
|
|
20
20
|
"identifier": "crypto-market-data-mcp",
|
|
21
|
-
"version": "0.1.
|
|
21
|
+
"version": "0.1.3",
|
|
22
22
|
"transport": {
|
|
23
23
|
"type": "stdio"
|
|
24
24
|
},
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
},
|
|
32
32
|
{
|
|
33
33
|
"name": "X402_MAX_PER_CALL_USD",
|
|
34
|
-
"description": "No single payment above this (default 0.
|
|
34
|
+
"description": "No single payment above this (default 0.02, the price of a watch)",
|
|
35
35
|
"isRequired": false,
|
|
36
36
|
"isSecret": false
|
|
37
37
|
},
|