@codespar/mcp-bitso
Advanced tools
+30
| { | ||
| "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", | ||
| "name": "io.github.codespar/mcp-bitso", | ||
| "description": "MCP server for Bitso — Latin American crypto exchange, trading, funding, withdrawals", | ||
| "repository": { | ||
| "url": "https://github.com/codespar/mcp-dev-brasil", | ||
| "source": "github", | ||
| "subfolder": "packages/crypto/bitso" | ||
| }, | ||
| "version": "0.1.2", | ||
| "packages": [ | ||
| { | ||
| "registryType": "npm", | ||
| "identifier": "@codespar/mcp-bitso", | ||
| "version": "0.1.2", | ||
| "transport": { | ||
| "type": "stdio" | ||
| }, | ||
| "environmentVariables": [ | ||
| { | ||
| "name": "BITSO_API_KEY", | ||
| "description": "API key for bitso", | ||
| "isRequired": true, | ||
| "format": "string", | ||
| "isSecret": true | ||
| } | ||
| ] | ||
| } | ||
| ] | ||
| } |
| import { describe, it, expect, vi, beforeEach } from "vitest"; | ||
| let listToolsHandler: Function; | ||
| let callToolHandler: Function; | ||
| vi.mock("@modelcontextprotocol/sdk/server/index.js", () => { | ||
| class FakeServer { | ||
| constructor() {} | ||
| setRequestHandler(schema: any, handler: Function) { | ||
| if (JSON.stringify(schema).includes("tools/list")) listToolsHandler = handler; | ||
| if (JSON.stringify(schema).includes("tools/call")) callToolHandler = handler; | ||
| } | ||
| connect() { return Promise.resolve(); } | ||
| } | ||
| return { Server: FakeServer }; | ||
| }); | ||
| vi.mock("@modelcontextprotocol/sdk/server/stdio.js", () => ({ StdioServerTransport: class {} })); | ||
| process.env.BITSO_API_KEY = "test-key"; | ||
| process.env.BITSO_API_SECRET = "test-secret"; | ||
| const mockFetch = vi.fn(); | ||
| global.fetch = mockFetch as any; | ||
| beforeEach(async () => { | ||
| vi.resetModules(); | ||
| listToolsHandler = undefined as any; | ||
| callToolHandler = undefined as any; | ||
| mockFetch.mockReset(); | ||
| global.fetch = mockFetch as any; | ||
| await import("../index.js"); | ||
| }); | ||
| describe("mcp-bitso", () => { | ||
| it("should register 10 tools", async () => { | ||
| const result = await listToolsHandler(); | ||
| expect(result.tools).toHaveLength(10); | ||
| }); | ||
| it("should call correct API endpoint for get_ticker", async () => { | ||
| mockFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ success: true, payload: { last: "100000" } }) }); | ||
| await callToolHandler({ params: { name: "get_ticker", arguments: { book: "btc_brl" } } }); | ||
| const [url] = mockFetch.mock.calls[0]; | ||
| expect(url).toContain("api.bitso.com/v3/ticker"); | ||
| expect(url).toContain("book=btc_brl"); | ||
| }); | ||
| }); |
+41
-8
@@ -23,2 +23,4 @@ #!/usr/bin/env node | ||
| import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; | ||
| import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; | ||
| import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js"; | ||
| import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; | ||
@@ -229,13 +231,44 @@ import * as crypto from "node:crypto"; | ||
| async function main() { | ||
| if (!API_KEY) { | ||
| console.error("BITSO_API_KEY environment variable is required"); | ||
| process.exit(1); | ||
| if (process.argv.includes("--http") || process.env.MCP_HTTP === "true") { | ||
| const { default: express } = await import("express"); | ||
| const { randomUUID } = await import("node:crypto"); | ||
| const app = express(); | ||
| app.use(express.json()); | ||
| const transports = new Map(); | ||
| app.get("/health", (_req, res) => res.json({ status: "ok", sessions: transports.size })); | ||
| app.post("/mcp", async (req, res) => { | ||
| const sid = req.headers["mcp-session-id"]; | ||
| if (sid && transports.has(sid)) { | ||
| await transports.get(sid).handleRequest(req, res, req.body); | ||
| return; | ||
| } | ||
| if (!sid && isInitializeRequest(req.body)) { | ||
| const t = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (id) => { transports.set(id, t); } }); | ||
| t.onclose = () => { if (t.sessionId) | ||
| transports.delete(t.sessionId); }; | ||
| const s = new Server({ name: "mcp-bitso", version: "0.1.0" }, { capabilities: { tools: {} } }); | ||
| server._requestHandlers.forEach((v, k) => s._requestHandlers.set(k, v)); | ||
| server._notificationHandlers?.forEach((v, k) => s._notificationHandlers.set(k, v)); | ||
| await s.connect(t); | ||
| await t.handleRequest(req, res, req.body); | ||
| return; | ||
| } | ||
| res.status(400).json({ jsonrpc: "2.0", error: { code: -32000, message: "Bad Request" }, id: null }); | ||
| }); | ||
| app.get("/mcp", async (req, res) => { const sid = req.headers["mcp-session-id"]; if (sid && transports.has(sid)) | ||
| await transports.get(sid).handleRequest(req, res); | ||
| else | ||
| res.status(400).send("Invalid session"); }); | ||
| app.delete("/mcp", async (req, res) => { const sid = req.headers["mcp-session-id"]; if (sid && transports.has(sid)) | ||
| await transports.get(sid).handleRequest(req, res); | ||
| else | ||
| res.status(400).send("Invalid session"); }); | ||
| const port = Number(process.env.MCP_PORT) || 3000; | ||
| app.listen(port, () => { console.error(`MCP HTTP server on http://localhost:${port}/mcp`); }); | ||
| } | ||
| if (!API_SECRET) { | ||
| console.error("BITSO_API_SECRET environment variable is required"); | ||
| process.exit(1); | ||
| else { | ||
| const transport = new StdioServerTransport(); | ||
| await server.connect(transport); | ||
| } | ||
| const transport = new StdioServerTransport(); | ||
| await server.connect(transport); | ||
| } | ||
| main().catch(console.error); |
+3
-2
| { | ||
| "name": "@codespar/mcp-bitso", | ||
| "version": "0.1.0", | ||
| "version": "0.1.2", | ||
| "description": "MCP server for Bitso — Latin American crypto exchange, trading, funding, withdrawals", | ||
@@ -29,3 +29,4 @@ "type": "module", | ||
| "latam" | ||
| ] | ||
| ], | ||
| "mcpName": "io.github.codespar/mcp-bitso" | ||
| } |
+4
-0
@@ -113,4 +113,8 @@ # @codespar/mcp-bitso | ||
| ## Enterprise | ||
| 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. | ||
| ## License | ||
| MIT |
+27
-9
@@ -25,2 +25,4 @@ #!/usr/bin/env node | ||
| import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; | ||
| import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; | ||
| import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js"; | ||
| import { | ||
@@ -235,14 +237,30 @@ CallToolRequestSchema, | ||
| async function main() { | ||
| if (!API_KEY) { | ||
| console.error("BITSO_API_KEY environment variable is required"); | ||
| process.exit(1); | ||
| if (process.argv.includes("--http") || process.env.MCP_HTTP === "true") { | ||
| const { default: express } = await import("express"); | ||
| const { randomUUID } = await import("node:crypto"); | ||
| const app = express(); | ||
| app.use(express.json()); | ||
| const transports = new Map<string, StreamableHTTPServerTransport>(); | ||
| app.get("/health", (_req: any, res: any) => res.json({ status: "ok", sessions: transports.size })); | ||
| app.post("/mcp", async (req: any, res: any) => { | ||
| const sid = req.headers["mcp-session-id"] as string | undefined; | ||
| if (sid && transports.has(sid)) { await transports.get(sid)!.handleRequest(req, res, req.body); return; } | ||
| if (!sid && isInitializeRequest(req.body)) { | ||
| const t = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (id) => { transports.set(id, t); } }); | ||
| t.onclose = () => { if (t.sessionId) transports.delete(t.sessionId); }; | ||
| 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); | ||
| await t.handleRequest(req, res, req.body); return; | ||
| } | ||
| res.status(400).json({ jsonrpc: "2.0", error: { code: -32000, message: "Bad Request" }, id: null }); | ||
| }); | ||
| 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"); }); | ||
| 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"); }); | ||
| const port = Number(process.env.MCP_PORT) || 3000; | ||
| app.listen(port, () => { console.error(`MCP HTTP server on http://localhost:${port}/mcp`); }); | ||
| } else { | ||
| const transport = new StdioServerTransport(); | ||
| await server.connect(transport); | ||
| } | ||
| if (!API_SECRET) { | ||
| console.error("BITSO_API_SECRET environment variable is required"); | ||
| process.exit(1); | ||
| } | ||
| const transport = new StdioServerTransport(); | ||
| await server.connect(transport); | ||
| } | ||
| main().catch(console.error); |
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 4 instances in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
31711
28.16%7
40%610
25%120
3.45%12
100%4
100%