@codespar/mcp-bitso 0.1.0 → 0.1.2
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 +41 -8
- package/package.json +3 -2
- package/server.json +30 -0
- package/src/__tests__/index.test.ts +50 -0
- package/src/index.ts +27 -9
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
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
22
22
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
23
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
24
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
23
25
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
24
26
|
import * as crypto from "node:crypto";
|
|
25
27
|
const API_KEY = process.env.BITSO_API_KEY || "";
|
|
@@ -225,15 +227,46 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
225
227
|
}
|
|
226
228
|
});
|
|
227
229
|
async function main() {
|
|
228
|
-
if (
|
|
229
|
-
|
|
230
|
-
|
|
230
|
+
if (process.argv.includes("--http") || process.env.MCP_HTTP === "true") {
|
|
231
|
+
const { default: express } = await import("express");
|
|
232
|
+
const { randomUUID } = await import("node:crypto");
|
|
233
|
+
const app = express();
|
|
234
|
+
app.use(express.json());
|
|
235
|
+
const transports = new Map();
|
|
236
|
+
app.get("/health", (_req, res) => res.json({ status: "ok", sessions: transports.size }));
|
|
237
|
+
app.post("/mcp", async (req, res) => {
|
|
238
|
+
const sid = req.headers["mcp-session-id"];
|
|
239
|
+
if (sid && transports.has(sid)) {
|
|
240
|
+
await transports.get(sid).handleRequest(req, res, req.body);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (!sid && isInitializeRequest(req.body)) {
|
|
244
|
+
const t = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (id) => { transports.set(id, t); } });
|
|
245
|
+
t.onclose = () => { if (t.sessionId)
|
|
246
|
+
transports.delete(t.sessionId); };
|
|
247
|
+
const s = new Server({ name: "mcp-bitso", version: "0.1.0" }, { capabilities: { tools: {} } });
|
|
248
|
+
server._requestHandlers.forEach((v, k) => s._requestHandlers.set(k, v));
|
|
249
|
+
server._notificationHandlers?.forEach((v, k) => s._notificationHandlers.set(k, v));
|
|
250
|
+
await s.connect(t);
|
|
251
|
+
await t.handleRequest(req, res, req.body);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
res.status(400).json({ jsonrpc: "2.0", error: { code: -32000, message: "Bad Request" }, id: null });
|
|
255
|
+
});
|
|
256
|
+
app.get("/mcp", async (req, res) => { const sid = req.headers["mcp-session-id"]; if (sid && transports.has(sid))
|
|
257
|
+
await transports.get(sid).handleRequest(req, res);
|
|
258
|
+
else
|
|
259
|
+
res.status(400).send("Invalid session"); });
|
|
260
|
+
app.delete("/mcp", async (req, res) => { const sid = req.headers["mcp-session-id"]; if (sid && transports.has(sid))
|
|
261
|
+
await transports.get(sid).handleRequest(req, res);
|
|
262
|
+
else
|
|
263
|
+
res.status(400).send("Invalid session"); });
|
|
264
|
+
const port = Number(process.env.MCP_PORT) || 3000;
|
|
265
|
+
app.listen(port, () => { console.error(`MCP HTTP server on http://localhost:${port}/mcp`); });
|
|
231
266
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
267
|
+
else {
|
|
268
|
+
const transport = new StdioServerTransport();
|
|
269
|
+
await server.connect(transport);
|
|
235
270
|
}
|
|
236
|
-
const transport = new StdioServerTransport();
|
|
237
|
-
await server.connect(transport);
|
|
238
271
|
}
|
|
239
272
|
main().catch(console.error);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codespar/mcp-bitso",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
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.1.2",
|
|
11
|
+
"packages": [
|
|
12
|
+
{
|
|
13
|
+
"registryType": "npm",
|
|
14
|
+
"identifier": "@codespar/mcp-bitso",
|
|
15
|
+
"version": "0.1.2",
|
|
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
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
|
|
23
23
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
24
24
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
25
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
26
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
25
27
|
import {
|
|
26
28
|
CallToolRequestSchema,
|
|
27
29
|
ListToolsRequestSchema,
|
|
@@ -231,16 +233,32 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
231
233
|
});
|
|
232
234
|
|
|
233
235
|
async function main() {
|
|
234
|
-
if (
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
236
|
+
if (process.argv.includes("--http") || process.env.MCP_HTTP === "true") {
|
|
237
|
+
const { default: express } = await import("express");
|
|
238
|
+
const { randomUUID } = await import("node:crypto");
|
|
239
|
+
const app = express();
|
|
240
|
+
app.use(express.json());
|
|
241
|
+
const transports = new Map<string, StreamableHTTPServerTransport>();
|
|
242
|
+
app.get("/health", (_req: any, res: any) => res.json({ status: "ok", sessions: transports.size }));
|
|
243
|
+
app.post("/mcp", async (req: any, res: any) => {
|
|
244
|
+
const sid = req.headers["mcp-session-id"] as string | undefined;
|
|
245
|
+
if (sid && transports.has(sid)) { await transports.get(sid)!.handleRequest(req, res, req.body); return; }
|
|
246
|
+
if (!sid && isInitializeRequest(req.body)) {
|
|
247
|
+
const t = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (id) => { transports.set(id, t); } });
|
|
248
|
+
t.onclose = () => { if (t.sessionId) transports.delete(t.sessionId); };
|
|
249
|
+
const s = new Server({ name: "mcp-bitso", version: "0.1.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);
|
|
250
|
+
await t.handleRequest(req, res, req.body); return;
|
|
251
|
+
}
|
|
252
|
+
res.status(400).json({ jsonrpc: "2.0", error: { code: -32000, message: "Bad Request" }, id: null });
|
|
253
|
+
});
|
|
254
|
+
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"); });
|
|
255
|
+
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"); });
|
|
256
|
+
const port = Number(process.env.MCP_PORT) || 3000;
|
|
257
|
+
app.listen(port, () => { console.error(`MCP HTTP server on http://localhost:${port}/mcp`); });
|
|
258
|
+
} else {
|
|
259
|
+
const transport = new StdioServerTransport();
|
|
260
|
+
await server.connect(transport);
|
|
241
261
|
}
|
|
242
|
-
const transport = new StdioServerTransport();
|
|
243
|
-
await server.connect(transport);
|
|
244
262
|
}
|
|
245
263
|
|
|
246
264
|
main().catch(console.error);
|