@codespar/mcp-bitso 0.1.0 → 0.2.0
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 -0
- package/dist/index.js +228 -9
- package/package.json +3 -2
- package/server.json +30 -0
- package/src/__tests__/index.test.ts +50 -0
- package/src/index.ts +197 -10
package/README.md
CHANGED
|
@@ -110,6 +110,10 @@ Want to contribute? [Open a PR](https://github.com/codespar/mcp-dev-brasil) or [
|
|
|
110
110
|
- [MCP Dev Brasil](https://github.com/codespar/mcp-dev-brasil)
|
|
111
111
|
- [Landing Page](https://codespar.dev/mcp)
|
|
112
112
|
|
|
113
|
+
## Enterprise
|
|
114
|
+
|
|
115
|
+
Need governance, budget limits, and audit trails for agent payments? [CodeSpar Enterprise](https://codespar.dev/enterprise) adds policy engine, payment routing, and compliance templates on top of these MCP servers.
|
|
116
|
+
|
|
113
117
|
## License
|
|
114
118
|
|
|
115
119
|
MIT
|
package/dist/index.js
CHANGED
|
@@ -13,6 +13,16 @@
|
|
|
13
13
|
* - list_trades: List executed trades
|
|
14
14
|
* - list_funding_sources: List available funding sources
|
|
15
15
|
* - create_withdrawal: Create a withdrawal request
|
|
16
|
+
* - list_ledger: List account ledger entries (trades, fees, fundings, withdrawals)
|
|
17
|
+
* - list_open_orders: List currently open orders
|
|
18
|
+
* - lookup_order: Look up an order by origin_id / client_id
|
|
19
|
+
* - cancel_all_orders: Cancel all open orders
|
|
20
|
+
* - list_fundings: List account fundings (deposits)
|
|
21
|
+
* - list_withdrawals: List account withdrawals
|
|
22
|
+
* - get_withdrawal: Retrieve a specific withdrawal by ID
|
|
23
|
+
* - list_fees: List applicable fees for the authenticated user
|
|
24
|
+
* - get_account_status: Retrieve account KYC and verification status
|
|
25
|
+
* - list_funding_destinations: Get funding destination info for a currency
|
|
16
26
|
*
|
|
17
27
|
* Environment:
|
|
18
28
|
* BITSO_API_KEY — API key from https://bitso.com/
|
|
@@ -20,6 +30,8 @@
|
|
|
20
30
|
*/
|
|
21
31
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
22
32
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
33
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
34
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
23
35
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
24
36
|
import * as crypto from "node:crypto";
|
|
25
37
|
const API_KEY = process.env.BITSO_API_KEY || "";
|
|
@@ -47,7 +59,7 @@ async function bitsoRequest(method, path, body) {
|
|
|
47
59
|
}
|
|
48
60
|
return res.json();
|
|
49
61
|
}
|
|
50
|
-
const server = new Server({ name: "mcp-bitso", version: "0.
|
|
62
|
+
const server = new Server({ name: "mcp-bitso", version: "0.2.0" }, { capabilities: { tools: {} } });
|
|
51
63
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
52
64
|
tools: [
|
|
53
65
|
{
|
|
@@ -164,6 +176,110 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
164
176
|
required: ["currency", "amount"],
|
|
165
177
|
},
|
|
166
178
|
},
|
|
179
|
+
{
|
|
180
|
+
name: "list_ledger",
|
|
181
|
+
description: "List account ledger entries (trades, fees, fundings, withdrawals)",
|
|
182
|
+
inputSchema: {
|
|
183
|
+
type: "object",
|
|
184
|
+
properties: {
|
|
185
|
+
operation: { type: "string", enum: ["trade", "fee", "funding", "withdrawal"], description: "Filter by operation type" },
|
|
186
|
+
limit: { type: "number", description: "Number of results (default 25, max 100)" },
|
|
187
|
+
marker: { type: "string", description: "Pagination marker" },
|
|
188
|
+
sort: { type: "string", enum: ["asc", "desc"], description: "Sort direction" },
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
name: "list_open_orders",
|
|
194
|
+
description: "List currently open orders for the authenticated user",
|
|
195
|
+
inputSchema: {
|
|
196
|
+
type: "object",
|
|
197
|
+
properties: {
|
|
198
|
+
book: { type: "string", description: "Filter by order book symbol (e.g. btc_mxn)" },
|
|
199
|
+
marker: { type: "string", description: "Pagination marker" },
|
|
200
|
+
sort: { type: "string", enum: ["asc", "desc"], description: "Sort direction" },
|
|
201
|
+
limit: { type: "number", description: "Number of results (default 25, max 100)" },
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
name: "lookup_order",
|
|
207
|
+
description: "Look up one or more orders by origin_id (client_id)",
|
|
208
|
+
inputSchema: {
|
|
209
|
+
type: "object",
|
|
210
|
+
properties: {
|
|
211
|
+
origin_ids: { type: "string", description: "Comma-separated origin_id values (client IDs)" },
|
|
212
|
+
},
|
|
213
|
+
required: ["origin_ids"],
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
name: "cancel_all_orders",
|
|
218
|
+
description: "Cancel all open orders for the authenticated user",
|
|
219
|
+
inputSchema: { type: "object", properties: {} },
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
name: "list_fundings",
|
|
223
|
+
description: "List account fundings (deposits)",
|
|
224
|
+
inputSchema: {
|
|
225
|
+
type: "object",
|
|
226
|
+
properties: {
|
|
227
|
+
fids: { type: "string", description: "Comma-separated funding IDs to filter" },
|
|
228
|
+
status: { type: "string", enum: ["pending", "complete", "cancelled"], description: "Filter by status" },
|
|
229
|
+
method: { type: "string", description: "Funding method (e.g. spei, bitcoin)" },
|
|
230
|
+
limit: { type: "number", description: "Number of results (default 25, max 100)" },
|
|
231
|
+
marker: { type: "string", description: "Pagination marker" },
|
|
232
|
+
sort: { type: "string", enum: ["asc", "desc"], description: "Sort direction" },
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
name: "list_withdrawals",
|
|
238
|
+
description: "List account withdrawals",
|
|
239
|
+
inputSchema: {
|
|
240
|
+
type: "object",
|
|
241
|
+
properties: {
|
|
242
|
+
wids: { type: "string", description: "Comma-separated withdrawal IDs to filter" },
|
|
243
|
+
status: { type: "string", enum: ["pending", "processing", "complete", "failed"], description: "Filter by status" },
|
|
244
|
+
method: { type: "string", description: "Withdrawal method (e.g. spei, bitcoin)" },
|
|
245
|
+
limit: { type: "number", description: "Number of results (default 25, max 100)" },
|
|
246
|
+
marker: { type: "string", description: "Pagination marker" },
|
|
247
|
+
sort: { type: "string", enum: ["asc", "desc"], description: "Sort direction" },
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
name: "get_withdrawal",
|
|
253
|
+
description: "Retrieve a specific withdrawal by its ID",
|
|
254
|
+
inputSchema: {
|
|
255
|
+
type: "object",
|
|
256
|
+
properties: {
|
|
257
|
+
wid: { type: "string", description: "Withdrawal ID" },
|
|
258
|
+
},
|
|
259
|
+
required: ["wid"],
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
name: "list_fees",
|
|
264
|
+
description: "List applicable fees for the authenticated user across trading pairs",
|
|
265
|
+
inputSchema: { type: "object", properties: {} },
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
name: "get_account_status",
|
|
269
|
+
description: "Retrieve account KYC and verification status (tier, limits, required docs)",
|
|
270
|
+
inputSchema: { type: "object", properties: {} },
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
name: "list_funding_destinations",
|
|
274
|
+
description: "Get funding destination details (address/CLABE) for a given currency",
|
|
275
|
+
inputSchema: {
|
|
276
|
+
type: "object",
|
|
277
|
+
properties: {
|
|
278
|
+
fund_currency: { type: "string", description: "Currency code (e.g. btc, mxn, ars)" },
|
|
279
|
+
},
|
|
280
|
+
required: ["fund_currency"],
|
|
281
|
+
},
|
|
282
|
+
},
|
|
167
283
|
],
|
|
168
284
|
}));
|
|
169
285
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
@@ -216,6 +332,78 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
216
332
|
return { content: [{ type: "text", text: JSON.stringify(await bitsoRequest("GET", "/funding_destination"), null, 2) }] };
|
|
217
333
|
case "create_withdrawal":
|
|
218
334
|
return { content: [{ type: "text", text: JSON.stringify(await bitsoRequest("POST", "/withdrawals", args), null, 2) }] };
|
|
335
|
+
case "list_ledger": {
|
|
336
|
+
const params = new URLSearchParams();
|
|
337
|
+
if (args?.limit)
|
|
338
|
+
params.set("limit", String(args.limit));
|
|
339
|
+
if (args?.marker)
|
|
340
|
+
params.set("marker", String(args.marker));
|
|
341
|
+
if (args?.sort)
|
|
342
|
+
params.set("sort", String(args.sort));
|
|
343
|
+
const op = args?.operation ? `/${args.operation}s` : "";
|
|
344
|
+
const qs = params.toString();
|
|
345
|
+
return { content: [{ type: "text", text: JSON.stringify(await bitsoRequest("GET", `/ledger${op}${qs ? `?${qs}` : ""}`), null, 2) }] };
|
|
346
|
+
}
|
|
347
|
+
case "list_open_orders": {
|
|
348
|
+
const params = new URLSearchParams();
|
|
349
|
+
if (args?.book)
|
|
350
|
+
params.set("book", String(args.book));
|
|
351
|
+
if (args?.limit)
|
|
352
|
+
params.set("limit", String(args.limit));
|
|
353
|
+
if (args?.marker)
|
|
354
|
+
params.set("marker", String(args.marker));
|
|
355
|
+
if (args?.sort)
|
|
356
|
+
params.set("sort", String(args.sort));
|
|
357
|
+
const qs = params.toString();
|
|
358
|
+
return { content: [{ type: "text", text: JSON.stringify(await bitsoRequest("GET", `/open_orders${qs ? `?${qs}` : ""}`), null, 2) }] };
|
|
359
|
+
}
|
|
360
|
+
case "lookup_order":
|
|
361
|
+
return { content: [{ type: "text", text: JSON.stringify(await bitsoRequest("GET", `/orders/${args?.origin_ids}`), null, 2) }] };
|
|
362
|
+
case "cancel_all_orders":
|
|
363
|
+
return { content: [{ type: "text", text: JSON.stringify(await bitsoRequest("DELETE", "/orders/all"), null, 2) }] };
|
|
364
|
+
case "list_fundings": {
|
|
365
|
+
const params = new URLSearchParams();
|
|
366
|
+
if (args?.status)
|
|
367
|
+
params.set("status", String(args.status));
|
|
368
|
+
if (args?.method)
|
|
369
|
+
params.set("method", String(args.method));
|
|
370
|
+
if (args?.limit)
|
|
371
|
+
params.set("limit", String(args.limit));
|
|
372
|
+
if (args?.marker)
|
|
373
|
+
params.set("marker", String(args.marker));
|
|
374
|
+
if (args?.sort)
|
|
375
|
+
params.set("sort", String(args.sort));
|
|
376
|
+
const suffix = args?.fids ? `/${args.fids}` : "";
|
|
377
|
+
const qs = params.toString();
|
|
378
|
+
return { content: [{ type: "text", text: JSON.stringify(await bitsoRequest("GET", `/fundings${suffix}${qs ? `?${qs}` : ""}`), null, 2) }] };
|
|
379
|
+
}
|
|
380
|
+
case "list_withdrawals": {
|
|
381
|
+
const params = new URLSearchParams();
|
|
382
|
+
if (args?.status)
|
|
383
|
+
params.set("status", String(args.status));
|
|
384
|
+
if (args?.method)
|
|
385
|
+
params.set("method", String(args.method));
|
|
386
|
+
if (args?.limit)
|
|
387
|
+
params.set("limit", String(args.limit));
|
|
388
|
+
if (args?.marker)
|
|
389
|
+
params.set("marker", String(args.marker));
|
|
390
|
+
if (args?.sort)
|
|
391
|
+
params.set("sort", String(args.sort));
|
|
392
|
+
const suffix = args?.wids ? `/${args.wids}` : "";
|
|
393
|
+
const qs = params.toString();
|
|
394
|
+
return { content: [{ type: "text", text: JSON.stringify(await bitsoRequest("GET", `/withdrawals${suffix}${qs ? `?${qs}` : ""}`), null, 2) }] };
|
|
395
|
+
}
|
|
396
|
+
case "get_withdrawal":
|
|
397
|
+
return { content: [{ type: "text", text: JSON.stringify(await bitsoRequest("GET", `/withdrawals/${args?.wid}`), null, 2) }] };
|
|
398
|
+
case "list_fees":
|
|
399
|
+
return { content: [{ type: "text", text: JSON.stringify(await bitsoRequest("GET", "/fees"), null, 2) }] };
|
|
400
|
+
case "get_account_status":
|
|
401
|
+
return { content: [{ type: "text", text: JSON.stringify(await bitsoRequest("GET", "/account_status"), null, 2) }] };
|
|
402
|
+
case "list_funding_destinations": {
|
|
403
|
+
const params = new URLSearchParams();
|
|
404
|
+
params.set("fund_currency", String(args?.fund_currency));
|
|
405
|
+
return { content: [{ type: "text", text: JSON.stringify(await bitsoRequest("GET", `/funding_destination?${params}`), null, 2) }] };
|
|
406
|
+
}
|
|
219
407
|
default:
|
|
220
408
|
return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
|
|
221
409
|
}
|
|
@@ -225,15 +413,46 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
225
413
|
}
|
|
226
414
|
});
|
|
227
415
|
async function main() {
|
|
228
|
-
if (
|
|
229
|
-
|
|
230
|
-
|
|
416
|
+
if (process.argv.includes("--http") || process.env.MCP_HTTP === "true") {
|
|
417
|
+
const { default: express } = await import("express");
|
|
418
|
+
const { randomUUID } = await import("node:crypto");
|
|
419
|
+
const app = express();
|
|
420
|
+
app.use(express.json());
|
|
421
|
+
const transports = new Map();
|
|
422
|
+
app.get("/health", (_req, res) => res.json({ status: "ok", sessions: transports.size }));
|
|
423
|
+
app.post("/mcp", async (req, res) => {
|
|
424
|
+
const sid = req.headers["mcp-session-id"];
|
|
425
|
+
if (sid && transports.has(sid)) {
|
|
426
|
+
await transports.get(sid).handleRequest(req, res, req.body);
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
if (!sid && isInitializeRequest(req.body)) {
|
|
430
|
+
const t = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (id) => { transports.set(id, t); } });
|
|
431
|
+
t.onclose = () => { if (t.sessionId)
|
|
432
|
+
transports.delete(t.sessionId); };
|
|
433
|
+
const s = new Server({ name: "mcp-bitso", version: "0.2.0" }, { capabilities: { tools: {} } });
|
|
434
|
+
server._requestHandlers.forEach((v, k) => s._requestHandlers.set(k, v));
|
|
435
|
+
server._notificationHandlers?.forEach((v, k) => s._notificationHandlers.set(k, v));
|
|
436
|
+
await s.connect(t);
|
|
437
|
+
await t.handleRequest(req, res, req.body);
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
res.status(400).json({ jsonrpc: "2.0", error: { code: -32000, message: "Bad Request" }, id: null });
|
|
441
|
+
});
|
|
442
|
+
app.get("/mcp", async (req, res) => { const sid = req.headers["mcp-session-id"]; if (sid && transports.has(sid))
|
|
443
|
+
await transports.get(sid).handleRequest(req, res);
|
|
444
|
+
else
|
|
445
|
+
res.status(400).send("Invalid session"); });
|
|
446
|
+
app.delete("/mcp", async (req, res) => { const sid = req.headers["mcp-session-id"]; if (sid && transports.has(sid))
|
|
447
|
+
await transports.get(sid).handleRequest(req, res);
|
|
448
|
+
else
|
|
449
|
+
res.status(400).send("Invalid session"); });
|
|
450
|
+
const port = Number(process.env.MCP_PORT) || 3000;
|
|
451
|
+
app.listen(port, () => { console.error(`MCP HTTP server on http://localhost:${port}/mcp`); });
|
|
231
452
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
453
|
+
else {
|
|
454
|
+
const transport = new StdioServerTransport();
|
|
455
|
+
await server.connect(transport);
|
|
235
456
|
}
|
|
236
|
-
const transport = new StdioServerTransport();
|
|
237
|
-
await server.connect(transport);
|
|
238
457
|
}
|
|
239
458
|
main().catch(console.error);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codespar/mcp-bitso",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "MCP server for Bitso — Latin American crypto exchange, trading, funding, withdrawals",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -26,5 +26,6 @@
|
|
|
26
26
|
"exchange",
|
|
27
27
|
"trading",
|
|
28
28
|
"latam"
|
|
29
|
-
]
|
|
29
|
+
],
|
|
30
|
+
"mcpName": "io.github.codespar/mcp-bitso"
|
|
30
31
|
}
|
package/server.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
+
"name": "io.github.codespar/mcp-bitso",
|
|
4
|
+
"description": "MCP server for Bitso — Latin American crypto exchange, trading, funding, withdrawals",
|
|
5
|
+
"repository": {
|
|
6
|
+
"url": "https://github.com/codespar/mcp-dev-brasil",
|
|
7
|
+
"source": "github",
|
|
8
|
+
"subfolder": "packages/crypto/bitso"
|
|
9
|
+
},
|
|
10
|
+
"version": "0.2.0",
|
|
11
|
+
"packages": [
|
|
12
|
+
{
|
|
13
|
+
"registryType": "npm",
|
|
14
|
+
"identifier": "@codespar/mcp-bitso",
|
|
15
|
+
"version": "0.2.0",
|
|
16
|
+
"transport": {
|
|
17
|
+
"type": "stdio"
|
|
18
|
+
},
|
|
19
|
+
"environmentVariables": [
|
|
20
|
+
{
|
|
21
|
+
"name": "BITSO_API_KEY",
|
|
22
|
+
"description": "API key for bitso",
|
|
23
|
+
"isRequired": true,
|
|
24
|
+
"format": "string",
|
|
25
|
+
"isSecret": true
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
|
|
3
|
+
let listToolsHandler: Function;
|
|
4
|
+
let callToolHandler: Function;
|
|
5
|
+
|
|
6
|
+
vi.mock("@modelcontextprotocol/sdk/server/index.js", () => {
|
|
7
|
+
class FakeServer {
|
|
8
|
+
constructor() {}
|
|
9
|
+
setRequestHandler(schema: any, handler: Function) {
|
|
10
|
+
if (JSON.stringify(schema).includes("tools/list")) listToolsHandler = handler;
|
|
11
|
+
if (JSON.stringify(schema).includes("tools/call")) callToolHandler = handler;
|
|
12
|
+
}
|
|
13
|
+
connect() { return Promise.resolve(); }
|
|
14
|
+
}
|
|
15
|
+
return { Server: FakeServer };
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
vi.mock("@modelcontextprotocol/sdk/server/stdio.js", () => ({ StdioServerTransport: class {} }));
|
|
19
|
+
|
|
20
|
+
process.env.BITSO_API_KEY = "test-key";
|
|
21
|
+
process.env.BITSO_API_SECRET = "test-secret";
|
|
22
|
+
|
|
23
|
+
const mockFetch = vi.fn();
|
|
24
|
+
global.fetch = mockFetch as any;
|
|
25
|
+
|
|
26
|
+
beforeEach(async () => {
|
|
27
|
+
vi.resetModules();
|
|
28
|
+
listToolsHandler = undefined as any;
|
|
29
|
+
callToolHandler = undefined as any;
|
|
30
|
+
mockFetch.mockReset();
|
|
31
|
+
global.fetch = mockFetch as any;
|
|
32
|
+
await import("../index.js");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("mcp-bitso", () => {
|
|
36
|
+
it("should register 10 tools", async () => {
|
|
37
|
+
const result = await listToolsHandler();
|
|
38
|
+
expect(result.tools).toHaveLength(10);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("should call correct API endpoint for get_ticker", async () => {
|
|
42
|
+
mockFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ success: true, payload: { last: "100000" } }) });
|
|
43
|
+
|
|
44
|
+
await callToolHandler({ params: { name: "get_ticker", arguments: { book: "btc_brl" } } });
|
|
45
|
+
|
|
46
|
+
const [url] = mockFetch.mock.calls[0];
|
|
47
|
+
expect(url).toContain("api.bitso.com/v3/ticker");
|
|
48
|
+
expect(url).toContain("book=btc_brl");
|
|
49
|
+
});
|
|
50
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -14,6 +14,16 @@
|
|
|
14
14
|
* - list_trades: List executed trades
|
|
15
15
|
* - list_funding_sources: List available funding sources
|
|
16
16
|
* - create_withdrawal: Create a withdrawal request
|
|
17
|
+
* - list_ledger: List account ledger entries (trades, fees, fundings, withdrawals)
|
|
18
|
+
* - list_open_orders: List currently open orders
|
|
19
|
+
* - lookup_order: Look up an order by origin_id / client_id
|
|
20
|
+
* - cancel_all_orders: Cancel all open orders
|
|
21
|
+
* - list_fundings: List account fundings (deposits)
|
|
22
|
+
* - list_withdrawals: List account withdrawals
|
|
23
|
+
* - get_withdrawal: Retrieve a specific withdrawal by ID
|
|
24
|
+
* - list_fees: List applicable fees for the authenticated user
|
|
25
|
+
* - get_account_status: Retrieve account KYC and verification status
|
|
26
|
+
* - list_funding_destinations: Get funding destination info for a currency
|
|
17
27
|
*
|
|
18
28
|
* Environment:
|
|
19
29
|
* BITSO_API_KEY — API key from https://bitso.com/
|
|
@@ -22,6 +32,8 @@
|
|
|
22
32
|
|
|
23
33
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
24
34
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
35
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
36
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
25
37
|
import {
|
|
26
38
|
CallToolRequestSchema,
|
|
27
39
|
ListToolsRequestSchema,
|
|
@@ -57,7 +69,7 @@ async function bitsoRequest(method: string, path: string, body?: unknown): Promi
|
|
|
57
69
|
}
|
|
58
70
|
|
|
59
71
|
const server = new Server(
|
|
60
|
-
{ name: "mcp-bitso", version: "0.
|
|
72
|
+
{ name: "mcp-bitso", version: "0.2.0" },
|
|
61
73
|
{ capabilities: { tools: {} } }
|
|
62
74
|
);
|
|
63
75
|
|
|
@@ -177,6 +189,110 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
177
189
|
required: ["currency", "amount"],
|
|
178
190
|
},
|
|
179
191
|
},
|
|
192
|
+
{
|
|
193
|
+
name: "list_ledger",
|
|
194
|
+
description: "List account ledger entries (trades, fees, fundings, withdrawals)",
|
|
195
|
+
inputSchema: {
|
|
196
|
+
type: "object",
|
|
197
|
+
properties: {
|
|
198
|
+
operation: { type: "string", enum: ["trade", "fee", "funding", "withdrawal"], description: "Filter by operation type" },
|
|
199
|
+
limit: { type: "number", description: "Number of results (default 25, max 100)" },
|
|
200
|
+
marker: { type: "string", description: "Pagination marker" },
|
|
201
|
+
sort: { type: "string", enum: ["asc", "desc"], description: "Sort direction" },
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
name: "list_open_orders",
|
|
207
|
+
description: "List currently open orders for the authenticated user",
|
|
208
|
+
inputSchema: {
|
|
209
|
+
type: "object",
|
|
210
|
+
properties: {
|
|
211
|
+
book: { type: "string", description: "Filter by order book symbol (e.g. btc_mxn)" },
|
|
212
|
+
marker: { type: "string", description: "Pagination marker" },
|
|
213
|
+
sort: { type: "string", enum: ["asc", "desc"], description: "Sort direction" },
|
|
214
|
+
limit: { type: "number", description: "Number of results (default 25, max 100)" },
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
name: "lookup_order",
|
|
220
|
+
description: "Look up one or more orders by origin_id (client_id)",
|
|
221
|
+
inputSchema: {
|
|
222
|
+
type: "object",
|
|
223
|
+
properties: {
|
|
224
|
+
origin_ids: { type: "string", description: "Comma-separated origin_id values (client IDs)" },
|
|
225
|
+
},
|
|
226
|
+
required: ["origin_ids"],
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
name: "cancel_all_orders",
|
|
231
|
+
description: "Cancel all open orders for the authenticated user",
|
|
232
|
+
inputSchema: { type: "object", properties: {} },
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
name: "list_fundings",
|
|
236
|
+
description: "List account fundings (deposits)",
|
|
237
|
+
inputSchema: {
|
|
238
|
+
type: "object",
|
|
239
|
+
properties: {
|
|
240
|
+
fids: { type: "string", description: "Comma-separated funding IDs to filter" },
|
|
241
|
+
status: { type: "string", enum: ["pending", "complete", "cancelled"], description: "Filter by status" },
|
|
242
|
+
method: { type: "string", description: "Funding method (e.g. spei, bitcoin)" },
|
|
243
|
+
limit: { type: "number", description: "Number of results (default 25, max 100)" },
|
|
244
|
+
marker: { type: "string", description: "Pagination marker" },
|
|
245
|
+
sort: { type: "string", enum: ["asc", "desc"], description: "Sort direction" },
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
name: "list_withdrawals",
|
|
251
|
+
description: "List account withdrawals",
|
|
252
|
+
inputSchema: {
|
|
253
|
+
type: "object",
|
|
254
|
+
properties: {
|
|
255
|
+
wids: { type: "string", description: "Comma-separated withdrawal IDs to filter" },
|
|
256
|
+
status: { type: "string", enum: ["pending", "processing", "complete", "failed"], description: "Filter by status" },
|
|
257
|
+
method: { type: "string", description: "Withdrawal method (e.g. spei, bitcoin)" },
|
|
258
|
+
limit: { type: "number", description: "Number of results (default 25, max 100)" },
|
|
259
|
+
marker: { type: "string", description: "Pagination marker" },
|
|
260
|
+
sort: { type: "string", enum: ["asc", "desc"], description: "Sort direction" },
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
name: "get_withdrawal",
|
|
266
|
+
description: "Retrieve a specific withdrawal by its ID",
|
|
267
|
+
inputSchema: {
|
|
268
|
+
type: "object",
|
|
269
|
+
properties: {
|
|
270
|
+
wid: { type: "string", description: "Withdrawal ID" },
|
|
271
|
+
},
|
|
272
|
+
required: ["wid"],
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
name: "list_fees",
|
|
277
|
+
description: "List applicable fees for the authenticated user across trading pairs",
|
|
278
|
+
inputSchema: { type: "object", properties: {} },
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
name: "get_account_status",
|
|
282
|
+
description: "Retrieve account KYC and verification status (tier, limits, required docs)",
|
|
283
|
+
inputSchema: { type: "object", properties: {} },
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
name: "list_funding_destinations",
|
|
287
|
+
description: "Get funding destination details (address/CLABE) for a given currency",
|
|
288
|
+
inputSchema: {
|
|
289
|
+
type: "object",
|
|
290
|
+
properties: {
|
|
291
|
+
fund_currency: { type: "string", description: "Currency code (e.g. btc, mxn, ars)" },
|
|
292
|
+
},
|
|
293
|
+
required: ["fund_currency"],
|
|
294
|
+
},
|
|
295
|
+
},
|
|
180
296
|
],
|
|
181
297
|
}));
|
|
182
298
|
|
|
@@ -222,6 +338,61 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
222
338
|
return { content: [{ type: "text", text: JSON.stringify(await bitsoRequest("GET", "/funding_destination"), null, 2) }] };
|
|
223
339
|
case "create_withdrawal":
|
|
224
340
|
return { content: [{ type: "text", text: JSON.stringify(await bitsoRequest("POST", "/withdrawals", args), null, 2) }] };
|
|
341
|
+
case "list_ledger": {
|
|
342
|
+
const params = new URLSearchParams();
|
|
343
|
+
if (args?.limit) params.set("limit", String(args.limit));
|
|
344
|
+
if (args?.marker) params.set("marker", String(args.marker));
|
|
345
|
+
if (args?.sort) params.set("sort", String(args.sort));
|
|
346
|
+
const op = args?.operation ? `/${args.operation}s` : "";
|
|
347
|
+
const qs = params.toString();
|
|
348
|
+
return { content: [{ type: "text", text: JSON.stringify(await bitsoRequest("GET", `/ledger${op}${qs ? `?${qs}` : ""}`), null, 2) }] };
|
|
349
|
+
}
|
|
350
|
+
case "list_open_orders": {
|
|
351
|
+
const params = new URLSearchParams();
|
|
352
|
+
if (args?.book) params.set("book", String(args.book));
|
|
353
|
+
if (args?.limit) params.set("limit", String(args.limit));
|
|
354
|
+
if (args?.marker) params.set("marker", String(args.marker));
|
|
355
|
+
if (args?.sort) params.set("sort", String(args.sort));
|
|
356
|
+
const qs = params.toString();
|
|
357
|
+
return { content: [{ type: "text", text: JSON.stringify(await bitsoRequest("GET", `/open_orders${qs ? `?${qs}` : ""}`), null, 2) }] };
|
|
358
|
+
}
|
|
359
|
+
case "lookup_order":
|
|
360
|
+
return { content: [{ type: "text", text: JSON.stringify(await bitsoRequest("GET", `/orders/${args?.origin_ids}`), null, 2) }] };
|
|
361
|
+
case "cancel_all_orders":
|
|
362
|
+
return { content: [{ type: "text", text: JSON.stringify(await bitsoRequest("DELETE", "/orders/all"), null, 2) }] };
|
|
363
|
+
case "list_fundings": {
|
|
364
|
+
const params = new URLSearchParams();
|
|
365
|
+
if (args?.status) params.set("status", String(args.status));
|
|
366
|
+
if (args?.method) params.set("method", String(args.method));
|
|
367
|
+
if (args?.limit) params.set("limit", String(args.limit));
|
|
368
|
+
if (args?.marker) params.set("marker", String(args.marker));
|
|
369
|
+
if (args?.sort) params.set("sort", String(args.sort));
|
|
370
|
+
const suffix = args?.fids ? `/${args.fids}` : "";
|
|
371
|
+
const qs = params.toString();
|
|
372
|
+
return { content: [{ type: "text", text: JSON.stringify(await bitsoRequest("GET", `/fundings${suffix}${qs ? `?${qs}` : ""}`), null, 2) }] };
|
|
373
|
+
}
|
|
374
|
+
case "list_withdrawals": {
|
|
375
|
+
const params = new URLSearchParams();
|
|
376
|
+
if (args?.status) params.set("status", String(args.status));
|
|
377
|
+
if (args?.method) params.set("method", String(args.method));
|
|
378
|
+
if (args?.limit) params.set("limit", String(args.limit));
|
|
379
|
+
if (args?.marker) params.set("marker", String(args.marker));
|
|
380
|
+
if (args?.sort) params.set("sort", String(args.sort));
|
|
381
|
+
const suffix = args?.wids ? `/${args.wids}` : "";
|
|
382
|
+
const qs = params.toString();
|
|
383
|
+
return { content: [{ type: "text", text: JSON.stringify(await bitsoRequest("GET", `/withdrawals${suffix}${qs ? `?${qs}` : ""}`), null, 2) }] };
|
|
384
|
+
}
|
|
385
|
+
case "get_withdrawal":
|
|
386
|
+
return { content: [{ type: "text", text: JSON.stringify(await bitsoRequest("GET", `/withdrawals/${args?.wid}`), null, 2) }] };
|
|
387
|
+
case "list_fees":
|
|
388
|
+
return { content: [{ type: "text", text: JSON.stringify(await bitsoRequest("GET", "/fees"), null, 2) }] };
|
|
389
|
+
case "get_account_status":
|
|
390
|
+
return { content: [{ type: "text", text: JSON.stringify(await bitsoRequest("GET", "/account_status"), null, 2) }] };
|
|
391
|
+
case "list_funding_destinations": {
|
|
392
|
+
const params = new URLSearchParams();
|
|
393
|
+
params.set("fund_currency", String(args?.fund_currency));
|
|
394
|
+
return { content: [{ type: "text", text: JSON.stringify(await bitsoRequest("GET", `/funding_destination?${params}`), null, 2) }] };
|
|
395
|
+
}
|
|
225
396
|
default:
|
|
226
397
|
return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
|
|
227
398
|
}
|
|
@@ -231,16 +402,32 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
231
402
|
});
|
|
232
403
|
|
|
233
404
|
async function main() {
|
|
234
|
-
if (
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
405
|
+
if (process.argv.includes("--http") || process.env.MCP_HTTP === "true") {
|
|
406
|
+
const { default: express } = await import("express");
|
|
407
|
+
const { randomUUID } = await import("node:crypto");
|
|
408
|
+
const app = express();
|
|
409
|
+
app.use(express.json());
|
|
410
|
+
const transports = new Map<string, StreamableHTTPServerTransport>();
|
|
411
|
+
app.get("/health", (_req: any, res: any) => res.json({ status: "ok", sessions: transports.size }));
|
|
412
|
+
app.post("/mcp", async (req: any, res: any) => {
|
|
413
|
+
const sid = req.headers["mcp-session-id"] as string | undefined;
|
|
414
|
+
if (sid && transports.has(sid)) { await transports.get(sid)!.handleRequest(req, res, req.body); return; }
|
|
415
|
+
if (!sid && isInitializeRequest(req.body)) {
|
|
416
|
+
const t = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (id) => { transports.set(id, t); } });
|
|
417
|
+
t.onclose = () => { if (t.sessionId) transports.delete(t.sessionId); };
|
|
418
|
+
const s = new Server({ name: "mcp-bitso", version: "0.2.0" }, { capabilities: { tools: {} } }); (server as any)._requestHandlers.forEach((v: any, k: any) => (s as any)._requestHandlers.set(k, v)); (server as any)._notificationHandlers?.forEach((v: any, k: any) => (s as any)._notificationHandlers.set(k, v)); await s.connect(t);
|
|
419
|
+
await t.handleRequest(req, res, req.body); return;
|
|
420
|
+
}
|
|
421
|
+
res.status(400).json({ jsonrpc: "2.0", error: { code: -32000, message: "Bad Request" }, id: null });
|
|
422
|
+
});
|
|
423
|
+
app.get("/mcp", async (req: any, res: any) => { const sid = req.headers["mcp-session-id"] as string; if (sid && transports.has(sid)) await transports.get(sid)!.handleRequest(req, res); else res.status(400).send("Invalid session"); });
|
|
424
|
+
app.delete("/mcp", async (req: any, res: any) => { const sid = req.headers["mcp-session-id"] as string; if (sid && transports.has(sid)) await transports.get(sid)!.handleRequest(req, res); else res.status(400).send("Invalid session"); });
|
|
425
|
+
const port = Number(process.env.MCP_PORT) || 3000;
|
|
426
|
+
app.listen(port, () => { console.error(`MCP HTTP server on http://localhost:${port}/mcp`); });
|
|
427
|
+
} else {
|
|
428
|
+
const transport = new StdioServerTransport();
|
|
429
|
+
await server.connect(transport);
|
|
241
430
|
}
|
|
242
|
-
const transport = new StdioServerTransport();
|
|
243
|
-
await server.connect(transport);
|
|
244
431
|
}
|
|
245
432
|
|
|
246
433
|
main().catch(console.error);
|