bitstamp-mcp 1.0.1 → 1.0.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 +2 -0
- package/dist/api.js +64 -0
- package/dist/server.js +43 -2
- package/package.json +2 -2
package/README.md
CHANGED
package/dist/api.js
CHANGED
|
@@ -20,3 +20,67 @@ export async function ticker(args) {
|
|
|
20
20
|
`Timestamp: ${get('timestamp') ? new Date(Number(get('timestamp')) * 1000).toISOString() : '?'}`,
|
|
21
21
|
].filter(Boolean).join('\n');
|
|
22
22
|
}
|
|
23
|
+
// ---- muxA tools:
|
|
24
|
+
const BITSTAMP = 'https://www.bitstamp.net/api/v2';
|
|
25
|
+
const OHLC_STEPS = [
|
|
26
|
+
[60, '1 minute'],
|
|
27
|
+
[180, '3 minutes'],
|
|
28
|
+
[300, '5 minutes'],
|
|
29
|
+
[900, '15 minutes'],
|
|
30
|
+
[1800, '30 minutes'],
|
|
31
|
+
[3600, '1 hour'],
|
|
32
|
+
[7200, '2 hours'],
|
|
33
|
+
[14400, '4 hours'],
|
|
34
|
+
[21600, '6 hours'],
|
|
35
|
+
[28800, '8 hours'],
|
|
36
|
+
[43200, '12 hours'],
|
|
37
|
+
[86400, '1 day'],
|
|
38
|
+
[259200, '3 days'],
|
|
39
|
+
];
|
|
40
|
+
function when(epoch) {
|
|
41
|
+
const n = Number(epoch);
|
|
42
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
43
|
+
return "unknown";
|
|
44
|
+
return new Date(n * 1000).toISOString().replace("T", " ").slice(0, 16) + " UTC";
|
|
45
|
+
}
|
|
46
|
+
function clamp(value, fallback, max) {
|
|
47
|
+
return Math.max(1, Math.min(Math.round(value ?? fallback), max));
|
|
48
|
+
}
|
|
49
|
+
async function bitstampGet(path) {
|
|
50
|
+
const res = await fetch(`${BITSTAMP}${path}`, {
|
|
51
|
+
headers: { "User-Agent": UA, Accept: "application/json" },
|
|
52
|
+
signal: AbortSignal.timeout(20000),
|
|
53
|
+
});
|
|
54
|
+
if (!res.ok)
|
|
55
|
+
throw new Error(`Bitstamp returned ${res.status}`);
|
|
56
|
+
const d = (await res.json());
|
|
57
|
+
if (d?.status === "error" || d?.code) {
|
|
58
|
+
const detail = d.reason ?? d.errors?.[0]?.message ?? "error";
|
|
59
|
+
throw new Error(`Bitstamp: ${String(detail)}`);
|
|
60
|
+
}
|
|
61
|
+
return d;
|
|
62
|
+
}
|
|
63
|
+
export async function tradingPairs(args) {
|
|
64
|
+
const d = (await bitstampGet("/trading-pairs-info/"));
|
|
65
|
+
if (!Array.isArray(d) || !d.length)
|
|
66
|
+
throw new Error("Bitstamp returned no trading pairs.");
|
|
67
|
+
const q = (args.search ?? "").trim().toLowerCase();
|
|
68
|
+
let rows = d;
|
|
69
|
+
if (q)
|
|
70
|
+
rows = rows.filter((p) => `${p.name ?? ""} ${p.url_symbol ?? ""}`.toLowerCase().includes(q));
|
|
71
|
+
if (args.trading_only !== false)
|
|
72
|
+
rows = rows.filter((p) => String(p.trading ?? "").toLowerCase() === "enabled");
|
|
73
|
+
if (!rows.length) {
|
|
74
|
+
return q ? `No trading pair matches "${q}".` : "Bitstamp lists no enabled trading pairs.";
|
|
75
|
+
}
|
|
76
|
+
const limit = clamp(args.limit, 25, 100);
|
|
77
|
+
const shown = rows.slice(0, limit);
|
|
78
|
+
return [
|
|
79
|
+
`Bitstamp trading pairs: ${rows.length} ${rows.length === 1 ? "match" : "matches"}, showing ${shown.length} of ${d.length} total.`,
|
|
80
|
+
...shown.map((p, i) => `${i + 1}. ${String(p.name ?? "?")} | symbol ${String(p.url_symbol ?? "?")} | base decimals ${p.base_decimals ?? "?"}` +
|
|
81
|
+
` | min order ${String(p.minimum_order ?? "not reported")} | trading ${p.trading ?? "?"} | instant ${String(p.instant_trading ?? "?")}`),
|
|
82
|
+
rows.length > limit ? `plus ${rows.length - limit} more` : "",
|
|
83
|
+
]
|
|
84
|
+
.filter(Boolean)
|
|
85
|
+
.join("\n");
|
|
86
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import { ticker } from "./api.js";
|
|
3
|
+
import { ticker, tradingPairs } from "./api.js";
|
|
4
4
|
const text = (value) => ({ content: [{ type: "text", text: value }] });
|
|
5
5
|
const textError = (t) => ({ content: [{ type: "text", text: t }], isError: true });
|
|
6
6
|
const READ_ONLY = { readOnlyHint: true, openWorldHint: true };
|
|
@@ -17,7 +17,48 @@ export function createServer() {
|
|
|
17
17
|
return text(await ticker(args));
|
|
18
18
|
}
|
|
19
19
|
catch (e) {
|
|
20
|
-
return textError(
|
|
20
|
+
return textError(String(e));
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
server.registerTool("order_book", {
|
|
24
|
+
title: "Order book",
|
|
25
|
+
description: "Live Bitstamp order book with best bid and ask, spread, top level notional depth and the ladder of both sides.",
|
|
26
|
+
inputSchema: z.object({ pair: z.string().describe("Pair symbol, e.g. btcusd."), group: z.number().describe("Price grouping.").optional(), limit: z.number().describe("Levels per side.").optional() }),
|
|
27
|
+
annotations: READ_ONLY,
|
|
28
|
+
}, async (args) => {
|
|
29
|
+
// TODO: implement book locally (paywall removed)
|
|
30
|
+
try {
|
|
31
|
+
return textError("Not yet implemented: book");
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
return textError(String(e));
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
server.registerTool("ohlc_candles", {
|
|
38
|
+
title: "OHLC candles",
|
|
39
|
+
description: "Bitstamp candlestick history for a pair at a chosen step from one minute to three days, with open, high, low, close and volume per candle.",
|
|
40
|
+
inputSchema: z.object({ pair: z.string().describe("Pair symbol, e.g. btcusd."), step: z.number().describe("Step in seconds, e.g. 3600 for hourly.").optional(), limit: z.number().describe("Max candles.").optional() }),
|
|
41
|
+
annotations: READ_ONLY,
|
|
42
|
+
}, async (args) => {
|
|
43
|
+
// TODO: implement candles locally (paywall removed)
|
|
44
|
+
try {
|
|
45
|
+
return textError("Not yet implemented: candles");
|
|
46
|
+
}
|
|
47
|
+
catch (e) {
|
|
48
|
+
return textError(String(e));
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
server.registerTool("trading_pairs", {
|
|
52
|
+
title: "Trading pairs",
|
|
53
|
+
description: "List Bitstamp trading pairs with URL symbol, decimal places, minimum order size and whether trading and instant trading are enabled.",
|
|
54
|
+
inputSchema: z.object({ trading_only: z.boolean().describe("Only enabled pairs.").optional(), search: z.string().describe("Filter by name or symbol.").optional(), limit: z.number().describe("Max pairs.").optional() }),
|
|
55
|
+
annotations: READ_ONLY,
|
|
56
|
+
}, async (args) => {
|
|
57
|
+
try {
|
|
58
|
+
return text(await tradingPairs(args));
|
|
59
|
+
}
|
|
60
|
+
catch (e) {
|
|
61
|
+
return textError(String(e));
|
|
21
62
|
}
|
|
22
63
|
});
|
|
23
64
|
return server;
|