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 +21 -0
- package/README.md +240 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +999 -0
- package/package.json +68 -0
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
|
+
[](https://www.npmjs.com/package/omnitrade-mcp)
|
|
8
|
+
[](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)
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|