finoptima 1.0.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/index.js ADDED
@@ -0,0 +1,188 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * EcoChain MCP Server (DEC-2026-043)
4
+ *
5
+ * Model Context Protocol server for AI agent access to EcoChain.
6
+ * Provides 22 tools: 1 login + 21 platform tools.
7
+ *
8
+ * Authentication: EcoAuth login flow (no hardcoded credentials)
9
+ * 1. AI asks user for their phone number
10
+ * 2. AI calls `login` tool → push notification sent to EcoAuth app
11
+ * 3. User approves on their phone
12
+ * 4. All subsequent tools use the JWT token
13
+ *
14
+ * Environment variables:
15
+ * ECOCHAIN_API_URL — Optional. API base URL (default: https://api.toutcreer.com)
16
+ */
17
+
18
+ const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
19
+ const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
20
+ const {
21
+ CallToolRequestSchema,
22
+ ListToolsRequestSchema,
23
+ } = require('@modelcontextprotocol/sdk/types.js');
24
+
25
+ const { allTools } = require('./tools/index.js');
26
+ const apiClient = require('./lib/api-client.js');
27
+
28
+ const POLL_INTERVAL_MS = 2000;
29
+ const POLL_TIMEOUT_MS = 120000;
30
+
31
+ let authenticated = false;
32
+
33
+ /**
34
+ * EcoAuth login flow
35
+ */
36
+ async function loginWithEcoAuth(phoneNumber) {
37
+ const loginResponse = await apiClient.post('/auth/login', { phoneNumber });
38
+
39
+ if (!loginResponse.twoFactorRequired && loginResponse.token) {
40
+ return loginResponse;
41
+ }
42
+
43
+ if (!loginResponse.twoFactorRequired || !loginResponse.challenge?.token) {
44
+ throw new Error('Unexpected login response: ' + JSON.stringify(loginResponse));
45
+ }
46
+
47
+ const challengeToken = loginResponse.challenge.token;
48
+ const deviceName = loginResponse.challenge.device_name || 'your device';
49
+ const userName = loginResponse.user?.displayName || phoneNumber;
50
+
51
+ // Return info for the AI to tell the user
52
+ const waitMessage = `Challenge sent to ${deviceName}. ${userName}, please approve on your EcoAuth app.`;
53
+ console.error(`[EcoChain MCP] ${waitMessage}`);
54
+
55
+ const startTime = Date.now();
56
+
57
+ while (Date.now() - startTime < POLL_TIMEOUT_MS) {
58
+ await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL_MS));
59
+
60
+ try {
61
+ const status = await apiClient.get(`/authenticator/challenge/${challengeToken}/status`);
62
+
63
+ if (status.status === 'approved') {
64
+ const completeResponse = await apiClient.post('/auth/login/complete', { challengeToken });
65
+ if (!completeResponse.token) {
66
+ throw new Error('Login complete but no token received');
67
+ }
68
+ return completeResponse;
69
+ }
70
+
71
+ if (status.status === 'denied') {
72
+ throw new Error('Login denied by user on EcoAuth');
73
+ }
74
+
75
+ if (status.status === 'expired') {
76
+ throw new Error('Challenge expired. Please try again.');
77
+ }
78
+ } catch (err) {
79
+ if (err.message?.includes('denied') || err.message?.includes('expired')) throw err;
80
+ console.error(`[EcoChain MCP] Poll error (retrying): ${err.message || err.error || err}`);
81
+ }
82
+ }
83
+
84
+ throw new Error(`Login timeout: no approval received within ${POLL_TIMEOUT_MS / 1000}s`);
85
+ }
86
+
87
+ // Login tool — the AI calls this after asking the user for their phone number
88
+ const loginTool = {
89
+ name: 'login',
90
+ description: 'Login to EcoChain via EcoAuth. Ask the user for their phone number first (e.g. +33612345678), then call this tool. The user will receive a push notification on their EcoAuth app and must approve it. This must be called before any other tool.',
91
+ inputSchema: {
92
+ type: 'object',
93
+ properties: {
94
+ phone_number: { type: 'string', description: 'User phone number in international format (e.g. +33612345678)' }
95
+ },
96
+ required: ['phone_number']
97
+ },
98
+ handler: async ({ phone_number }) => {
99
+ if (authenticated) {
100
+ return JSON.stringify({ status: 'already_authenticated', user_id: process.env.ECOCHAIN_USER_ID });
101
+ }
102
+
103
+ const result = await loginWithEcoAuth(phone_number);
104
+ apiClient.setToken(result.token);
105
+ process.env.ECOCHAIN_USER_ID = result.user.uid;
106
+ authenticated = true;
107
+
108
+ return JSON.stringify({
109
+ status: 'authenticated',
110
+ user: result.user.displayName,
111
+ user_id: result.user.uid,
112
+ expires_in: result.expiresIn
113
+ });
114
+ }
115
+ };
116
+
117
+ // All tools: login first, then platform tools
118
+ const allToolsWithLogin = [loginTool, ...allTools];
119
+
120
+ // Create MCP server
121
+ const server = new Server(
122
+ {
123
+ name: 'ecochain',
124
+ version: '1.0.0',
125
+ },
126
+ {
127
+ capabilities: {
128
+ tools: {},
129
+ },
130
+ }
131
+ );
132
+
133
+ // List tools
134
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
135
+ return {
136
+ tools: allToolsWithLogin.map(tool => ({
137
+ name: tool.name,
138
+ description: tool.description,
139
+ inputSchema: tool.inputSchema,
140
+ })),
141
+ };
142
+ });
143
+
144
+ // Execute tools
145
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
146
+ const { name, arguments: args } = request.params;
147
+
148
+ const tool = allToolsWithLogin.find(t => t.name === name);
149
+ if (!tool) {
150
+ return {
151
+ content: [{ type: 'text', text: `Unknown tool: ${name}` }],
152
+ isError: true,
153
+ };
154
+ }
155
+
156
+ // Block all tools except login if not authenticated
157
+ if (!authenticated && name !== 'login') {
158
+ return {
159
+ content: [{ type: 'text', text: 'Not authenticated. Please ask the user for their phone number and call the `login` tool first.' }],
160
+ isError: true,
161
+ };
162
+ }
163
+
164
+ try {
165
+ const result = await tool.handler(args || {});
166
+ return {
167
+ content: [{ type: 'text', text: result }],
168
+ };
169
+ } catch (err) {
170
+ const errorMessage = err.error || err.message || String(err);
171
+ return {
172
+ content: [{ type: 'text', text: `Error: ${errorMessage}` }],
173
+ isError: true,
174
+ };
175
+ }
176
+ });
177
+
178
+ // Start server — no auth at startup, AI will call login tool
179
+ async function main() {
180
+ const transport = new StdioServerTransport();
181
+ await server.connect(transport);
182
+ console.error('[EcoChain MCP] Server started. Call the `login` tool to authenticate.');
183
+ }
184
+
185
+ main().catch((err) => {
186
+ console.error('[EcoChain MCP] Fatal error:', err);
187
+ process.exit(1);
188
+ });
@@ -0,0 +1,102 @@
1
+ /**
2
+ * EcoChain API Client (DEC-2026-043)
3
+ *
4
+ * Makes authenticated HTTPS requests to the backend API.
5
+ * Auth: JWT Bearer token obtained via EcoAuth login flow.
6
+ */
7
+
8
+ const https = require('https');
9
+ const http = require('http');
10
+
11
+ class ApiClient {
12
+ constructor() {
13
+ this.apiUrl = process.env.ECOCHAIN_API_URL || 'https://api.toutcreer.com';
14
+ this.jwtToken = null; // Set after EcoAuth login
15
+
16
+ const parsed = new URL(this.apiUrl);
17
+ this.hostname = parsed.hostname;
18
+ this.port = parsed.port || (parsed.protocol === 'https:' ? 443 : 80);
19
+ this.protocol = parsed.protocol;
20
+ this.basePath = parsed.pathname.replace(/\/$/, '');
21
+ }
22
+
23
+ /**
24
+ * Set JWT token after successful EcoAuth login
25
+ */
26
+ setToken(token) {
27
+ this.jwtToken = token;
28
+ }
29
+
30
+ /**
31
+ * Make an HTTP request to the backend API
32
+ */
33
+ async request(method, path, body = null) {
34
+ const fullPath = `${this.basePath}/v3${path}`;
35
+
36
+ const headers = {
37
+ 'Content-Type': 'application/json',
38
+ };
39
+
40
+ if (this.jwtToken) {
41
+ headers['Authorization'] = `Bearer ${this.jwtToken}`;
42
+ }
43
+
44
+ const bodyStr = body ? JSON.stringify(body) : null;
45
+ if (bodyStr) {
46
+ headers['Content-Length'] = Buffer.byteLength(bodyStr);
47
+ }
48
+
49
+ const options = {
50
+ hostname: this.hostname,
51
+ port: this.port,
52
+ path: fullPath,
53
+ method: method.toUpperCase(),
54
+ headers
55
+ };
56
+
57
+ const transport = this.protocol === 'https:' ? https : http;
58
+
59
+ return new Promise((resolve, reject) => {
60
+ const req = transport.request(options, (res) => {
61
+ let data = '';
62
+ res.on('data', chunk => { data += chunk; });
63
+ res.on('end', () => {
64
+ try {
65
+ const parsed = JSON.parse(data);
66
+ if (res.statusCode >= 400) {
67
+ reject({
68
+ status: res.statusCode,
69
+ error: parsed.error || `HTTP ${res.statusCode}`,
70
+ data: parsed
71
+ });
72
+ } else {
73
+ resolve(parsed);
74
+ }
75
+ } catch {
76
+ if (res.statusCode >= 400) {
77
+ reject({ status: res.statusCode, error: data });
78
+ } else {
79
+ resolve(data);
80
+ }
81
+ }
82
+ });
83
+ });
84
+
85
+ req.on('error', reject);
86
+ req.setTimeout(30000, () => {
87
+ req.destroy();
88
+ reject(new Error('Request timeout'));
89
+ });
90
+
91
+ if (bodyStr) req.write(bodyStr);
92
+ req.end();
93
+ });
94
+ }
95
+
96
+ // Convenience methods
97
+ async get(path) { return this.request('GET', path); }
98
+ async post(path, body) { return this.request('POST', path, body); }
99
+ async delete(path, body) { return this.request('DELETE', path, body); }
100
+ }
101
+
102
+ module.exports = new ApiClient();
package/lib/logger.js ADDED
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Activity Logger for MCP Server (DEC-2026-043)
3
+ *
4
+ * Logs every MCP tool invocation to the backend activity log.
5
+ * Non-blocking: failures don't affect the tool response.
6
+ */
7
+
8
+ const apiClient = require('./api-client');
9
+
10
+ /**
11
+ * Log an agent action to the activity log
12
+ */
13
+ async function logActivity({ action, requestData, responseSummary, status, errorMessage, durationMs }) {
14
+ try {
15
+ await apiClient.post('/agent-keys/log', {
16
+ action,
17
+ request_data: requestData || null,
18
+ response_summary: responseSummary || null,
19
+ status: status || 'success',
20
+ error_message: errorMessage || null,
21
+ duration_ms: durationMs || null
22
+ });
23
+ } catch (err) {
24
+ // Non-blocking: log errors silently
25
+ console.error('[MCP Logger] Failed to log activity:', err.message || err);
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Wrap a tool handler with automatic logging
31
+ */
32
+ function withLogging(toolName, handler) {
33
+ return async (args) => {
34
+ const start = Date.now();
35
+ try {
36
+ const result = await handler(args);
37
+ const durationMs = Date.now() - start;
38
+
39
+ // Log success (non-blocking)
40
+ logActivity({
41
+ action: toolName,
42
+ requestData: args,
43
+ responseSummary: { success: true },
44
+ status: 'success',
45
+ durationMs
46
+ });
47
+
48
+ return result;
49
+ } catch (err) {
50
+ const durationMs = Date.now() - start;
51
+
52
+ // Log error (non-blocking)
53
+ logActivity({
54
+ action: toolName,
55
+ requestData: args,
56
+ status: 'error',
57
+ errorMessage: err.message || String(err),
58
+ durationMs
59
+ });
60
+
61
+ throw err;
62
+ }
63
+ };
64
+ }
65
+
66
+ module.exports = { logActivity, withLogging };
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "finoptima",
3
+ "version": "1.0.0",
4
+ "description": "FINOPTIMA — EcoChain AI Finance Agent via MCP. Portfolio optimization, AMM liquidity, trading, swaps. Secured by EcoAuth 2FA.",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "finoptima": "index.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node index.js"
11
+ },
12
+ "keywords": [
13
+ "mcp",
14
+ "ai-agent",
15
+ "finance",
16
+ "defi",
17
+ "amm",
18
+ "trading",
19
+ "ecochain",
20
+ "ecoauth",
21
+ "model-context-protocol"
22
+ ],
23
+ "author": "EcoChain Team",
24
+ "license": "MIT",
25
+ "homepage": "https://www.toutcreer.com/trading/finoptima.md",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://www.toutcreer.com"
29
+ },
30
+ "dependencies": {
31
+ "@modelcontextprotocol/sdk": "^1.0.0"
32
+ },
33
+ "engines": {
34
+ "node": ">=18.0.0"
35
+ }
36
+ }
package/tools/index.js ADDED
@@ -0,0 +1,399 @@
1
+ /**
2
+ * MCP Tools Registry (DEC-2026-043)
3
+ *
4
+ * 21 tools organized by scope:
5
+ * - READ (14): Portfolio, balances, prices, pools, orderbook, etc.
6
+ * - TRADE (2): Place/cancel orders
7
+ * - SWAP (2): Quote/execute AMM swaps
8
+ * - LIQUIDITY (2): Add/remove liquidity
9
+ * - SEND (1): Internal transfers
10
+ */
11
+
12
+ const apiClient = require('../lib/api-client');
13
+ const { withLogging } = require('../lib/logger');
14
+
15
+ // Helper: get user ID from env (set at startup from API key lookup)
16
+ function uid() {
17
+ return process.env.ECOCHAIN_USER_ID || 'me';
18
+ }
19
+
20
+ // ============================================
21
+ // READ TOOLS (scope: read)
22
+ // ============================================
23
+
24
+ const readTools = [
25
+ {
26
+ name: 'get_balances',
27
+ description: 'Get all wallet balances for the user (ECO, BTC, ETH). Returns chain, balance, and formatted values.',
28
+ inputSchema: { type: 'object', properties: {}, required: [] },
29
+ handler: withLogging('get_balances', async () => {
30
+ const data = await apiClient.get(`/wallets/${uid()}`);
31
+ return JSON.stringify(data, null, 2);
32
+ })
33
+ },
34
+ {
35
+ name: 'get_balance',
36
+ description: 'Get balance for a specific chain (ECO, BTC, or ETH).',
37
+ inputSchema: {
38
+ type: 'object',
39
+ properties: {
40
+ chain: { type: 'string', description: 'Chain name: ECO, BTC, or ETH', enum: ['ECO', 'BTC', 'ETH'] }
41
+ },
42
+ required: ['chain']
43
+ },
44
+ handler: withLogging('get_balance', async ({ chain }) => {
45
+ const data = await apiClient.get(`/wallets/${uid()}/${chain}/balance`);
46
+ return JSON.stringify(data, null, 2);
47
+ })
48
+ },
49
+ {
50
+ name: 'get_portfolio',
51
+ description: 'Get complete portfolio view: all wallets plus LP positions in liquidity pools.',
52
+ inputSchema: { type: 'object', properties: {}, required: [] },
53
+ handler: withLogging('get_portfolio', async () => {
54
+ const [wallets, positions] = await Promise.all([
55
+ apiClient.get(`/wallets/${uid()}`),
56
+ apiClient.get(`/amm/positions/${uid()}`)
57
+ ]);
58
+ return JSON.stringify({ wallets, lp_positions: positions }, null, 2);
59
+ })
60
+ },
61
+ {
62
+ name: 'get_prices',
63
+ description: 'Get current BTC and ETH prices in EUR from CoinGecko.',
64
+ inputSchema: { type: 'object', properties: {}, required: [] },
65
+ handler: withLogging('get_prices', async () => {
66
+ const data = await apiClient.get('/prices');
67
+ return JSON.stringify(data, null, 2);
68
+ })
69
+ },
70
+ {
71
+ name: 'get_pools',
72
+ description: 'List all AMM liquidity pools with their reserves, fees, and TVL.',
73
+ inputSchema: { type: 'object', properties: {}, required: [] },
74
+ handler: withLogging('get_pools', async () => {
75
+ const data = await apiClient.get('/amm/pools');
76
+ return JSON.stringify(data, null, 2);
77
+ })
78
+ },
79
+ {
80
+ name: 'get_pool_details',
81
+ description: 'Get detailed information about a specific liquidity pool (reserves, constant product k, fee rate).',
82
+ inputSchema: {
83
+ type: 'object',
84
+ properties: {
85
+ pool_id: { type: 'string', description: 'The UUID of the pool' }
86
+ },
87
+ required: ['pool_id']
88
+ },
89
+ handler: withLogging('get_pool_details', async ({ pool_id }) => {
90
+ const data = await apiClient.get(`/amm/pools/${pool_id}`);
91
+ return JSON.stringify(data, null, 2);
92
+ })
93
+ },
94
+ {
95
+ name: 'get_lp_positions',
96
+ description: 'Get all LP (Liquidity Provider) positions for the user, showing share of each pool.',
97
+ inputSchema: { type: 'object', properties: {}, required: [] },
98
+ handler: withLogging('get_lp_positions', async () => {
99
+ const data = await apiClient.get(`/amm/positions/${uid()}`);
100
+ return JSON.stringify(data, null, 2);
101
+ })
102
+ },
103
+ {
104
+ name: 'get_orderbook',
105
+ description: 'Get the order book (bids and asks) for a trading pair.',
106
+ inputSchema: {
107
+ type: 'object',
108
+ properties: {
109
+ pair_id: { type: 'string', description: 'Trading pair ID (e.g., ECO-BTC)' }
110
+ },
111
+ required: ['pair_id']
112
+ },
113
+ handler: withLogging('get_orderbook', async ({ pair_id }) => {
114
+ const data = await apiClient.get(`/trading/orderbook/${pair_id}`);
115
+ return JSON.stringify(data, null, 2);
116
+ })
117
+ },
118
+ {
119
+ name: 'get_ticker',
120
+ description: 'Get 24h trading statistics for a pair (price, volume, high, low, change).',
121
+ inputSchema: {
122
+ type: 'object',
123
+ properties: {
124
+ pair_id: { type: 'string', description: 'Trading pair ID (e.g., ECO-BTC)' }
125
+ },
126
+ required: ['pair_id']
127
+ },
128
+ handler: withLogging('get_ticker', async ({ pair_id }) => {
129
+ const data = await apiClient.get(`/trading/ticker/${pair_id}`);
130
+ return JSON.stringify(data, null, 2);
131
+ })
132
+ },
133
+ {
134
+ name: 'get_candles',
135
+ description: 'Get OHLCV candlestick data for technical analysis. Intervals: 1m, 5m, 15m, 1h, 4h, 1d, 1w.',
136
+ inputSchema: {
137
+ type: 'object',
138
+ properties: {
139
+ pair_id: { type: 'string', description: 'Trading pair ID' },
140
+ interval: { type: 'string', description: 'Candle interval', enum: ['1m', '5m', '15m', '1h', '4h', '1d', '1w'] },
141
+ limit: { type: 'number', description: 'Number of candles (default 100, max 500)' }
142
+ },
143
+ required: ['pair_id']
144
+ },
145
+ handler: withLogging('get_candles', async ({ pair_id, interval, limit }) => {
146
+ const params = new URLSearchParams();
147
+ if (interval) params.set('interval', interval);
148
+ if (limit) params.set('limit', limit.toString());
149
+ const qs = params.toString();
150
+ const data = await apiClient.get(`/trading/candles/${pair_id}${qs ? '?' + qs : ''}`);
151
+ return JSON.stringify(data, null, 2);
152
+ })
153
+ },
154
+ {
155
+ name: 'get_trading_pairs',
156
+ description: 'List all active trading pairs on the exchange.',
157
+ inputSchema: { type: 'object', properties: {}, required: [] },
158
+ handler: withLogging('get_trading_pairs', async () => {
159
+ const data = await apiClient.get('/trading/pairs');
160
+ return JSON.stringify(data, null, 2);
161
+ })
162
+ },
163
+ {
164
+ name: 'get_my_orders',
165
+ description: 'Get all open orders for the user, optionally filtered by status.',
166
+ inputSchema: {
167
+ type: 'object',
168
+ properties: {
169
+ status: { type: 'string', description: 'Filter by status: open, filled, cancelled', enum: ['open', 'filled', 'cancelled'] }
170
+ },
171
+ required: []
172
+ },
173
+ handler: withLogging('get_my_orders', async ({ status }) => {
174
+ const params = status ? `?status=${status}` : '';
175
+ const data = await apiClient.get(`/trading/orders/${uid()}${params}`);
176
+ return JSON.stringify(data, null, 2);
177
+ })
178
+ },
179
+ {
180
+ name: 'get_trade_history',
181
+ description: 'Get the trade execution history for the user.',
182
+ inputSchema: {
183
+ type: 'object',
184
+ properties: {
185
+ limit: { type: 'number', description: 'Number of trades (default 50)' }
186
+ },
187
+ required: []
188
+ },
189
+ handler: withLogging('get_trade_history', async ({ limit }) => {
190
+ const params = limit ? `?limit=${limit}` : '';
191
+ const data = await apiClient.get(`/trading/history/${uid()}${params}`);
192
+ return JSON.stringify(data, null, 2);
193
+ })
194
+ },
195
+ {
196
+ name: 'get_transaction_history',
197
+ description: 'Get transaction history (sends, receives, deposits, withdrawals).',
198
+ inputSchema: {
199
+ type: 'object',
200
+ properties: {
201
+ limit: { type: 'number', description: 'Number of transactions (default 50)' },
202
+ type: { type: 'string', description: 'Filter by type: send, receive, deposit, withdrawal' }
203
+ },
204
+ required: []
205
+ },
206
+ handler: withLogging('get_transaction_history', async ({ limit, type }) => {
207
+ const params = new URLSearchParams();
208
+ if (limit) params.set('limit', limit.toString());
209
+ if (type) params.set('type', type);
210
+ const qs = params.toString();
211
+ const data = await apiClient.get(`/transactions/${uid()}${qs ? '?' + qs : ''}`);
212
+ return JSON.stringify(data, null, 2);
213
+ })
214
+ }
215
+ ];
216
+
217
+ // ============================================
218
+ // TRADE TOOLS (scope: trade)
219
+ // ============================================
220
+
221
+ const tradeTools = [
222
+ {
223
+ name: 'place_order',
224
+ description: 'Place a trading order (limit or market, buy or sell). Returns the created order.',
225
+ inputSchema: {
226
+ type: 'object',
227
+ properties: {
228
+ pair_id: { type: 'string', description: 'Trading pair ID (e.g., ECO-BTC)' },
229
+ side: { type: 'string', description: 'Order side', enum: ['buy', 'sell'] },
230
+ type: { type: 'string', description: 'Order type', enum: ['limit', 'market'] },
231
+ amount: { type: 'number', description: 'Amount to trade (in base currency)' },
232
+ price: { type: 'number', description: 'Price per unit (required for limit orders)' }
233
+ },
234
+ required: ['pair_id', 'side', 'type', 'amount']
235
+ },
236
+ handler: withLogging('place_order', async ({ pair_id, side, type, amount, price }) => {
237
+ const body = {
238
+ user_id: uid(),
239
+ pair_id,
240
+ side,
241
+ type,
242
+ amount
243
+ };
244
+ if (price !== undefined) body.price = price;
245
+ const data = await apiClient.post('/trading/orders', body);
246
+ return JSON.stringify(data, null, 2);
247
+ })
248
+ },
249
+ {
250
+ name: 'cancel_order',
251
+ description: 'Cancel an open trading order by its ID.',
252
+ inputSchema: {
253
+ type: 'object',
254
+ properties: {
255
+ order_id: { type: 'string', description: 'The UUID of the order to cancel' }
256
+ },
257
+ required: ['order_id']
258
+ },
259
+ handler: withLogging('cancel_order', async ({ order_id }) => {
260
+ const data = await apiClient.delete(`/trading/orders/${order_id}`, { user_id: uid() });
261
+ return JSON.stringify(data, null, 2);
262
+ })
263
+ }
264
+ ];
265
+
266
+ // ============================================
267
+ // SWAP TOOLS (scope: swap)
268
+ // ============================================
269
+
270
+ const swapTools = [
271
+ {
272
+ name: 'get_swap_quote',
273
+ description: 'Get a swap quote without executing. Shows expected output, price impact, and fees (0.3%).',
274
+ inputSchema: {
275
+ type: 'object',
276
+ properties: {
277
+ token_in: { type: 'string', description: 'Input token (e.g., ECO, BTC)', enum: ['ECO', 'BTC', 'ETH'] },
278
+ token_out: { type: 'string', description: 'Output token (e.g., BTC, ECO)', enum: ['ECO', 'BTC', 'ETH'] },
279
+ amount_in: { type: 'number', description: 'Amount of input token to swap' }
280
+ },
281
+ required: ['token_in', 'token_out', 'amount_in']
282
+ },
283
+ handler: withLogging('get_swap_quote', async ({ token_in, token_out, amount_in }) => {
284
+ const data = await apiClient.post('/amm/swap/quote', { from_token: token_in, to_token: token_out, amount: amount_in });
285
+ return JSON.stringify(data, null, 2);
286
+ })
287
+ },
288
+ {
289
+ name: 'execute_swap',
290
+ description: 'Execute an AMM swap. Swaps tokens through a liquidity pool with 0.3% fee. Returns the executed swap details.',
291
+ inputSchema: {
292
+ type: 'object',
293
+ properties: {
294
+ token_in: { type: 'string', description: 'Input token', enum: ['ECO', 'BTC', 'ETH'] },
295
+ token_out: { type: 'string', description: 'Output token', enum: ['ECO', 'BTC', 'ETH'] },
296
+ amount_in: { type: 'number', description: 'Amount of input token' },
297
+ min_amount_out: { type: 'number', description: 'Minimum acceptable output (slippage protection)' }
298
+ },
299
+ required: ['token_in', 'token_out', 'amount_in']
300
+ },
301
+ handler: withLogging('execute_swap', async ({ token_in, token_out, amount_in, min_amount_out }) => {
302
+ const body = { user_id: uid(), token_in, token_out, amount_in };
303
+ if (min_amount_out !== undefined) body.min_amount_out = min_amount_out;
304
+ const data = await apiClient.post('/amm/swap', body);
305
+ return JSON.stringify(data, null, 2);
306
+ })
307
+ }
308
+ ];
309
+
310
+ // ============================================
311
+ // LIQUIDITY TOOLS (scope: liquidity)
312
+ // ============================================
313
+
314
+ const liquidityTools = [
315
+ {
316
+ name: 'add_liquidity',
317
+ description: 'Add liquidity to an AMM pool. You must provide both tokens in the correct ratio. Mints LP tokens in return.',
318
+ inputSchema: {
319
+ type: 'object',
320
+ properties: {
321
+ pool_id: { type: 'string', description: 'Pool UUID to add liquidity to' },
322
+ amount_a: { type: 'number', description: 'Amount of token A to deposit' },
323
+ amount_b: { type: 'number', description: 'Amount of token B to deposit' }
324
+ },
325
+ required: ['pool_id', 'amount_a', 'amount_b']
326
+ },
327
+ handler: withLogging('add_liquidity', async ({ pool_id, amount_a, amount_b }) => {
328
+ const data = await apiClient.post('/amm/liquidity/add', {
329
+ user_id: uid(),
330
+ pool_id,
331
+ amount_a,
332
+ amount_b
333
+ });
334
+ return JSON.stringify(data, null, 2);
335
+ })
336
+ },
337
+ {
338
+ name: 'remove_liquidity',
339
+ description: 'Remove liquidity from an AMM pool by burning LP tokens. Returns both tokens proportionally.',
340
+ inputSchema: {
341
+ type: 'object',
342
+ properties: {
343
+ pool_id: { type: 'string', description: 'Pool UUID' },
344
+ lp_amount: { type: 'number', description: 'Amount of LP tokens to burn' }
345
+ },
346
+ required: ['pool_id', 'lp_amount']
347
+ },
348
+ handler: withLogging('remove_liquidity', async ({ pool_id, lp_amount }) => {
349
+ const data = await apiClient.post('/amm/liquidity/remove', {
350
+ user_id: uid(),
351
+ pool_id,
352
+ lp_amount
353
+ });
354
+ return JSON.stringify(data, null, 2);
355
+ })
356
+ }
357
+ ];
358
+
359
+ // ============================================
360
+ // SEND TOOLS (scope: send)
361
+ // ============================================
362
+
363
+ const sendTools = [
364
+ {
365
+ name: 'send_funds',
366
+ description: 'Send funds to another user (by user ID or phone number). Internal transfer only — no blockchain fees.',
367
+ inputSchema: {
368
+ type: 'object',
369
+ properties: {
370
+ to: { type: 'string', description: 'Recipient user ID (UUID) or phone number' },
371
+ chain: { type: 'string', description: 'Token to send', enum: ['ECO', 'BTC', 'ETH'] },
372
+ amount: { type: 'number', description: 'Amount to send (in base units: cents for ECO, satoshis for BTC)' },
373
+ note: { type: 'string', description: 'Optional note for the transfer' }
374
+ },
375
+ required: ['to', 'chain', 'amount']
376
+ },
377
+ handler: withLogging('send_funds', async ({ to, chain, amount, note }) => {
378
+ const body = {
379
+ from_uid: uid(),
380
+ to_identifier: to,
381
+ chain,
382
+ amount
383
+ };
384
+ if (note) body.note = note;
385
+ const data = await apiClient.post('/transactions/send', body);
386
+ return JSON.stringify(data, null, 2);
387
+ })
388
+ }
389
+ ];
390
+
391
+ // Export all tools grouped by scope
392
+ module.exports = {
393
+ readTools,
394
+ tradeTools,
395
+ swapTools,
396
+ liquidityTools,
397
+ sendTools,
398
+ allTools: [...readTools, ...tradeTools, ...swapTools, ...liquidityTools, ...sendTools]
399
+ };