@codespar/mcp-foxbit 0.1.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 +130 -0
- package/dist/index.js +262 -0
- package/package.json +31 -0
- package/server.json +37 -0
- package/src/index.ts +269 -0
- package/tsconfig.json +14 -0
package/README.md
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# @codespar/mcp-foxbit
|
|
2
|
+
|
|
3
|
+
> MCP server for **Foxbit** — Brazilian cryptocurrency exchange with trading, orderbook, and market data
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@codespar/mcp-foxbit)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
## Quick Start
|
|
9
|
+
|
|
10
|
+
### Claude Desktop
|
|
11
|
+
|
|
12
|
+
Add to `~/.config/claude/claude_desktop_config.json`:
|
|
13
|
+
|
|
14
|
+
```json
|
|
15
|
+
{
|
|
16
|
+
"mcpServers": {
|
|
17
|
+
"foxbit": {
|
|
18
|
+
"command": "npx",
|
|
19
|
+
"args": ["-y", "@codespar/mcp-foxbit"],
|
|
20
|
+
"env": {
|
|
21
|
+
"FOXBIT_API_KEY": "your-key",
|
|
22
|
+
"FOXBIT_API_SECRET": "your-secret"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Claude Code
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
claude mcp add foxbit -- npx @codespar/mcp-foxbit
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Cursor / VS Code
|
|
36
|
+
|
|
37
|
+
Add to `.cursor/mcp.json` or `.vscode/mcp.json`:
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"servers": {
|
|
42
|
+
"foxbit": {
|
|
43
|
+
"command": "npx",
|
|
44
|
+
"args": ["-y", "@codespar/mcp-foxbit"],
|
|
45
|
+
"env": {
|
|
46
|
+
"FOXBIT_API_KEY": "your-key",
|
|
47
|
+
"FOXBIT_API_SECRET": "your-secret"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Tools
|
|
55
|
+
|
|
56
|
+
| Tool | Description |
|
|
57
|
+
|------|-------------|
|
|
58
|
+
| `list_markets` | List all available trading pairs |
|
|
59
|
+
| `get_ticker` | Get 24h ticker data for a market |
|
|
60
|
+
| `get_orderbook` | Get order book for a market |
|
|
61
|
+
| `get_account_balances` | Get account balances |
|
|
62
|
+
| `create_order` | Create a buy or sell order (limit/market) |
|
|
63
|
+
| `get_order` | Get order details by ID |
|
|
64
|
+
| `list_orders` | List orders with filters |
|
|
65
|
+
| `cancel_order` | Cancel an open order |
|
|
66
|
+
| `list_trades` | List executed trades |
|
|
67
|
+
| `list_deposits_withdrawals` | List deposits and withdrawals for a currency |
|
|
68
|
+
|
|
69
|
+
## Authentication
|
|
70
|
+
|
|
71
|
+
Foxbit uses HMAC-SHA256 request signing. Each request includes three headers:
|
|
72
|
+
|
|
73
|
+
- `X-FB-ACCESS-KEY` — API key
|
|
74
|
+
- `X-FB-ACCESS-TIMESTAMP` — UNIX timestamp in milliseconds
|
|
75
|
+
- `X-FB-ACCESS-SIGNATURE` — hex HMAC-SHA256 of `timestamp + method + path + queryString + body` using API secret
|
|
76
|
+
|
|
77
|
+
Base URL: `https://api.foxbit.com.br/rest/v3`
|
|
78
|
+
|
|
79
|
+
### Get your credentials
|
|
80
|
+
|
|
81
|
+
1. Go to [Foxbit](https://app.foxbit.com.br)
|
|
82
|
+
2. Create an account (KYC required for Brazilian residents)
|
|
83
|
+
3. Navigate to API settings to generate key and secret
|
|
84
|
+
4. Set the environment variables
|
|
85
|
+
|
|
86
|
+
## Environment Variables
|
|
87
|
+
|
|
88
|
+
| Variable | Required | Description |
|
|
89
|
+
|----------|----------|-------------|
|
|
90
|
+
| `FOXBIT_API_KEY` | Yes | API key from Foxbit |
|
|
91
|
+
| `FOXBIT_API_SECRET` | Yes | API secret for HMAC-SHA256 |
|
|
92
|
+
|
|
93
|
+
## Brazilian Crypto Exchanges in CodeSpar
|
|
94
|
+
|
|
95
|
+
Hedge liquidity across multiple BR venues:
|
|
96
|
+
|
|
97
|
+
- **[Mercado Bitcoin](../mercado-bitcoin)** — biggest BR exchange, 200+ tokens, deep altcoin coverage
|
|
98
|
+
- **Foxbit (this)** — 2nd BR exchange, focus on BTC / ETH / LTC, strong institutional desk
|
|
99
|
+
|
|
100
|
+
Merchants and traders use both for best execution and redundancy.
|
|
101
|
+
|
|
102
|
+
## Roadmap
|
|
103
|
+
|
|
104
|
+
### v0.2 (planned)
|
|
105
|
+
- `get_candles` — OHLCV candlestick data
|
|
106
|
+
- `create_withdrawal` — Initiate crypto/PIX withdrawal
|
|
107
|
+
- `list_currencies` — Available currencies and networks
|
|
108
|
+
- `get_fees` — Trading fees for account tier
|
|
109
|
+
- `create_stop_order` — Stop-limit / stop-market orders
|
|
110
|
+
|
|
111
|
+
### v0.3 (planned)
|
|
112
|
+
- Institutional / OTC desk integrations
|
|
113
|
+
- WebSocket market data streams (where MCP transport allows)
|
|
114
|
+
|
|
115
|
+
Want to contribute? [Open a PR](https://github.com/codespar/mcp-dev-brasil) or [request a tool](https://github.com/codespar/mcp-dev-brasil/issues).
|
|
116
|
+
|
|
117
|
+
## Links
|
|
118
|
+
|
|
119
|
+
- [Foxbit Website](https://foxbit.com.br)
|
|
120
|
+
- [Foxbit API Documentation](https://docs.foxbit.com.br)
|
|
121
|
+
- [MCP Dev Brasil](https://github.com/codespar/mcp-dev-brasil)
|
|
122
|
+
- [Landing Page](https://codespar.dev/mcp)
|
|
123
|
+
|
|
124
|
+
## Enterprise
|
|
125
|
+
|
|
126
|
+
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.
|
|
127
|
+
|
|
128
|
+
## License
|
|
129
|
+
|
|
130
|
+
MIT
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* MCP Server for Foxbit — Brazilian cryptocurrency exchange.
|
|
4
|
+
*
|
|
5
|
+
* Tools:
|
|
6
|
+
* - list_markets: List all available trading pairs
|
|
7
|
+
* - get_ticker: Get 24h ticker data for a market
|
|
8
|
+
* - get_orderbook: Get order book for a market
|
|
9
|
+
* - get_account_balances: Get account balances
|
|
10
|
+
* - create_order: Create a buy or sell order (limit/market)
|
|
11
|
+
* - get_order: Get order details by ID
|
|
12
|
+
* - list_orders: List orders with filters
|
|
13
|
+
* - cancel_order: Cancel an open order
|
|
14
|
+
* - list_trades: List executed trades
|
|
15
|
+
* - list_deposits_withdrawals: List deposits and withdrawals for a currency
|
|
16
|
+
*
|
|
17
|
+
* Environment:
|
|
18
|
+
* FOXBIT_API_KEY — API key from https://app.foxbit.com.br/
|
|
19
|
+
* FOXBIT_API_SECRET — API secret for HMAC-SHA256 signature
|
|
20
|
+
*/
|
|
21
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
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";
|
|
25
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
26
|
+
import * as crypto from "node:crypto";
|
|
27
|
+
const API_KEY = process.env.FOXBIT_API_KEY || "";
|
|
28
|
+
const API_SECRET = process.env.FOXBIT_API_SECRET || "";
|
|
29
|
+
const BASE_URL = "https://api.foxbit.com.br";
|
|
30
|
+
const PATH_PREFIX = "/rest/v3";
|
|
31
|
+
async function foxbitRequest(method, path, query, body) {
|
|
32
|
+
const timestamp = Date.now().toString();
|
|
33
|
+
const bodyStr = body ? JSON.stringify(body) : "";
|
|
34
|
+
const params = new URLSearchParams();
|
|
35
|
+
if (query) {
|
|
36
|
+
for (const [k, v] of Object.entries(query)) {
|
|
37
|
+
if (v !== undefined && v !== null && v !== "")
|
|
38
|
+
params.set(k, String(v));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const queryString = params.toString();
|
|
42
|
+
const fullPath = `${PATH_PREFIX}${path}`;
|
|
43
|
+
const prehash = timestamp + method.toUpperCase() + fullPath + queryString + bodyStr;
|
|
44
|
+
const signature = crypto.createHmac("sha256", API_SECRET).update(prehash).digest("hex");
|
|
45
|
+
const url = `${BASE_URL}${fullPath}${queryString ? `?${queryString}` : ""}`;
|
|
46
|
+
const res = await fetch(url, {
|
|
47
|
+
method,
|
|
48
|
+
headers: {
|
|
49
|
+
"Content-Type": "application/json",
|
|
50
|
+
"X-FB-ACCESS-KEY": API_KEY,
|
|
51
|
+
"X-FB-ACCESS-TIMESTAMP": timestamp,
|
|
52
|
+
"X-FB-ACCESS-SIGNATURE": signature,
|
|
53
|
+
},
|
|
54
|
+
body: bodyStr || undefined,
|
|
55
|
+
});
|
|
56
|
+
if (!res.ok) {
|
|
57
|
+
const err = await res.text();
|
|
58
|
+
throw new Error(`Foxbit API ${res.status}: ${err}`);
|
|
59
|
+
}
|
|
60
|
+
return res.json();
|
|
61
|
+
}
|
|
62
|
+
const server = new Server({ name: "mcp-foxbit", version: "0.1.0" }, { capabilities: { tools: {} } });
|
|
63
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
64
|
+
tools: [
|
|
65
|
+
{
|
|
66
|
+
name: "list_markets",
|
|
67
|
+
description: "List all available trading pairs / markets on Foxbit",
|
|
68
|
+
inputSchema: { type: "object", properties: {} },
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: "get_ticker",
|
|
72
|
+
description: "Get 24h ticker data for a market (price, volume, high/low)",
|
|
73
|
+
inputSchema: {
|
|
74
|
+
type: "object",
|
|
75
|
+
properties: {
|
|
76
|
+
symbol: { type: "string", description: "Market symbol (e.g. btcbrl, ethbrl, ltcbrl)" },
|
|
77
|
+
},
|
|
78
|
+
required: ["symbol"],
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: "get_orderbook",
|
|
83
|
+
description: "Get order book (bids and asks) for a market",
|
|
84
|
+
inputSchema: {
|
|
85
|
+
type: "object",
|
|
86
|
+
properties: {
|
|
87
|
+
symbol: { type: "string", description: "Market symbol (e.g. btcbrl)" },
|
|
88
|
+
depth: { type: "number", description: "Number of price levels per side" },
|
|
89
|
+
},
|
|
90
|
+
required: ["symbol"],
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: "get_account_balances",
|
|
95
|
+
description: "Get account balances for all currencies",
|
|
96
|
+
inputSchema: { type: "object", properties: {} },
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: "create_order",
|
|
100
|
+
description: "Create a buy or sell order (limit or market)",
|
|
101
|
+
inputSchema: {
|
|
102
|
+
type: "object",
|
|
103
|
+
properties: {
|
|
104
|
+
market_symbol: { type: "string", description: "Market symbol (e.g. btcbrl)" },
|
|
105
|
+
side: { type: "string", enum: ["BUY", "SELL"], description: "Order side" },
|
|
106
|
+
type: { type: "string", enum: ["LIMIT", "MARKET", "STOP_LIMIT", "STOP_MARKET"], description: "Order type" },
|
|
107
|
+
quantity: { type: "string", description: "Base asset quantity (e.g. BTC)" },
|
|
108
|
+
price: { type: "string", description: "Limit price (required for LIMIT orders)" },
|
|
109
|
+
client_order_id: { type: "string", description: "Client-supplied order ID" },
|
|
110
|
+
time_in_force: { type: "string", enum: ["GTC", "IOC", "FOK"], description: "Time in force" },
|
|
111
|
+
},
|
|
112
|
+
required: ["market_symbol", "side", "type", "quantity"],
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: "get_order",
|
|
117
|
+
description: "Get order details by ID",
|
|
118
|
+
inputSchema: {
|
|
119
|
+
type: "object",
|
|
120
|
+
properties: {
|
|
121
|
+
id: { type: "string", description: "Order ID" },
|
|
122
|
+
},
|
|
123
|
+
required: ["id"],
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: "list_orders",
|
|
128
|
+
description: "List orders with optional filters",
|
|
129
|
+
inputSchema: {
|
|
130
|
+
type: "object",
|
|
131
|
+
properties: {
|
|
132
|
+
market_symbol: { type: "string", description: "Filter by market symbol" },
|
|
133
|
+
state: { type: "string", enum: ["ACTIVE", "FILLED", "CANCELED", "PARTIALLY_FILLED", "PARTIALLY_CANCELED"], description: "Filter by order state" },
|
|
134
|
+
side: { type: "string", enum: ["BUY", "SELL"], description: "Filter by side" },
|
|
135
|
+
start_time: { type: "string", description: "Start time (ISO 8601)" },
|
|
136
|
+
end_time: { type: "string", description: "End time (ISO 8601)" },
|
|
137
|
+
page_size: { type: "number", description: "Results per page" },
|
|
138
|
+
page: { type: "number", description: "Page number" },
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
name: "cancel_order",
|
|
144
|
+
description: "Cancel an open order by ID",
|
|
145
|
+
inputSchema: {
|
|
146
|
+
type: "object",
|
|
147
|
+
properties: {
|
|
148
|
+
id: { type: "string", description: "Order ID to cancel" },
|
|
149
|
+
},
|
|
150
|
+
required: ["id"],
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
name: "list_trades",
|
|
155
|
+
description: "List user's executed trades",
|
|
156
|
+
inputSchema: {
|
|
157
|
+
type: "object",
|
|
158
|
+
properties: {
|
|
159
|
+
market_symbol: { type: "string", description: "Filter by market symbol" },
|
|
160
|
+
start_time: { type: "string", description: "Start time (ISO 8601)" },
|
|
161
|
+
end_time: { type: "string", description: "End time (ISO 8601)" },
|
|
162
|
+
page_size: { type: "number", description: "Results per page" },
|
|
163
|
+
page: { type: "number", description: "Page number" },
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
name: "list_deposits_withdrawals",
|
|
169
|
+
description: "List deposits and withdrawals (transactions) for a currency",
|
|
170
|
+
inputSchema: {
|
|
171
|
+
type: "object",
|
|
172
|
+
properties: {
|
|
173
|
+
currency_symbol: { type: "string", description: "Currency symbol (e.g. brl, btc, eth)" },
|
|
174
|
+
type: { type: "string", enum: ["deposit", "withdraw"], description: "Filter by transaction type" },
|
|
175
|
+
start_time: { type: "string", description: "Start time (ISO 8601)" },
|
|
176
|
+
end_time: { type: "string", description: "End time (ISO 8601)" },
|
|
177
|
+
page_size: { type: "number", description: "Results per page" },
|
|
178
|
+
page: { type: "number", description: "Page number" },
|
|
179
|
+
},
|
|
180
|
+
required: ["currency_symbol"],
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
}));
|
|
185
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
186
|
+
const { name, arguments: args } = request.params;
|
|
187
|
+
try {
|
|
188
|
+
switch (name) {
|
|
189
|
+
case "list_markets":
|
|
190
|
+
return { content: [{ type: "text", text: JSON.stringify(await foxbitRequest("GET", "/markets"), null, 2) }] };
|
|
191
|
+
case "get_ticker":
|
|
192
|
+
return { content: [{ type: "text", text: JSON.stringify(await foxbitRequest("GET", `/markets/${args?.symbol}/ticker/24hr`), null, 2) }] };
|
|
193
|
+
case "get_orderbook":
|
|
194
|
+
return { content: [{ type: "text", text: JSON.stringify(await foxbitRequest("GET", `/markets/${args?.symbol}/orderbook`, { depth: args?.depth }), null, 2) }] };
|
|
195
|
+
case "get_account_balances":
|
|
196
|
+
return { content: [{ type: "text", text: JSON.stringify(await foxbitRequest("GET", "/accounts"), null, 2) }] };
|
|
197
|
+
case "create_order":
|
|
198
|
+
return { content: [{ type: "text", text: JSON.stringify(await foxbitRequest("POST", "/orders", undefined, args), null, 2) }] };
|
|
199
|
+
case "get_order":
|
|
200
|
+
return { content: [{ type: "text", text: JSON.stringify(await foxbitRequest("GET", `/orders/${args?.id}`), null, 2) }] };
|
|
201
|
+
case "list_orders":
|
|
202
|
+
return { content: [{ type: "text", text: JSON.stringify(await foxbitRequest("GET", "/orders", args), null, 2) }] };
|
|
203
|
+
case "cancel_order":
|
|
204
|
+
return { content: [{ type: "text", text: JSON.stringify(await foxbitRequest("DELETE", `/orders/${args?.id}`), null, 2) }] };
|
|
205
|
+
case "list_trades":
|
|
206
|
+
return { content: [{ type: "text", text: JSON.stringify(await foxbitRequest("GET", "/trades", args), null, 2) }] };
|
|
207
|
+
case "list_deposits_withdrawals": {
|
|
208
|
+
const { currency_symbol, ...rest } = (args || {});
|
|
209
|
+
return { content: [{ type: "text", text: JSON.stringify(await foxbitRequest("GET", `/accounts/${currency_symbol}/transactions`, rest), null, 2) }] };
|
|
210
|
+
}
|
|
211
|
+
default:
|
|
212
|
+
return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
async function main() {
|
|
220
|
+
if (process.argv.includes("--http") || process.env.MCP_HTTP === "true") {
|
|
221
|
+
const { default: express } = await import("express");
|
|
222
|
+
const { randomUUID } = await import("node:crypto");
|
|
223
|
+
const app = express();
|
|
224
|
+
app.use(express.json());
|
|
225
|
+
const transports = new Map();
|
|
226
|
+
app.get("/health", (_req, res) => res.json({ status: "ok", sessions: transports.size }));
|
|
227
|
+
app.post("/mcp", async (req, res) => {
|
|
228
|
+
const sid = req.headers["mcp-session-id"];
|
|
229
|
+
if (sid && transports.has(sid)) {
|
|
230
|
+
await transports.get(sid).handleRequest(req, res, req.body);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (!sid && isInitializeRequest(req.body)) {
|
|
234
|
+
const t = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (id) => { transports.set(id, t); } });
|
|
235
|
+
t.onclose = () => { if (t.sessionId)
|
|
236
|
+
transports.delete(t.sessionId); };
|
|
237
|
+
const s = new Server({ name: "mcp-foxbit", version: "0.1.0" }, { capabilities: { tools: {} } });
|
|
238
|
+
server._requestHandlers.forEach((v, k) => s._requestHandlers.set(k, v));
|
|
239
|
+
server._notificationHandlers?.forEach((v, k) => s._notificationHandlers.set(k, v));
|
|
240
|
+
await s.connect(t);
|
|
241
|
+
await t.handleRequest(req, res, req.body);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
res.status(400).json({ jsonrpc: "2.0", error: { code: -32000, message: "Bad Request" }, id: null });
|
|
245
|
+
});
|
|
246
|
+
app.get("/mcp", async (req, res) => { const sid = req.headers["mcp-session-id"]; if (sid && transports.has(sid))
|
|
247
|
+
await transports.get(sid).handleRequest(req, res);
|
|
248
|
+
else
|
|
249
|
+
res.status(400).send("Invalid session"); });
|
|
250
|
+
app.delete("/mcp", async (req, res) => { const sid = req.headers["mcp-session-id"]; if (sid && transports.has(sid))
|
|
251
|
+
await transports.get(sid).handleRequest(req, res);
|
|
252
|
+
else
|
|
253
|
+
res.status(400).send("Invalid session"); });
|
|
254
|
+
const port = Number(process.env.MCP_PORT) || 3000;
|
|
255
|
+
app.listen(port, () => { console.error(`MCP HTTP server on http://localhost:${port}/mcp`); });
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
const transport = new StdioServerTransport();
|
|
259
|
+
await server.connect(transport);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
main().catch(console.error);
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@codespar/mcp-foxbit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for Foxbit — Brazilian crypto exchange, trading, orderbook, institutional",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"mcp-foxbit": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"start": "node dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/node": "^22.0.0",
|
|
19
|
+
"typescript": "^5.8.0"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"keywords": [
|
|
23
|
+
"mcp",
|
|
24
|
+
"foxbit",
|
|
25
|
+
"crypto",
|
|
26
|
+
"exchange",
|
|
27
|
+
"trading",
|
|
28
|
+
"brazil"
|
|
29
|
+
],
|
|
30
|
+
"mcpName": "io.github.codespar/mcp-foxbit"
|
|
31
|
+
}
|
package/server.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
+
"name": "io.github.codespar/mcp-foxbit",
|
|
4
|
+
"description": "MCP server for Foxbit — Brazilian crypto exchange, trading, orderbook, institutional",
|
|
5
|
+
"repository": {
|
|
6
|
+
"url": "https://github.com/codespar/mcp-dev-brasil",
|
|
7
|
+
"source": "github",
|
|
8
|
+
"subfolder": "packages/crypto/foxbit"
|
|
9
|
+
},
|
|
10
|
+
"version": "0.1.0",
|
|
11
|
+
"packages": [
|
|
12
|
+
{
|
|
13
|
+
"registryType": "npm",
|
|
14
|
+
"identifier": "@codespar/mcp-foxbit",
|
|
15
|
+
"version": "0.1.0",
|
|
16
|
+
"transport": {
|
|
17
|
+
"type": "stdio"
|
|
18
|
+
},
|
|
19
|
+
"environmentVariables": [
|
|
20
|
+
{
|
|
21
|
+
"name": "FOXBIT_API_KEY",
|
|
22
|
+
"description": "API key for Foxbit",
|
|
23
|
+
"isRequired": true,
|
|
24
|
+
"format": "string",
|
|
25
|
+
"isSecret": true
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"name": "FOXBIT_API_SECRET",
|
|
29
|
+
"description": "API secret for Foxbit HMAC-SHA256 signature",
|
|
30
|
+
"isRequired": true,
|
|
31
|
+
"format": "string",
|
|
32
|
+
"isSecret": true
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MCP Server for Foxbit — Brazilian cryptocurrency exchange.
|
|
5
|
+
*
|
|
6
|
+
* Tools:
|
|
7
|
+
* - list_markets: List all available trading pairs
|
|
8
|
+
* - get_ticker: Get 24h ticker data for a market
|
|
9
|
+
* - get_orderbook: Get order book for a market
|
|
10
|
+
* - get_account_balances: Get account balances
|
|
11
|
+
* - create_order: Create a buy or sell order (limit/market)
|
|
12
|
+
* - get_order: Get order details by ID
|
|
13
|
+
* - list_orders: List orders with filters
|
|
14
|
+
* - cancel_order: Cancel an open order
|
|
15
|
+
* - list_trades: List executed trades
|
|
16
|
+
* - list_deposits_withdrawals: List deposits and withdrawals for a currency
|
|
17
|
+
*
|
|
18
|
+
* Environment:
|
|
19
|
+
* FOXBIT_API_KEY — API key from https://app.foxbit.com.br/
|
|
20
|
+
* FOXBIT_API_SECRET — API secret for HMAC-SHA256 signature
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
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";
|
|
27
|
+
import {
|
|
28
|
+
CallToolRequestSchema,
|
|
29
|
+
ListToolsRequestSchema,
|
|
30
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
31
|
+
import * as crypto from "node:crypto";
|
|
32
|
+
|
|
33
|
+
const API_KEY = process.env.FOXBIT_API_KEY || "";
|
|
34
|
+
const API_SECRET = process.env.FOXBIT_API_SECRET || "";
|
|
35
|
+
const BASE_URL = "https://api.foxbit.com.br";
|
|
36
|
+
const PATH_PREFIX = "/rest/v3";
|
|
37
|
+
|
|
38
|
+
async function foxbitRequest(
|
|
39
|
+
method: string,
|
|
40
|
+
path: string,
|
|
41
|
+
query?: Record<string, string | number | boolean | undefined>,
|
|
42
|
+
body?: unknown,
|
|
43
|
+
): Promise<unknown> {
|
|
44
|
+
const timestamp = Date.now().toString();
|
|
45
|
+
const bodyStr = body ? JSON.stringify(body) : "";
|
|
46
|
+
|
|
47
|
+
const params = new URLSearchParams();
|
|
48
|
+
if (query) {
|
|
49
|
+
for (const [k, v] of Object.entries(query)) {
|
|
50
|
+
if (v !== undefined && v !== null && v !== "") params.set(k, String(v));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const queryString = params.toString();
|
|
54
|
+
const fullPath = `${PATH_PREFIX}${path}`;
|
|
55
|
+
|
|
56
|
+
const prehash = timestamp + method.toUpperCase() + fullPath + queryString + bodyStr;
|
|
57
|
+
const signature = crypto.createHmac("sha256", API_SECRET).update(prehash).digest("hex");
|
|
58
|
+
|
|
59
|
+
const url = `${BASE_URL}${fullPath}${queryString ? `?${queryString}` : ""}`;
|
|
60
|
+
const res = await fetch(url, {
|
|
61
|
+
method,
|
|
62
|
+
headers: {
|
|
63
|
+
"Content-Type": "application/json",
|
|
64
|
+
"X-FB-ACCESS-KEY": API_KEY,
|
|
65
|
+
"X-FB-ACCESS-TIMESTAMP": timestamp,
|
|
66
|
+
"X-FB-ACCESS-SIGNATURE": signature,
|
|
67
|
+
},
|
|
68
|
+
body: bodyStr || undefined,
|
|
69
|
+
});
|
|
70
|
+
if (!res.ok) {
|
|
71
|
+
const err = await res.text();
|
|
72
|
+
throw new Error(`Foxbit API ${res.status}: ${err}`);
|
|
73
|
+
}
|
|
74
|
+
return res.json();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const server = new Server(
|
|
78
|
+
{ name: "mcp-foxbit", version: "0.1.0" },
|
|
79
|
+
{ capabilities: { tools: {} } },
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
83
|
+
tools: [
|
|
84
|
+
{
|
|
85
|
+
name: "list_markets",
|
|
86
|
+
description: "List all available trading pairs / markets on Foxbit",
|
|
87
|
+
inputSchema: { type: "object", properties: {} },
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: "get_ticker",
|
|
91
|
+
description: "Get 24h ticker data for a market (price, volume, high/low)",
|
|
92
|
+
inputSchema: {
|
|
93
|
+
type: "object",
|
|
94
|
+
properties: {
|
|
95
|
+
symbol: { type: "string", description: "Market symbol (e.g. btcbrl, ethbrl, ltcbrl)" },
|
|
96
|
+
},
|
|
97
|
+
required: ["symbol"],
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: "get_orderbook",
|
|
102
|
+
description: "Get order book (bids and asks) for a market",
|
|
103
|
+
inputSchema: {
|
|
104
|
+
type: "object",
|
|
105
|
+
properties: {
|
|
106
|
+
symbol: { type: "string", description: "Market symbol (e.g. btcbrl)" },
|
|
107
|
+
depth: { type: "number", description: "Number of price levels per side" },
|
|
108
|
+
},
|
|
109
|
+
required: ["symbol"],
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: "get_account_balances",
|
|
114
|
+
description: "Get account balances for all currencies",
|
|
115
|
+
inputSchema: { type: "object", properties: {} },
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: "create_order",
|
|
119
|
+
description: "Create a buy or sell order (limit or market)",
|
|
120
|
+
inputSchema: {
|
|
121
|
+
type: "object",
|
|
122
|
+
properties: {
|
|
123
|
+
market_symbol: { type: "string", description: "Market symbol (e.g. btcbrl)" },
|
|
124
|
+
side: { type: "string", enum: ["BUY", "SELL"], description: "Order side" },
|
|
125
|
+
type: { type: "string", enum: ["LIMIT", "MARKET", "STOP_LIMIT", "STOP_MARKET"], description: "Order type" },
|
|
126
|
+
quantity: { type: "string", description: "Base asset quantity (e.g. BTC)" },
|
|
127
|
+
price: { type: "string", description: "Limit price (required for LIMIT orders)" },
|
|
128
|
+
client_order_id: { type: "string", description: "Client-supplied order ID" },
|
|
129
|
+
time_in_force: { type: "string", enum: ["GTC", "IOC", "FOK"], description: "Time in force" },
|
|
130
|
+
},
|
|
131
|
+
required: ["market_symbol", "side", "type", "quantity"],
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: "get_order",
|
|
136
|
+
description: "Get order details by ID",
|
|
137
|
+
inputSchema: {
|
|
138
|
+
type: "object",
|
|
139
|
+
properties: {
|
|
140
|
+
id: { type: "string", description: "Order ID" },
|
|
141
|
+
},
|
|
142
|
+
required: ["id"],
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
name: "list_orders",
|
|
147
|
+
description: "List orders with optional filters",
|
|
148
|
+
inputSchema: {
|
|
149
|
+
type: "object",
|
|
150
|
+
properties: {
|
|
151
|
+
market_symbol: { type: "string", description: "Filter by market symbol" },
|
|
152
|
+
state: { type: "string", enum: ["ACTIVE", "FILLED", "CANCELED", "PARTIALLY_FILLED", "PARTIALLY_CANCELED"], description: "Filter by order state" },
|
|
153
|
+
side: { type: "string", enum: ["BUY", "SELL"], description: "Filter by side" },
|
|
154
|
+
start_time: { type: "string", description: "Start time (ISO 8601)" },
|
|
155
|
+
end_time: { type: "string", description: "End time (ISO 8601)" },
|
|
156
|
+
page_size: { type: "number", description: "Results per page" },
|
|
157
|
+
page: { type: "number", description: "Page number" },
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
name: "cancel_order",
|
|
163
|
+
description: "Cancel an open order by ID",
|
|
164
|
+
inputSchema: {
|
|
165
|
+
type: "object",
|
|
166
|
+
properties: {
|
|
167
|
+
id: { type: "string", description: "Order ID to cancel" },
|
|
168
|
+
},
|
|
169
|
+
required: ["id"],
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
name: "list_trades",
|
|
174
|
+
description: "List user's executed trades",
|
|
175
|
+
inputSchema: {
|
|
176
|
+
type: "object",
|
|
177
|
+
properties: {
|
|
178
|
+
market_symbol: { type: "string", description: "Filter by market symbol" },
|
|
179
|
+
start_time: { type: "string", description: "Start time (ISO 8601)" },
|
|
180
|
+
end_time: { type: "string", description: "End time (ISO 8601)" },
|
|
181
|
+
page_size: { type: "number", description: "Results per page" },
|
|
182
|
+
page: { type: "number", description: "Page number" },
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
name: "list_deposits_withdrawals",
|
|
188
|
+
description: "List deposits and withdrawals (transactions) for a currency",
|
|
189
|
+
inputSchema: {
|
|
190
|
+
type: "object",
|
|
191
|
+
properties: {
|
|
192
|
+
currency_symbol: { type: "string", description: "Currency symbol (e.g. brl, btc, eth)" },
|
|
193
|
+
type: { type: "string", enum: ["deposit", "withdraw"], description: "Filter by transaction type" },
|
|
194
|
+
start_time: { type: "string", description: "Start time (ISO 8601)" },
|
|
195
|
+
end_time: { type: "string", description: "End time (ISO 8601)" },
|
|
196
|
+
page_size: { type: "number", description: "Results per page" },
|
|
197
|
+
page: { type: "number", description: "Page number" },
|
|
198
|
+
},
|
|
199
|
+
required: ["currency_symbol"],
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
],
|
|
203
|
+
}));
|
|
204
|
+
|
|
205
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
206
|
+
const { name, arguments: args } = request.params;
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
switch (name) {
|
|
210
|
+
case "list_markets":
|
|
211
|
+
return { content: [{ type: "text", text: JSON.stringify(await foxbitRequest("GET", "/markets"), null, 2) }] };
|
|
212
|
+
case "get_ticker":
|
|
213
|
+
return { content: [{ type: "text", text: JSON.stringify(await foxbitRequest("GET", `/markets/${args?.symbol}/ticker/24hr`), null, 2) }] };
|
|
214
|
+
case "get_orderbook":
|
|
215
|
+
return { content: [{ type: "text", text: JSON.stringify(await foxbitRequest("GET", `/markets/${args?.symbol}/orderbook`, { depth: args?.depth as number | undefined }), null, 2) }] };
|
|
216
|
+
case "get_account_balances":
|
|
217
|
+
return { content: [{ type: "text", text: JSON.stringify(await foxbitRequest("GET", "/accounts"), null, 2) }] };
|
|
218
|
+
case "create_order":
|
|
219
|
+
return { content: [{ type: "text", text: JSON.stringify(await foxbitRequest("POST", "/orders", undefined, args), null, 2) }] };
|
|
220
|
+
case "get_order":
|
|
221
|
+
return { content: [{ type: "text", text: JSON.stringify(await foxbitRequest("GET", `/orders/${args?.id}`), null, 2) }] };
|
|
222
|
+
case "list_orders":
|
|
223
|
+
return { content: [{ type: "text", text: JSON.stringify(await foxbitRequest("GET", "/orders", args as Record<string, string | number | undefined>), null, 2) }] };
|
|
224
|
+
case "cancel_order":
|
|
225
|
+
return { content: [{ type: "text", text: JSON.stringify(await foxbitRequest("DELETE", `/orders/${args?.id}`), null, 2) }] };
|
|
226
|
+
case "list_trades":
|
|
227
|
+
return { content: [{ type: "text", text: JSON.stringify(await foxbitRequest("GET", "/trades", args as Record<string, string | number | undefined>), null, 2) }] };
|
|
228
|
+
case "list_deposits_withdrawals": {
|
|
229
|
+
const { currency_symbol, ...rest } = (args || {}) as Record<string, string | number | undefined>;
|
|
230
|
+
return { content: [{ type: "text", text: JSON.stringify(await foxbitRequest("GET", `/accounts/${currency_symbol}/transactions`, rest), null, 2) }] };
|
|
231
|
+
}
|
|
232
|
+
default:
|
|
233
|
+
return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
|
|
234
|
+
}
|
|
235
|
+
} catch (err) {
|
|
236
|
+
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
async function main() {
|
|
241
|
+
if (process.argv.includes("--http") || process.env.MCP_HTTP === "true") {
|
|
242
|
+
const { default: express } = await import("express");
|
|
243
|
+
const { randomUUID } = await import("node:crypto");
|
|
244
|
+
const app = express();
|
|
245
|
+
app.use(express.json());
|
|
246
|
+
const transports = new Map<string, StreamableHTTPServerTransport>();
|
|
247
|
+
app.get("/health", (_req: any, res: any) => res.json({ status: "ok", sessions: transports.size }));
|
|
248
|
+
app.post("/mcp", async (req: any, res: any) => {
|
|
249
|
+
const sid = req.headers["mcp-session-id"] as string | undefined;
|
|
250
|
+
if (sid && transports.has(sid)) { await transports.get(sid)!.handleRequest(req, res, req.body); return; }
|
|
251
|
+
if (!sid && isInitializeRequest(req.body)) {
|
|
252
|
+
const t = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (id) => { transports.set(id, t); } });
|
|
253
|
+
t.onclose = () => { if (t.sessionId) transports.delete(t.sessionId); };
|
|
254
|
+
const s = new Server({ name: "mcp-foxbit", 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);
|
|
255
|
+
await t.handleRequest(req, res, req.body); return;
|
|
256
|
+
}
|
|
257
|
+
res.status(400).json({ jsonrpc: "2.0", error: { code: -32000, message: "Bad Request" }, id: null });
|
|
258
|
+
});
|
|
259
|
+
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"); });
|
|
260
|
+
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"); });
|
|
261
|
+
const port = Number(process.env.MCP_PORT) || 3000;
|
|
262
|
+
app.listen(port, () => { console.error(`MCP HTTP server on http://localhost:${port}/mcp`); });
|
|
263
|
+
} else {
|
|
264
|
+
const transport = new StdioServerTransport();
|
|
265
|
+
await server.connect(transport);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
main().catch(console.error);
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"declaration": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src"]
|
|
14
|
+
}
|