omnitrade-mcp 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Connectry
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,240 @@
1
+ # OmniTrade MCP
2
+
3
+ > **One AI. 107 Exchanges. Natural language trading.**
4
+
5
+ Connect Claude to Binance, Coinbase, Kraken, and 104 more cryptocurrency exchanges through the Model Context Protocol (MCP).
6
+
7
+ [![npm version](https://img.shields.io/npm/v/omnitrade-mcp)](https://www.npmjs.com/package/omnitrade-mcp)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
9
+
10
+ ## Features
11
+
12
+ - 🔗 **107 Exchanges** — Connect to any exchange supported by CCXT
13
+ - 🤖 **Natural Language** — Ask Claude to trade in plain English
14
+ - 🔒 **Local-Only** — API keys never leave your machine
15
+ - ⚡ **Arbitrage Detection** — Find price differences across exchanges
16
+ - 📊 **Unified Portfolio** — See all holdings in one view
17
+ - 🛡️ **Safety First** — Order limits, pair whitelists, testnet mode
18
+
19
+ ## Quick Start
20
+
21
+ ### 1. Install
22
+
23
+ ```bash
24
+ npm install -g omnitrade-mcp
25
+ ```
26
+
27
+ ### 2. Configure
28
+
29
+ Create `~/.omnitrade/config.json`:
30
+
31
+ ```json
32
+ {
33
+ "exchanges": {
34
+ "binance": {
35
+ "apiKey": "YOUR_API_KEY",
36
+ "secret": "YOUR_SECRET",
37
+ "testnet": true
38
+ },
39
+ "coinbase": {
40
+ "apiKey": "YOUR_API_KEY",
41
+ "secret": "YOUR_SECRET",
42
+ "password": "YOUR_PASSPHRASE",
43
+ "testnet": true
44
+ }
45
+ },
46
+ "security": {
47
+ "maxOrderSize": 100,
48
+ "confirmTrades": true
49
+ }
50
+ }
51
+ ```
52
+
53
+ Set proper permissions:
54
+
55
+ ```bash
56
+ chmod 600 ~/.omnitrade/config.json
57
+ ```
58
+
59
+ ### 3. Add to Claude Desktop
60
+
61
+ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
62
+
63
+ ```json
64
+ {
65
+ "mcpServers": {
66
+ "omnitrade": {
67
+ "command": "omnitrade-mcp"
68
+ }
69
+ }
70
+ }
71
+ ```
72
+
73
+ ### 4. Trade
74
+
75
+ Restart Claude Desktop and start chatting:
76
+
77
+ - *"What's my balance on Binance?"*
78
+ - *"Show me ETH prices across all exchanges"*
79
+ - *"Buy $50 of BTC on the cheapest exchange"*
80
+ - *"Are there any arbitrage opportunities for SOL?"*
81
+
82
+ ## Available Tools
83
+
84
+ | Tool | Description |
85
+ |------|-------------|
86
+ | `get_balances` | Get portfolio balances across exchanges |
87
+ | `get_portfolio` | Unified portfolio summary |
88
+ | `get_prices` | Current prices for a trading pair |
89
+ | `compare_prices` | Find best price across exchanges |
90
+ | `place_order` | Execute buy/sell orders |
91
+ | `get_orders` | View open and recent orders |
92
+ | `cancel_order` | Cancel an open order |
93
+ | `get_arbitrage` | Find arbitrage opportunities |
94
+ | `check_spread` | Check spread for a specific pair |
95
+
96
+ ## Supported Exchanges
97
+
98
+ OmniTrade supports **107 exchanges** through [CCXT](https://github.com/ccxt/ccxt), including:
99
+
100
+ **Tier 1 (Certified):** Binance, Bybit, OKX, Gate.io, KuCoin, Bitget, HTX, Crypto.com, MEXC, WOO X, Hyperliquid
101
+
102
+ **Tier 2:** Coinbase, Kraken, Bitstamp, Gemini, Bitfinex, Poloniex, Deribit, Upbit, Bithumb, Bitvavo, and 80+ more
103
+
104
+ ## Security
105
+
106
+ ### Local-Only Architecture
107
+
108
+ ```
109
+ ┌─────────────────────────────────────┐
110
+ │ YOUR MACHINE │
111
+ │ │
112
+ │ Claude ←→ OmniTrade MCP │
113
+ │ ↓ │
114
+ │ config.json (your keys) │
115
+ │ ↓ │
116
+ │ Exchange APIs │
117
+ └─────────────────────────────────────┘
118
+ (HTTPS to exchanges)
119
+ ```
120
+
121
+ - ✅ API keys stay on your machine
122
+ - ✅ No cloud storage
123
+ - ✅ No telemetry
124
+ - ✅ Open source — audit the code
125
+
126
+ ### API Key Best Practices
127
+
128
+ **Always:**
129
+ - Enable only View + Trade permissions
130
+ - **Disable** withdrawal permissions
131
+ - Use IP restrictions when available
132
+ - Use testnet for testing
133
+
134
+ **Never:**
135
+ - Share your config file
136
+ - Commit config to git
137
+ - Enable withdrawal permissions
138
+
139
+ ### Safety Features
140
+
141
+ ```json
142
+ {
143
+ "security": {
144
+ "maxOrderSize": 100, // Max $100 per order
145
+ "allowedPairs": ["BTC/USDT", "ETH/USDT"], // Whitelist
146
+ "testnetOnly": true, // Force testnet
147
+ "confirmTrades": true // Require confirmation
148
+ }
149
+ }
150
+ ```
151
+
152
+ ## Configuration
153
+
154
+ ### Full Config Example
155
+
156
+ ```json
157
+ {
158
+ "exchanges": {
159
+ "binance": {
160
+ "apiKey": "xxx",
161
+ "secret": "xxx",
162
+ "testnet": true
163
+ },
164
+ "coinbase": {
165
+ "apiKey": "xxx",
166
+ "secret": "xxx",
167
+ "password": "xxx",
168
+ "testnet": true
169
+ },
170
+ "kraken": {
171
+ "apiKey": "xxx",
172
+ "secret": "xxx",
173
+ "testnet": false
174
+ }
175
+ },
176
+ "defaultExchange": "binance",
177
+ "security": {
178
+ "maxOrderSize": 100,
179
+ "allowedPairs": ["BTC/USDT", "ETH/USDT", "SOL/USDT"],
180
+ "testnetOnly": false,
181
+ "confirmTrades": true
182
+ }
183
+ }
184
+ ```
185
+
186
+ ### Config Locations
187
+
188
+ The config file is searched in order:
189
+
190
+ 1. `~/.omnitrade/config.json` (recommended)
191
+ 2. `./omnitrade.config.json`
192
+ 3. `./.omnitrade.json`
193
+
194
+ ## Testnet Setup
195
+
196
+ ### Binance Testnet
197
+
198
+ 1. Go to https://testnet.binance.vision/
199
+ 2. Login with GitHub
200
+ 3. Generate API keys
201
+ 4. Get free testnet coins from faucet
202
+
203
+ ### Coinbase Sandbox
204
+
205
+ 1. Go to https://portal.cdp.coinbase.com/
206
+ 2. Create new project
207
+ 3. Enable sandbox mode
208
+ 4. Generate API keys
209
+
210
+ ## Disclaimer
211
+
212
+ ```
213
+ ⚠️ IMPORTANT
214
+
215
+ This software is provided "as is" without warranty of any kind.
216
+
217
+ Cryptocurrency trading involves substantial risk of loss.
218
+ Past performance does not guarantee future results.
219
+
220
+ This software does NOT provide financial, investment, or trading advice.
221
+ You are solely responsible for your trading decisions.
222
+
223
+ Always test with testnet before using real funds.
224
+ Never trade more than you can afford to lose.
225
+ ```
226
+
227
+ ## License
228
+
229
+ MIT © [Connectry Labs](https://connectry.io)
230
+
231
+ ## Links
232
+
233
+ - [GitHub](https://github.com/Connectry-io/omnitrade-mcp)
234
+ - [npm](https://www.npmjs.com/package/omnitrade-mcp)
235
+ - [CCXT Documentation](https://docs.ccxt.com/)
236
+ - [MCP Specification](https://modelcontextprotocol.io/)
237
+
238
+ ---
239
+
240
+ Made with ⚡ by [Connectry Labs](https://connectry.io)
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/index.js ADDED
@@ -0,0 +1,999 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+
7
+ // src/config/loader.ts
8
+ import { readFileSync, existsSync, statSync } from "fs";
9
+ import { homedir } from "os";
10
+ import { join } from "path";
11
+
12
+ // src/types/index.ts
13
+ import { z } from "zod";
14
+ var ExchangeConfigSchema = z.object({
15
+ apiKey: z.string().min(1, "API key is required"),
16
+ secret: z.string().min(1, "Secret is required"),
17
+ password: z.string().optional(),
18
+ // For exchanges requiring passphrase (Coinbase, KuCoin)
19
+ testnet: z.boolean().default(true)
20
+ // Default to testnet for safety
21
+ });
22
+ var SecurityConfigSchema = z.object({
23
+ confirmTrades: z.boolean().default(true),
24
+ maxOrderSize: z.number().positive().default(100),
25
+ // Max USD per order
26
+ allowedPairs: z.array(z.string()).optional(),
27
+ // Whitelist of trading pairs
28
+ testnetOnly: z.boolean().default(false),
29
+ // Force testnet mode globally
30
+ disableWithdrawals: z.boolean().default(true)
31
+ // Extra safety (we don't support withdrawals anyway)
32
+ });
33
+ var ConfigSchema = z.object({
34
+ exchanges: z.record(z.string(), ExchangeConfigSchema),
35
+ security: SecurityConfigSchema.optional(),
36
+ defaultExchange: z.string().optional()
37
+ });
38
+
39
+ // src/config/loader.ts
40
+ var CONFIG_PATHS = [
41
+ join(homedir(), ".omnitrade", "config.json"),
42
+ join(process.cwd(), "omnitrade.config.json"),
43
+ join(process.cwd(), ".omnitrade.json")
44
+ ];
45
+ function loadConfig() {
46
+ for (const configPath of CONFIG_PATHS) {
47
+ if (existsSync(configPath)) {
48
+ checkFilePermissions(configPath);
49
+ const raw = readFileSync(configPath, "utf-8");
50
+ let parsed;
51
+ try {
52
+ parsed = JSON.parse(raw);
53
+ } catch {
54
+ throw new Error(`Invalid JSON in config file: ${configPath}`);
55
+ }
56
+ const result = ConfigSchema.safeParse(parsed);
57
+ if (!result.success) {
58
+ const errors = result.error.errors.map((e) => ` - ${e.path.join(".")}: ${e.message}`).join("\n");
59
+ throw new Error(`Invalid config at ${configPath}:
60
+ ${errors}`);
61
+ }
62
+ console.error(`\u2713 Config loaded from: ${configPath}`);
63
+ console.error(`\u2713 Exchanges configured: ${Object.keys(result.data.exchanges).join(", ")}`);
64
+ return result.data;
65
+ }
66
+ }
67
+ throw new Error(
68
+ `Config file not found.
69
+
70
+ Create a config file at: ${CONFIG_PATHS[0]}
71
+
72
+ Example config:
73
+ ` + JSON.stringify(
74
+ {
75
+ exchanges: {
76
+ binance: {
77
+ apiKey: "YOUR_API_KEY",
78
+ secret: "YOUR_SECRET",
79
+ testnet: true
80
+ }
81
+ },
82
+ security: {
83
+ maxOrderSize: 100,
84
+ confirmTrades: true
85
+ }
86
+ },
87
+ null,
88
+ 2
89
+ ) + `
90
+
91
+ Then set permissions: chmod 600 ${CONFIG_PATHS[0]}
92
+
93
+ Docs: https://github.com/Connectry-io/omnitrade-mcp#configuration`
94
+ );
95
+ }
96
+ function checkFilePermissions(configPath) {
97
+ try {
98
+ const stats = statSync(configPath);
99
+ const mode = stats.mode & 511;
100
+ if (mode & 36) {
101
+ console.error(`
102
+ \u26A0\uFE0F SECURITY WARNING: Config file is readable by others!`);
103
+ console.error(` File: ${configPath}`);
104
+ console.error(` Current permissions: ${mode.toString(8)}`);
105
+ console.error(` Recommended: Run 'chmod 600 ${configPath}'
106
+ `);
107
+ }
108
+ } catch {
109
+ }
110
+ }
111
+
112
+ // src/exchanges/manager.ts
113
+ import ccxt from "ccxt";
114
+ var ExchangeManager = class {
115
+ exchanges = /* @__PURE__ */ new Map();
116
+ config;
117
+ constructor(config) {
118
+ this.config = config;
119
+ this.initializeExchanges();
120
+ }
121
+ /**
122
+ * Initialize exchange connections from config
123
+ */
124
+ initializeExchanges() {
125
+ for (const [name, exchangeConfig] of Object.entries(this.config.exchanges)) {
126
+ try {
127
+ const exchange = this.createExchange(name, exchangeConfig);
128
+ if (exchange) {
129
+ this.exchanges.set(name, exchange);
130
+ console.error(`\u2713 Exchange initialized: ${name}${exchangeConfig.testnet ? " (testnet)" : ""}`);
131
+ }
132
+ } catch (error) {
133
+ console.error(`\u2717 Failed to initialize ${name}: ${error.message}`);
134
+ }
135
+ }
136
+ if (this.exchanges.size === 0) {
137
+ throw new Error("No exchanges could be initialized. Check your config.");
138
+ }
139
+ }
140
+ /**
141
+ * Create a CCXT exchange instance
142
+ */
143
+ createExchange(name, config) {
144
+ const exchangeId = name.toLowerCase();
145
+ if (!ccxt.exchanges.includes(exchangeId)) {
146
+ console.error(`\u2717 Unknown exchange: ${name}. Supported: ${ccxt.exchanges.slice(0, 10).join(", ")}...`);
147
+ return null;
148
+ }
149
+ const ExchangeClass = ccxt[exchangeId];
150
+ if (!ExchangeClass) {
151
+ console.error(`\u2717 Could not load exchange class: ${name}`);
152
+ return null;
153
+ }
154
+ const exchange = new ExchangeClass({
155
+ apiKey: config.apiKey,
156
+ secret: config.secret,
157
+ password: config.password,
158
+ enableRateLimit: true,
159
+ // Respect rate limits
160
+ options: {
161
+ defaultType: "spot",
162
+ // Default to spot trading
163
+ adjustForTimeDifference: true
164
+ // Handle time sync issues
165
+ }
166
+ });
167
+ if (config.testnet) {
168
+ try {
169
+ exchange.setSandboxMode(true);
170
+ } catch {
171
+ console.error(`\u26A0 ${name} does not support testnet mode, using production`);
172
+ }
173
+ }
174
+ return exchange;
175
+ }
176
+ /**
177
+ * Get a specific exchange by name
178
+ */
179
+ get(name) {
180
+ return this.exchanges.get(name.toLowerCase());
181
+ }
182
+ /**
183
+ * Get all initialized exchanges
184
+ */
185
+ getAll() {
186
+ return this.exchanges;
187
+ }
188
+ /**
189
+ * Get list of initialized exchange names
190
+ */
191
+ getNames() {
192
+ return Array.from(this.exchanges.keys());
193
+ }
194
+ /**
195
+ * Get the default exchange (first configured or explicitly set)
196
+ */
197
+ getDefault() {
198
+ if (this.config.defaultExchange) {
199
+ return this.exchanges.get(this.config.defaultExchange.toLowerCase());
200
+ }
201
+ return this.exchanges.values().next().value;
202
+ }
203
+ /**
204
+ * Get default exchange name
205
+ */
206
+ getDefaultName() {
207
+ if (this.config.defaultExchange) {
208
+ return this.config.defaultExchange.toLowerCase();
209
+ }
210
+ return this.exchanges.keys().next().value;
211
+ }
212
+ /**
213
+ * Check if an exchange is in testnet mode
214
+ */
215
+ isTestnet(name) {
216
+ const config = this.config.exchanges[name];
217
+ return config?.testnet ?? true;
218
+ }
219
+ };
220
+
221
+ // src/tools/balances.ts
222
+ import { z as z2 } from "zod";
223
+ function registerBalanceTools(server, exchangeManager) {
224
+ server.tool(
225
+ "get_balances",
226
+ "Get portfolio balances across all connected exchanges. Returns asset holdings with free (available) and locked (in orders) amounts.",
227
+ {
228
+ exchange: z2.string().optional().describe('Specific exchange name (e.g., "binance"). Omit to query all exchanges.'),
229
+ asset: z2.string().optional().describe('Filter by specific asset (e.g., "BTC", "ETH"). Omit for all assets.'),
230
+ hideZero: z2.boolean().default(true).describe("Hide assets with zero balance (default: true)")
231
+ },
232
+ async ({ exchange, asset, hideZero }) => {
233
+ const balances = [];
234
+ const exchangesToQuery = exchange ? [[exchange.toLowerCase(), exchangeManager.get(exchange)]].filter(
235
+ ([, ex]) => ex !== void 0
236
+ ) : Array.from(exchangeManager.getAll().entries());
237
+ if (exchangesToQuery.length === 0) {
238
+ return {
239
+ content: [
240
+ {
241
+ type: "text",
242
+ text: exchange ? `Exchange not configured: ${exchange}. Available: ${exchangeManager.getNames().join(", ")}` : "No exchanges configured."
243
+ }
244
+ ],
245
+ isError: true
246
+ };
247
+ }
248
+ for (const [name, ex] of exchangesToQuery) {
249
+ if (!ex) continue;
250
+ try {
251
+ const balance = await ex.fetchBalance();
252
+ for (const [symbol, total] of Object.entries(balance.total)) {
253
+ const totalAmount = total;
254
+ if (hideZero && totalAmount === 0) continue;
255
+ if (asset && symbol.toUpperCase() !== asset.toUpperCase()) continue;
256
+ const free = balance.free[symbol] || 0;
257
+ const locked = balance.used[symbol] || 0;
258
+ balances.push({
259
+ exchange: name,
260
+ asset: symbol,
261
+ free,
262
+ locked,
263
+ total: totalAmount
264
+ });
265
+ }
266
+ } catch (error) {
267
+ balances.push({
268
+ exchange: name,
269
+ asset: "ERROR",
270
+ free: 0,
271
+ locked: 0,
272
+ total: 0,
273
+ error: error.message
274
+ });
275
+ }
276
+ }
277
+ balances.sort((a, b) => b.total - a.total);
278
+ const summary = {
279
+ exchanges: exchangeManager.getNames().length,
280
+ assetsFound: balances.filter((b) => !("error" in b)).length,
281
+ balances
282
+ };
283
+ return {
284
+ content: [
285
+ {
286
+ type: "text",
287
+ text: JSON.stringify(summary, null, 2)
288
+ }
289
+ ]
290
+ };
291
+ }
292
+ );
293
+ server.tool(
294
+ "get_portfolio",
295
+ "Get a unified portfolio summary across all exchanges with total values.",
296
+ {},
297
+ async () => {
298
+ const assetTotals = {};
299
+ const errors = [];
300
+ for (const [name, ex] of exchangeManager.getAll()) {
301
+ try {
302
+ const balance = await ex.fetchBalance();
303
+ for (const [symbol, total] of Object.entries(balance.total)) {
304
+ const amount = total;
305
+ if (amount === 0) continue;
306
+ if (!assetTotals[symbol]) {
307
+ assetTotals[symbol] = { total: 0, byExchange: {} };
308
+ }
309
+ assetTotals[symbol].total += amount;
310
+ assetTotals[symbol].byExchange[name] = amount;
311
+ }
312
+ } catch (error) {
313
+ errors.push(`${name}: ${error.message}`);
314
+ }
315
+ }
316
+ const assets = Object.entries(assetTotals).map(([asset, data]) => ({
317
+ asset,
318
+ total: data.total,
319
+ distribution: data.byExchange
320
+ })).sort((a, b) => b.total - a.total);
321
+ const portfolio = {
322
+ summary: {
323
+ totalAssets: assets.length,
324
+ exchanges: exchangeManager.getNames()
325
+ },
326
+ assets,
327
+ errors: errors.length > 0 ? errors : void 0
328
+ };
329
+ return {
330
+ content: [
331
+ {
332
+ type: "text",
333
+ text: JSON.stringify(portfolio, null, 2)
334
+ }
335
+ ]
336
+ };
337
+ }
338
+ );
339
+ }
340
+
341
+ // src/tools/prices.ts
342
+ import { z as z3 } from "zod";
343
+ function registerPriceTools(server, exchangeManager) {
344
+ server.tool(
345
+ "get_prices",
346
+ "Get current market prices for a trading pair across exchanges. Returns bid, ask, and last traded price.",
347
+ {
348
+ symbol: z3.string().describe('Trading pair in format "BASE/QUOTE" (e.g., "BTC/USDT", "ETH/USD")'),
349
+ exchange: z3.string().optional().describe("Specific exchange name. Omit to query all exchanges.")
350
+ },
351
+ async ({ symbol, exchange }) => {
352
+ const prices = [];
353
+ const errors = [];
354
+ const normalizedSymbol = symbol.toUpperCase();
355
+ const exchangesToQuery = exchange ? [[exchange.toLowerCase(), exchangeManager.get(exchange)]].filter(
356
+ ([, ex]) => ex !== void 0
357
+ ) : Array.from(exchangeManager.getAll().entries());
358
+ if (exchangesToQuery.length === 0) {
359
+ return {
360
+ content: [
361
+ {
362
+ type: "text",
363
+ text: exchange ? `Exchange not configured: ${exchange}` : "No exchanges configured."
364
+ }
365
+ ],
366
+ isError: true
367
+ };
368
+ }
369
+ for (const [name, ex] of exchangesToQuery) {
370
+ if (!ex) continue;
371
+ try {
372
+ const ticker = await ex.fetchTicker(normalizedSymbol);
373
+ prices.push({
374
+ exchange: name,
375
+ symbol: normalizedSymbol,
376
+ bid: ticker.bid ?? 0,
377
+ ask: ticker.ask ?? 0,
378
+ last: ticker.last ?? 0,
379
+ timestamp: ticker.timestamp ?? Date.now()
380
+ });
381
+ } catch (error) {
382
+ errors.push(`${name}: ${error.message}`);
383
+ }
384
+ }
385
+ if (prices.length === 0) {
386
+ return {
387
+ content: [
388
+ {
389
+ type: "text",
390
+ text: `No prices found for ${normalizedSymbol}. Errors: ${errors.join("; ")}`
391
+ }
392
+ ],
393
+ isError: true
394
+ };
395
+ }
396
+ prices.sort((a, b) => b.last - a.last);
397
+ const result = {
398
+ symbol: normalizedSymbol,
399
+ priceCount: prices.length,
400
+ prices,
401
+ errors: errors.length > 0 ? errors : void 0
402
+ };
403
+ return {
404
+ content: [
405
+ {
406
+ type: "text",
407
+ text: JSON.stringify(result, null, 2)
408
+ }
409
+ ]
410
+ };
411
+ }
412
+ );
413
+ server.tool(
414
+ "compare_prices",
415
+ "Find the best price for buying or selling across all connected exchanges. Identifies arbitrage opportunities.",
416
+ {
417
+ symbol: z3.string().describe('Trading pair in format "BASE/QUOTE" (e.g., "BTC/USDT")'),
418
+ side: z3.enum(["buy", "sell"]).describe('"buy" to find lowest ask price, "sell" to find highest bid price')
419
+ },
420
+ async ({ symbol, side }) => {
421
+ const normalizedSymbol = symbol.toUpperCase();
422
+ const prices = [];
423
+ for (const [name, ex] of exchangeManager.getAll()) {
424
+ try {
425
+ const ticker = await ex.fetchTicker(normalizedSymbol);
426
+ if (ticker.bid && ticker.ask) {
427
+ prices.push({
428
+ exchange: name,
429
+ symbol: normalizedSymbol,
430
+ bid: ticker.bid,
431
+ ask: ticker.ask,
432
+ last: ticker.last ?? 0,
433
+ timestamp: ticker.timestamp ?? Date.now()
434
+ });
435
+ }
436
+ } catch {
437
+ continue;
438
+ }
439
+ }
440
+ if (prices.length === 0) {
441
+ return {
442
+ content: [
443
+ {
444
+ type: "text",
445
+ text: `No prices found for ${normalizedSymbol} on any exchange.`
446
+ }
447
+ ],
448
+ isError: true
449
+ };
450
+ }
451
+ if (prices.length === 1) {
452
+ const only = prices[0];
453
+ return {
454
+ content: [
455
+ {
456
+ type: "text",
457
+ text: JSON.stringify(
458
+ {
459
+ recommendation: `${side.toUpperCase()} on ${only.exchange} (only exchange with this pair)`,
460
+ bestExchange: only.exchange,
461
+ bestPrice: side === "buy" ? only.ask : only.bid,
462
+ note: "Only one exchange has this trading pair."
463
+ },
464
+ null,
465
+ 2
466
+ )
467
+ }
468
+ ]
469
+ };
470
+ }
471
+ const sorted = [...prices].sort((a, b) => {
472
+ if (side === "buy") {
473
+ return a.ask - b.ask;
474
+ } else {
475
+ return b.bid - a.bid;
476
+ }
477
+ });
478
+ const best = sorted[0];
479
+ const worst = sorted[sorted.length - 1];
480
+ const bestPrice = side === "buy" ? best.ask : best.bid;
481
+ const worstPrice = side === "buy" ? worst.ask : worst.bid;
482
+ const savings = Math.abs(worstPrice - bestPrice);
483
+ const savingsPercent = savings / worstPrice * 100;
484
+ const comparison = {
485
+ recommendation: `${side.toUpperCase()} on ${best.exchange} for best price`,
486
+ bestExchange: best.exchange,
487
+ bestPrice,
488
+ allPrices: sorted.map((p) => ({
489
+ exchange: p.exchange,
490
+ price: side === "buy" ? p.ask : p.bid
491
+ })),
492
+ savings: {
493
+ amount: parseFloat(savings.toFixed(8)),
494
+ percent: savingsPercent.toFixed(2) + "%",
495
+ vs: worst.exchange
496
+ }
497
+ };
498
+ return {
499
+ content: [
500
+ {
501
+ type: "text",
502
+ text: JSON.stringify(comparison, null, 2)
503
+ }
504
+ ]
505
+ };
506
+ }
507
+ );
508
+ }
509
+
510
+ // src/tools/orders.ts
511
+ import { z as z4 } from "zod";
512
+ function registerOrderTools(server, exchangeManager, config) {
513
+ server.tool(
514
+ "place_order",
515
+ "Execute a buy or sell order on an exchange. Supports market and limit orders with safety checks.",
516
+ {
517
+ exchange: z4.string().describe('Exchange to trade on (e.g., "binance", "coinbase")'),
518
+ symbol: z4.string().describe('Trading pair (e.g., "BTC/USDT", "ETH/USD")'),
519
+ side: z4.enum(["buy", "sell"]).describe('Order side: "buy" or "sell"'),
520
+ type: z4.enum(["market", "limit"]).default("market").describe('Order type: "market" (immediate) or "limit" (at specified price)'),
521
+ amount: z4.number().positive().describe("Amount of base currency to trade"),
522
+ price: z4.number().positive().optional().describe("Limit price (required for limit orders, ignored for market orders)")
523
+ },
524
+ async ({ exchange, symbol, side, type, amount, price }) => {
525
+ const ex = exchangeManager.get(exchange);
526
+ if (!ex) {
527
+ return {
528
+ content: [
529
+ {
530
+ type: "text",
531
+ text: `Exchange not configured: ${exchange}. Available: ${exchangeManager.getNames().join(", ")}`
532
+ }
533
+ ],
534
+ isError: true
535
+ };
536
+ }
537
+ const normalizedSymbol = symbol.toUpperCase();
538
+ const security = config.security;
539
+ if (security?.allowedPairs && security.allowedPairs.length > 0) {
540
+ const allowed = security.allowedPairs.map((p) => p.toUpperCase());
541
+ if (!allowed.includes(normalizedSymbol)) {
542
+ return {
543
+ content: [
544
+ {
545
+ type: "text",
546
+ text: `Trading pair not in allowlist: ${normalizedSymbol}
547
+ Allowed pairs: ${allowed.join(", ")}`
548
+ }
549
+ ],
550
+ isError: true
551
+ };
552
+ }
553
+ }
554
+ if (security?.maxOrderSize) {
555
+ try {
556
+ const ticker = await ex.fetchTicker(normalizedSymbol);
557
+ const estimatedUsdValue = amount * (ticker.last ?? 0);
558
+ if (estimatedUsdValue > security.maxOrderSize) {
559
+ return {
560
+ content: [
561
+ {
562
+ type: "text",
563
+ text: `Order exceeds maximum allowed size.
564
+ Estimated value: $${estimatedUsdValue.toFixed(2)}
565
+ Maximum allowed: $${security.maxOrderSize}
566
+ Reduce amount or update security.maxOrderSize in config.`
567
+ }
568
+ ],
569
+ isError: true
570
+ };
571
+ }
572
+ } catch {
573
+ console.error("Warning: Could not verify order size against limit");
574
+ }
575
+ }
576
+ if (type === "limit" && !price) {
577
+ return {
578
+ content: [
579
+ {
580
+ type: "text",
581
+ text: 'Limit orders require a price. Please specify the "price" parameter.'
582
+ }
583
+ ],
584
+ isError: true
585
+ };
586
+ }
587
+ try {
588
+ let order;
589
+ if (type === "market") {
590
+ order = await ex.createMarketOrder(normalizedSymbol, side, amount);
591
+ } else {
592
+ order = await ex.createLimitOrder(normalizedSymbol, side, amount, price);
593
+ }
594
+ const result = {
595
+ success: true,
596
+ orderId: order.id,
597
+ exchange,
598
+ symbol: normalizedSymbol,
599
+ side,
600
+ type,
601
+ amount: order.amount,
602
+ filled: order.filled,
603
+ remaining: order.remaining,
604
+ price: order.price ?? order.average,
605
+ cost: order.cost,
606
+ status: order.status,
607
+ timestamp: order.timestamp ?? Date.now()
608
+ };
609
+ return {
610
+ content: [
611
+ {
612
+ type: "text",
613
+ text: JSON.stringify(result, null, 2)
614
+ }
615
+ ]
616
+ };
617
+ } catch (error) {
618
+ return {
619
+ content: [
620
+ {
621
+ type: "text",
622
+ text: `Order failed: ${error.message}`
623
+ }
624
+ ],
625
+ isError: true
626
+ };
627
+ }
628
+ }
629
+ );
630
+ server.tool(
631
+ "get_orders",
632
+ "View open and recent orders. Can filter by exchange, trading pair, or status.",
633
+ {
634
+ exchange: z4.string().optional().describe("Filter by exchange name"),
635
+ symbol: z4.string().optional().describe("Filter by trading pair"),
636
+ status: z4.enum(["open", "closed", "all"]).default("open").describe('Order status filter: "open", "closed", or "all"'),
637
+ limit: z4.number().positive().max(100).default(20).describe("Maximum orders to return")
638
+ },
639
+ async ({ exchange, symbol, status, limit }) => {
640
+ const orders = [];
641
+ const errors = [];
642
+ const normalizedSymbol = symbol?.toUpperCase();
643
+ const exchangesToQuery = exchange ? [[exchange.toLowerCase(), exchangeManager.get(exchange)]].filter(
644
+ ([, ex]) => ex !== void 0
645
+ ) : Array.from(exchangeManager.getAll().entries());
646
+ for (const [name, ex] of exchangesToQuery) {
647
+ if (!ex) continue;
648
+ try {
649
+ let fetchedOrders = [];
650
+ if (status === "open" || status === "all") {
651
+ const open = await ex.fetchOpenOrders(normalizedSymbol);
652
+ fetchedOrders.push(...open.map((o) => ({ ...o, exchange: name })));
653
+ }
654
+ if (status === "closed" || status === "all") {
655
+ const closed = await ex.fetchClosedOrders(normalizedSymbol, void 0, limit);
656
+ fetchedOrders.push(...closed.map((o) => ({ ...o, exchange: name })));
657
+ }
658
+ orders.push(...fetchedOrders);
659
+ } catch (error) {
660
+ errors.push(`${name}: ${error.message}`);
661
+ }
662
+ }
663
+ orders.sort((a, b) => {
664
+ const aTime = a.timestamp ?? 0;
665
+ const bTime = b.timestamp ?? 0;
666
+ return bTime - aTime;
667
+ });
668
+ const limited = orders.slice(0, limit);
669
+ const result = {
670
+ count: limited.length,
671
+ status,
672
+ orders: limited,
673
+ errors: errors.length > 0 ? errors : void 0
674
+ };
675
+ return {
676
+ content: [
677
+ {
678
+ type: "text",
679
+ text: JSON.stringify(result, null, 2)
680
+ }
681
+ ]
682
+ };
683
+ }
684
+ );
685
+ server.tool(
686
+ "cancel_order",
687
+ "Cancel an open order by its ID.",
688
+ {
689
+ exchange: z4.string().describe("Exchange where the order was placed"),
690
+ orderId: z4.string().describe("Order ID to cancel"),
691
+ symbol: z4.string().describe("Trading pair of the order")
692
+ },
693
+ async ({ exchange, orderId, symbol }) => {
694
+ const ex = exchangeManager.get(exchange);
695
+ if (!ex) {
696
+ return {
697
+ content: [
698
+ {
699
+ type: "text",
700
+ text: `Exchange not configured: ${exchange}`
701
+ }
702
+ ],
703
+ isError: true
704
+ };
705
+ }
706
+ try {
707
+ const result = await ex.cancelOrder(orderId, symbol.toUpperCase());
708
+ return {
709
+ content: [
710
+ {
711
+ type: "text",
712
+ text: JSON.stringify(
713
+ {
714
+ success: true,
715
+ orderId,
716
+ exchange,
717
+ symbol: symbol.toUpperCase(),
718
+ status: "cancelled",
719
+ result
720
+ },
721
+ null,
722
+ 2
723
+ )
724
+ }
725
+ ]
726
+ };
727
+ } catch (error) {
728
+ return {
729
+ content: [
730
+ {
731
+ type: "text",
732
+ text: `Failed to cancel order: ${error.message}`
733
+ }
734
+ ],
735
+ isError: true
736
+ };
737
+ }
738
+ }
739
+ );
740
+ }
741
+
742
+ // src/tools/arbitrage.ts
743
+ import { z as z5 } from "zod";
744
+ var DEFAULT_SYMBOLS = [
745
+ "BTC/USDT",
746
+ "ETH/USDT",
747
+ "BNB/USDT",
748
+ "SOL/USDT",
749
+ "XRP/USDT",
750
+ "ADA/USDT",
751
+ "DOGE/USDT",
752
+ "DOT/USDT",
753
+ "MATIC/USDT",
754
+ "LTC/USDT"
755
+ ];
756
+ function registerArbitrageTools(server, exchangeManager) {
757
+ server.tool(
758
+ "get_arbitrage",
759
+ "Find arbitrage opportunities by comparing prices across exchanges. Identifies pairs where you can buy low on one exchange and sell high on another.",
760
+ {
761
+ symbols: z5.array(z5.string()).optional().describe(
762
+ 'Trading pairs to check (e.g., ["BTC/USDT", "ETH/USDT"]). Defaults to top 10 pairs.'
763
+ ),
764
+ minSpread: z5.number().min(0).max(100).default(0.5).describe("Minimum spread percentage to report (default: 0.5%)")
765
+ },
766
+ async ({ symbols, minSpread }) => {
767
+ const checkSymbols = symbols ?? DEFAULT_SYMBOLS;
768
+ const opportunities = [];
769
+ const exchangeNames = exchangeManager.getNames();
770
+ if (exchangeNames.length < 2) {
771
+ return {
772
+ content: [
773
+ {
774
+ type: "text",
775
+ text: "Arbitrage requires at least 2 exchanges configured. Currently have: " + exchangeNames.length
776
+ }
777
+ ],
778
+ isError: true
779
+ };
780
+ }
781
+ for (const symbol of checkSymbols) {
782
+ const normalizedSymbol = symbol.toUpperCase();
783
+ const prices = [];
784
+ for (const [name, ex] of exchangeManager.getAll()) {
785
+ try {
786
+ const ticker = await ex.fetchTicker(normalizedSymbol);
787
+ if (ticker.bid && ticker.ask && ticker.bid > 0 && ticker.ask > 0) {
788
+ prices.push({
789
+ exchange: name,
790
+ bid: ticker.bid,
791
+ ask: ticker.ask
792
+ });
793
+ }
794
+ } catch {
795
+ continue;
796
+ }
797
+ }
798
+ if (prices.length < 2) continue;
799
+ const bestBuy = prices.reduce((a, b) => a.ask < b.ask ? a : b);
800
+ const bestSell = prices.reduce((a, b) => a.bid > b.bid ? a : b);
801
+ if (bestSell.bid > bestBuy.ask) {
802
+ const spread = bestSell.bid - bestBuy.ask;
803
+ const spreadPercent = spread / bestBuy.ask * 100;
804
+ if (spreadPercent >= minSpread) {
805
+ opportunities.push({
806
+ symbol: normalizedSymbol,
807
+ buyExchange: bestBuy.exchange,
808
+ buyPrice: bestBuy.ask,
809
+ sellExchange: bestSell.exchange,
810
+ sellPrice: bestSell.bid,
811
+ spreadPercent: parseFloat(spreadPercent.toFixed(3)),
812
+ potentialProfit: parseFloat(spread.toFixed(8))
813
+ });
814
+ }
815
+ }
816
+ }
817
+ opportunities.sort((a, b) => b.spreadPercent - a.spreadPercent);
818
+ const result = {
819
+ found: opportunities.length,
820
+ opportunities,
821
+ note: "Spreads shown BEFORE trading fees. Always account for:\n\u2022 Trading fees on both exchanges (typically 0.1-0.2% each)\n\u2022 Transfer fees if moving funds between exchanges\n\u2022 Slippage on larger orders\n\u2022 Price movement during execution"
822
+ };
823
+ return {
824
+ content: [
825
+ {
826
+ type: "text",
827
+ text: JSON.stringify(result, null, 2)
828
+ }
829
+ ]
830
+ };
831
+ }
832
+ );
833
+ server.tool(
834
+ "check_spread",
835
+ "Check the price spread for a single trading pair across all exchanges.",
836
+ {
837
+ symbol: z5.string().describe('Trading pair to check (e.g., "BTC/USDT")')
838
+ },
839
+ async ({ symbol }) => {
840
+ const normalizedSymbol = symbol.toUpperCase();
841
+ const prices = [];
842
+ for (const [name, ex] of exchangeManager.getAll()) {
843
+ try {
844
+ const ticker = await ex.fetchTicker(normalizedSymbol);
845
+ if (ticker.bid && ticker.ask) {
846
+ const spread = ticker.ask - ticker.bid;
847
+ const spreadPercent = spread / ticker.bid * 100;
848
+ prices.push({
849
+ exchange: name,
850
+ bid: ticker.bid,
851
+ ask: ticker.ask,
852
+ spread: parseFloat(spread.toFixed(8)),
853
+ spreadPercent: parseFloat(spreadPercent.toFixed(3))
854
+ });
855
+ }
856
+ } catch {
857
+ continue;
858
+ }
859
+ }
860
+ if (prices.length === 0) {
861
+ return {
862
+ content: [
863
+ {
864
+ type: "text",
865
+ text: `No prices found for ${normalizedSymbol} on any exchange.`
866
+ }
867
+ ],
868
+ isError: true
869
+ };
870
+ }
871
+ prices.sort((a, b) => a.ask - b.ask);
872
+ let arbitrage = null;
873
+ if (prices.length >= 2) {
874
+ const bestBuy = prices[0];
875
+ const bestSell = prices.reduce((a, b) => a.bid > b.bid ? a : b);
876
+ if (bestSell.bid > bestBuy.ask && bestSell.exchange !== bestBuy.exchange) {
877
+ const profit = bestSell.bid - bestBuy.ask;
878
+ const profitPercent = profit / bestBuy.ask * 100;
879
+ arbitrage = {
880
+ exists: true,
881
+ buyOn: bestBuy.exchange,
882
+ buyAt: bestBuy.ask,
883
+ sellOn: bestSell.exchange,
884
+ sellAt: bestSell.bid,
885
+ profit: parseFloat(profit.toFixed(8)),
886
+ profitPercent: parseFloat(profitPercent.toFixed(3)) + "%"
887
+ };
888
+ } else {
889
+ arbitrage = {
890
+ exists: false,
891
+ reason: "Best bid is not higher than best ask across exchanges"
892
+ };
893
+ }
894
+ }
895
+ const result = {
896
+ symbol: normalizedSymbol,
897
+ exchanges: prices.length,
898
+ prices,
899
+ arbitrage
900
+ };
901
+ return {
902
+ content: [
903
+ {
904
+ type: "text",
905
+ text: JSON.stringify(result, null, 2)
906
+ }
907
+ ]
908
+ };
909
+ }
910
+ );
911
+ }
912
+
913
+ // src/index.ts
914
+ var VERSION = "0.1.0";
915
+ function showBanner() {
916
+ console.error(`
917
+ \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
918
+ \u2551 \u2551
919
+ \u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2551
920
+ \u2551 \u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557 \u2551
921
+ \u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2588\u2588\u2554\u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551 \u2551
922
+ \u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2554\u255D\u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551 \u2551
923
+ \u2551 \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u255A\u2550\u255D \u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2551
924
+ \u2551 \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u2551
925
+ \u2551 \u2551
926
+ \u2551 MCP Server v${VERSION} \u2551
927
+ \u2551 One AI. 107 Exchanges. Natural language trading. \u2551
928
+ \u2551 \u2551
929
+ \u2551 by Connectry Labs \u2022 https://connectry.io \u2551
930
+ \u2551 \u2551
931
+ \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
932
+ `);
933
+ }
934
+ async function main() {
935
+ showBanner();
936
+ console.error("Loading configuration...");
937
+ let config;
938
+ try {
939
+ config = loadConfig();
940
+ } catch (error) {
941
+ console.error(`
942
+ \u274C ${error.message}`);
943
+ process.exit(1);
944
+ }
945
+ console.error("\nInitializing exchanges...");
946
+ let exchangeManager;
947
+ try {
948
+ exchangeManager = new ExchangeManager(config);
949
+ } catch (error) {
950
+ console.error(`
951
+ \u274C ${error.message}`);
952
+ process.exit(1);
953
+ }
954
+ console.error("\nStarting MCP server...");
955
+ const server = new McpServer({
956
+ name: "omnitrade-mcp",
957
+ version: VERSION
958
+ });
959
+ registerBalanceTools(server, exchangeManager);
960
+ registerPriceTools(server, exchangeManager);
961
+ registerOrderTools(server, exchangeManager, config);
962
+ registerArbitrageTools(server, exchangeManager);
963
+ console.error("\u2713 Tools registered: get_balances, get_portfolio, get_prices, compare_prices, place_order, get_orders, cancel_order, get_arbitrage, check_spread");
964
+ if (config.security?.testnetOnly) {
965
+ console.error("\n\u{1F512} TESTNET-ONLY MODE: All exchanges forced to testnet");
966
+ }
967
+ if (config.security?.maxOrderSize) {
968
+ console.error(`\u{1F512} Order size limit: $${config.security.maxOrderSize} max per trade`);
969
+ }
970
+ if (config.security?.allowedPairs?.length) {
971
+ console.error(`\u{1F512} Trading pairs whitelist: ${config.security.allowedPairs.join(", ")}`);
972
+ }
973
+ const transport = new StdioServerTransport();
974
+ await server.connect(transport);
975
+ console.error("\n\u2705 OmniTrade MCP server is ready!");
976
+ console.error(" Waiting for Claude to connect via MCP...\n");
977
+ }
978
+ process.on("uncaughtException", (error) => {
979
+ console.error("Uncaught exception:", error.message);
980
+ process.exit(1);
981
+ });
982
+ process.on("unhandledRejection", (reason) => {
983
+ console.error("Unhandled rejection:", reason);
984
+ process.exit(1);
985
+ });
986
+ main().catch((error) => {
987
+ console.error("Fatal error:", error.message);
988
+ process.exit(1);
989
+ });
990
+ /**
991
+ * OmniTrade MCP Server
992
+ *
993
+ * Multi-exchange AI trading via Model Context Protocol.
994
+ * Connects Claude to 107+ cryptocurrency exchanges through CCXT.
995
+ *
996
+ * @author Connectry Labs
997
+ * @license MIT
998
+ * @see https://github.com/Connectry-io/omnitrade-mcp
999
+ */
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "omnitrade-mcp",
3
+ "version": "0.1.0",
4
+ "description": "Multi-exchange AI trading via MCP. 107 exchanges. One AI.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "omnitrade-mcp": "dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsup src/index.ts --format esm --dts --clean",
12
+ "dev": "tsup src/index.ts --format esm --watch",
13
+ "test": "vitest",
14
+ "lint": "eslint src/",
15
+ "typecheck": "tsc --noEmit",
16
+ "prepublishOnly": "npm run build"
17
+ },
18
+ "keywords": [
19
+ "mcp",
20
+ "model-context-protocol",
21
+ "trading",
22
+ "crypto",
23
+ "cryptocurrency",
24
+ "ai",
25
+ "claude",
26
+ "binance",
27
+ "coinbase",
28
+ "kraken",
29
+ "exchange",
30
+ "arbitrage",
31
+ "ccxt"
32
+ ],
33
+ "author": {
34
+ "name": "Connectry Labs",
35
+ "email": "labs@connectry.io",
36
+ "url": "https://connectry.io"
37
+ },
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "license": "MIT",
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "https://github.com/Connectry-io/omnitrade-mcp"
45
+ },
46
+ "homepage": "https://github.com/Connectry-io/omnitrade-mcp#readme",
47
+ "bugs": {
48
+ "url": "https://github.com/Connectry-io/omnitrade-mcp/issues"
49
+ },
50
+ "engines": {
51
+ "node": ">=18.0.0"
52
+ },
53
+ "files": [
54
+ "dist",
55
+ "README.md",
56
+ "LICENSE"
57
+ ],
58
+ "dependencies": {
59
+ "@modelcontextprotocol/sdk": "^1.26.0",
60
+ "ccxt": "^4.5.38",
61
+ "zod": "^4.3.6"
62
+ },
63
+ "devDependencies": {
64
+ "@types/node": "^25.2.3",
65
+ "tsup": "^8.5.1",
66
+ "typescript": "^5.9.3"
67
+ }
68
+ }