@zebpay_rajesh/zebpay-mcp-server 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.

Potentially problematic release.


This version of @zebpay_rajesh/zebpay-mcp-server might be problematic. Click here for more details.

Files changed (120) hide show
  1. package/.env.example +14 -0
  2. package/README.md +223 -0
  3. package/dist/__tests__/errors.test.d.ts +5 -0
  4. package/dist/__tests__/errors.test.js +147 -0
  5. package/dist/__tests__/errors.test.js.map +1 -0
  6. package/dist/__tests__/prompts.test.d.ts +1 -0
  7. package/dist/__tests__/prompts.test.js +73 -0
  8. package/dist/__tests__/prompts.test.js.map +1 -0
  9. package/dist/__tests__/resources.test.d.ts +1 -0
  10. package/dist/__tests__/resources.test.js +79 -0
  11. package/dist/__tests__/resources.test.js.map +1 -0
  12. package/dist/__tests__/validation.test.d.ts +15 -0
  13. package/dist/__tests__/validation.test.js +64 -0
  14. package/dist/__tests__/validation.test.js.map +1 -0
  15. package/dist/config.d.ts +19 -0
  16. package/dist/config.js +81 -0
  17. package/dist/config.js.map +1 -0
  18. package/dist/http/httpClient.d.ts +40 -0
  19. package/dist/http/httpClient.js +341 -0
  20. package/dist/http/httpClient.js.map +1 -0
  21. package/dist/index.d.ts +1 -0
  22. package/dist/index.js +60 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/mcp/errors.d.ts +21 -0
  25. package/dist/mcp/errors.js +214 -0
  26. package/dist/mcp/errors.js.map +1 -0
  27. package/dist/mcp/logging.d.ts +21 -0
  28. package/dist/mcp/logging.js +241 -0
  29. package/dist/mcp/logging.js.map +1 -0
  30. package/dist/mcp/prompts.d.ts +9 -0
  31. package/dist/mcp/prompts.js +165 -0
  32. package/dist/mcp/prompts.js.map +1 -0
  33. package/dist/mcp/resources.d.ts +9 -0
  34. package/dist/mcp/resources.js +125 -0
  35. package/dist/mcp/resources.js.map +1 -0
  36. package/dist/mcp/tools_futures.d.ts +5 -0
  37. package/dist/mcp/tools_futures.js +694 -0
  38. package/dist/mcp/tools_futures.js.map +1 -0
  39. package/dist/mcp/tools_spot.d.ts +11 -0
  40. package/dist/mcp/tools_spot.js +2225 -0
  41. package/dist/mcp/tools_spot.js.map +1 -0
  42. package/dist/private/FuturesClient.d.ts +57 -0
  43. package/dist/private/FuturesClient.js +181 -0
  44. package/dist/private/FuturesClient.js.map +1 -0
  45. package/dist/private/SpotClient.d.ts +44 -0
  46. package/dist/private/SpotClient.js +201 -0
  47. package/dist/private/SpotClient.js.map +1 -0
  48. package/dist/private/ZebpayAPI.d.ts +19 -0
  49. package/dist/private/ZebpayAPI.js +172 -0
  50. package/dist/private/ZebpayAPI.js.map +1 -0
  51. package/dist/public/PublicClient.d.ts +79 -0
  52. package/dist/public/PublicClient.js +283 -0
  53. package/dist/public/PublicClient.js.map +1 -0
  54. package/dist/public/PublicFuturesClient.d.ts +27 -0
  55. package/dist/public/PublicFuturesClient.js +187 -0
  56. package/dist/public/PublicFuturesClient.js.map +1 -0
  57. package/dist/security/credentials.d.ts +42 -0
  58. package/dist/security/credentials.js +80 -0
  59. package/dist/security/credentials.js.map +1 -0
  60. package/dist/security/signing.d.ts +33 -0
  61. package/dist/security/signing.js +56 -0
  62. package/dist/security/signing.js.map +1 -0
  63. package/dist/types/responses.d.ts +130 -0
  64. package/dist/types/responses.js +6 -0
  65. package/dist/types/responses.js.map +1 -0
  66. package/dist/utils/cache.d.ts +29 -0
  67. package/dist/utils/cache.js +72 -0
  68. package/dist/utils/cache.js.map +1 -0
  69. package/dist/utils/fileLogger.d.ts +10 -0
  70. package/dist/utils/fileLogger.js +81 -0
  71. package/dist/utils/fileLogger.js.map +1 -0
  72. package/dist/utils/metrics.d.ts +35 -0
  73. package/dist/utils/metrics.js +94 -0
  74. package/dist/utils/metrics.js.map +1 -0
  75. package/dist/utils/responseFormatter.d.ts +93 -0
  76. package/dist/utils/responseFormatter.js +268 -0
  77. package/dist/utils/responseFormatter.js.map +1 -0
  78. package/dist/validation/schemas.d.ts +70 -0
  79. package/dist/validation/schemas.js +48 -0
  80. package/dist/validation/schemas.js.map +1 -0
  81. package/dist/validation/validators.d.ts +28 -0
  82. package/dist/validation/validators.js +129 -0
  83. package/dist/validation/validators.js.map +1 -0
  84. package/docs/LOGGING.md +371 -0
  85. package/docs/zebpay-ai-trading-beginner.png +0 -0
  86. package/mcp-config.json.example +20 -0
  87. package/package.json +54 -0
  88. package/scripts/README.md +103 -0
  89. package/scripts/clear-logs.js +52 -0
  90. package/scripts/log-stats.js +264 -0
  91. package/scripts/log-viewer.js +288 -0
  92. package/server.json +31 -0
  93. package/src/__tests__/errors.test.ts +180 -0
  94. package/src/__tests__/prompts.test.ts +89 -0
  95. package/src/__tests__/resources.test.ts +95 -0
  96. package/src/__tests__/validation.test.ts +88 -0
  97. package/src/config.ts +108 -0
  98. package/src/http/httpClient.ts +398 -0
  99. package/src/index.ts +71 -0
  100. package/src/mcp/errors.ts +262 -0
  101. package/src/mcp/logging.ts +284 -0
  102. package/src/mcp/prompts.ts +206 -0
  103. package/src/mcp/resources.ts +163 -0
  104. package/src/mcp/tools_futures.ts +874 -0
  105. package/src/mcp/tools_spot.ts +2702 -0
  106. package/src/private/FuturesClient.ts +189 -0
  107. package/src/private/SpotClient.ts +250 -0
  108. package/src/private/ZebpayAPI.ts +205 -0
  109. package/src/public/PublicClient.ts +381 -0
  110. package/src/public/PublicFuturesClient.ts +228 -0
  111. package/src/security/credentials.ts +114 -0
  112. package/src/security/signing.ts +98 -0
  113. package/src/types/responses.ts +146 -0
  114. package/src/utils/cache.ts +90 -0
  115. package/src/utils/fileLogger.ts +88 -0
  116. package/src/utils/metrics.ts +135 -0
  117. package/src/utils/responseFormatter.ts +361 -0
  118. package/src/validation/schemas.ts +66 -0
  119. package/src/validation/validators.ts +189 -0
  120. package/tsconfig.json +21 -0
@@ -0,0 +1,288 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Log Viewer - Easy tracking of API requests and responses
4
+ *
5
+ * Usage:
6
+ * node scripts/log-viewer.js # View all logs
7
+ * node scripts/log-viewer.js --type=http # Only HTTP logs
8
+ * node scripts/log-viewer.js --type=mcp # Only MCP logs
9
+ * node scripts/log-viewer.js --errors # Only errors
10
+ * node scripts/log-viewer.js --symbol=BTC-INR # Filter by symbol
11
+ * node scripts/log-viewer.js --last=50 # Last 50 entries
12
+ * node scripts/log-viewer.js --watch # Watch mode (tail -f)
13
+ * node scripts/log-viewer.js --tool=placeMarketOrder # Filter by tool
14
+ */
15
+
16
+ import { readFileSync, existsSync, watchFile } from 'fs';
17
+ import { resolve } from 'path';
18
+
19
+ const args = process.argv.slice(2);
20
+ const options = {
21
+ type: getArg('type'), // http, mcp, http_request, http_response, etc.
22
+ errors: hasFlag('errors'), // Show only errors
23
+ symbol: getArg('symbol'), // Filter by trading symbol
24
+ tool: getArg('tool'), // Filter by tool name
25
+ last: parseInt(getArg('last')) || null, // Show last N entries
26
+ watch: hasFlag('watch'), // Watch mode
27
+ raw: hasFlag('raw'), // Show raw JSON
28
+ correlationId: getArg('correlation'), // Filter by correlation ID
29
+ };
30
+
31
+ const logPath = resolve('logs/mcp-server.log');
32
+
33
+ // ANSI color codes
34
+ const colors = {
35
+ reset: '\x1b[0m',
36
+ bright: '\x1b[1m',
37
+ dim: '\x1b[2m',
38
+ red: '\x1b[31m',
39
+ green: '\x1b[32m',
40
+ yellow: '\x1b[33m',
41
+ blue: '\x1b[34m',
42
+ magenta: '\x1b[35m',
43
+ cyan: '\x1b[36m',
44
+ white: '\x1b[37m',
45
+ gray: '\x1b[90m',
46
+ };
47
+
48
+ function getArg(name) {
49
+ const arg = args.find(a => a.startsWith(`--${name}=`));
50
+ return arg ? arg.split('=')[1] : null;
51
+ }
52
+
53
+ function hasFlag(name) {
54
+ return args.includes(`--${name}`);
55
+ }
56
+
57
+ function colorize(text, color) {
58
+ return `${colors[color] || ''}${text}${colors.reset}`;
59
+ }
60
+
61
+ function formatLog(logEntry) {
62
+ if (options.raw) {
63
+ return JSON.stringify(logEntry, null, 2);
64
+ }
65
+
66
+ const timestamp = new Date(logEntry.timestamp).toLocaleString();
67
+ const level = logEntry.level?.toUpperCase() || 'INFO';
68
+
69
+ // Level colors
70
+ let levelColor = 'white';
71
+ if (level === 'ERROR') levelColor = 'red';
72
+ else if (level === 'WARN') levelColor = 'yellow';
73
+ else if (level === 'INFO') levelColor = 'green';
74
+ else if (level === 'DEBUG') levelColor = 'blue';
75
+
76
+ const levelText = colorize(level.padEnd(5), levelColor);
77
+ const timeText = colorize(timestamp, 'gray');
78
+
79
+ // Format based on log type
80
+ if (logEntry.type === 'http_request') {
81
+ return [
82
+ `${levelText} ${timeText} ${colorize('HTTP REQUEST', 'cyan')}`,
83
+ ` ${colorize(logEntry.method, 'bright')} ${logEntry.url}`,
84
+ ` ${colorize('Headers:', 'dim')} ${JSON.stringify(logEntry.headers)}`,
85
+ logEntry.body ? ` ${colorize('Body:', 'dim')} ${JSON.stringify(logEntry.body)}` : '',
86
+ ].filter(Boolean).join('\n');
87
+ }
88
+
89
+ if (logEntry.type === 'http_response') {
90
+ const statusColor = logEntry.status >= 400 ? 'red' : 'green';
91
+ return [
92
+ `${levelText} ${timeText} ${colorize('HTTP RESPONSE', 'cyan')}`,
93
+ ` ${colorize(logEntry.method, 'bright')} ${logEntry.url}`,
94
+ ` ${colorize('Status:', 'dim')} ${colorize(logEntry.status, statusColor)} (${logEntry.durationMs}ms)`,
95
+ ` ${colorize('Headers:', 'dim')} ${JSON.stringify(logEntry.headers)}`,
96
+ logEntry.body ? ` ${colorize('Body:', 'dim')} ${JSON.stringify(logEntry.body).substring(0, 200)}${JSON.stringify(logEntry.body).length > 200 ? '...' : ''}` : '',
97
+ ].filter(Boolean).join('\n');
98
+ }
99
+
100
+ if (logEntry.type === 'http_error') {
101
+ return [
102
+ `${levelText} ${timeText} ${colorize('HTTP ERROR', 'red')}`,
103
+ ` ${colorize(logEntry.method, 'bright')} ${logEntry.url}`,
104
+ ` ${colorize('Error:', 'red')} ${logEntry.error}`,
105
+ ` ${colorize('Duration:', 'dim')} ${logEntry.durationMs}ms`,
106
+ logEntry.stack ? ` ${colorize('Stack:', 'dim')} ${logEntry.stack.split('\n')[0]}` : '',
107
+ ].filter(Boolean).join('\n');
108
+ }
109
+
110
+ if (logEntry.type === 'mcp_request') {
111
+ return [
112
+ `${levelText} ${timeText} ${colorize('MCP REQUEST', 'magenta')}`,
113
+ ` ${colorize('Tool:', 'bright')} ${logEntry.tool}`,
114
+ ` ${colorize('Correlation:', 'dim')} ${logEntry.correlationId}`,
115
+ ` ${colorize('Params:', 'dim')} ${JSON.stringify(logEntry.params)}`,
116
+ ].filter(Boolean).join('\n');
117
+ }
118
+
119
+ if (logEntry.type === 'mcp_response') {
120
+ return [
121
+ `${levelText} ${timeText} ${colorize('MCP RESPONSE', 'magenta')}`,
122
+ ` ${colorize('Tool:', 'bright')} ${logEntry.tool}`,
123
+ ` ${colorize('Correlation:', 'dim')} ${logEntry.correlationId}`,
124
+ ` ${colorize('Duration:', 'dim')} ${logEntry.durationMs}ms`,
125
+ ` ${colorize('Success:', 'green')} ${logEntry.success}`,
126
+ logEntry.result ? ` ${colorize('Result:', 'dim')} ${JSON.stringify(logEntry.result).substring(0, 200)}${JSON.stringify(logEntry.result).length > 200 ? '...' : ''}` : '',
127
+ ].filter(Boolean).join('\n');
128
+ }
129
+
130
+ if (logEntry.type === 'mcp_error') {
131
+ return [
132
+ `${levelText} ${timeText} ${colorize('MCP ERROR', 'red')}`,
133
+ ` ${colorize('Tool:', 'bright')} ${logEntry.tool}`,
134
+ ` ${colorize('Correlation:', 'dim')} ${logEntry.correlationId}`,
135
+ ` ${colorize('Error:', 'red')} ${logEntry.error}`,
136
+ logEntry.errorCode ? ` ${colorize('Error Code:', 'dim')} ${logEntry.errorCode}` : '',
137
+ ` ${colorize('Duration:', 'dim')} ${logEntry.durationMs}ms`,
138
+ ].filter(Boolean).join('\n');
139
+ }
140
+
141
+ // Default format
142
+ return `${levelText} ${timeText} ${colorize(logEntry.type || 'LOG', 'white')} ${JSON.stringify(logEntry)}`;
143
+ }
144
+
145
+ function matchesFilters(logEntry) {
146
+ // Type filter
147
+ if (options.type) {
148
+ const typeFilter = options.type.toLowerCase();
149
+ const logType = (logEntry.type || '').toLowerCase();
150
+
151
+ if (typeFilter === 'http' && !logType.startsWith('http')) return false;
152
+ if (typeFilter === 'mcp' && !logType.startsWith('mcp')) return false;
153
+ if (typeFilter !== 'http' && typeFilter !== 'mcp' && logType !== typeFilter) return false;
154
+ }
155
+
156
+ // Error filter
157
+ if (options.errors && logEntry.level !== 'error' && !logEntry.type?.includes('error')) {
158
+ return false;
159
+ }
160
+
161
+ // Symbol filter
162
+ if (options.symbol) {
163
+ const logText = JSON.stringify(logEntry).toLowerCase();
164
+ if (!logText.includes(options.symbol.toLowerCase())) return false;
165
+ }
166
+
167
+ // Tool filter
168
+ if (options.tool && logEntry.tool?.toLowerCase() !== options.tool.toLowerCase()) {
169
+ return false;
170
+ }
171
+
172
+ // Correlation ID filter
173
+ if (options.correlationId && logEntry.correlationId !== options.correlationId) {
174
+ return false;
175
+ }
176
+
177
+ return true;
178
+ }
179
+
180
+ function processLogs(content) {
181
+ const lines = content.trim().split('\n').filter(Boolean);
182
+ const entries = [];
183
+
184
+ for (const line of lines) {
185
+ try {
186
+ const logEntry = JSON.parse(line);
187
+ if (matchesFilters(logEntry)) {
188
+ entries.push(logEntry);
189
+ }
190
+ } catch (e) {
191
+ // Skip invalid JSON lines
192
+ }
193
+ }
194
+
195
+ // Apply "last N" filter
196
+ const filteredEntries = options.last ? entries.slice(-options.last) : entries;
197
+
198
+ return filteredEntries;
199
+ }
200
+
201
+ function displayLogs(entries) {
202
+ if (entries.length === 0) {
203
+ console.log(colorize('No logs found matching the filters.', 'yellow'));
204
+ return;
205
+ }
206
+
207
+ console.log(colorize(`\n${'='.repeat(80)}`, 'gray'));
208
+ console.log(colorize(` Found ${entries.length} log entries`, 'bright'));
209
+ console.log(colorize(`${'='.repeat(80)}\n`, 'gray'));
210
+
211
+ for (const entry of entries) {
212
+ console.log(formatLog(entry));
213
+ console.log(''); // Blank line between entries
214
+ }
215
+ }
216
+
217
+ function printHelp() {
218
+ console.log(`
219
+ ${colorize('Zebpay MCP Server Log Viewer', 'bright')}
220
+
221
+ ${colorize('Usage:', 'cyan')}
222
+ node scripts/log-viewer.js [options]
223
+
224
+ ${colorize('Options:', 'cyan')}
225
+ --type=TYPE Filter by log type (http, mcp, http_request, http_response, etc.)
226
+ --errors Show only errors
227
+ --symbol=SYMBOL Filter by trading symbol (e.g., BTC-INR)
228
+ --tool=TOOL Filter by MCP tool name (e.g., placeMarketOrder)
229
+ --correlation=ID Filter by correlation ID
230
+ --last=N Show last N entries
231
+ --watch Watch mode (live tail)
232
+ --raw Show raw JSON output
233
+ --help Show this help message
234
+
235
+ ${colorize('Examples:', 'cyan')}
236
+ node scripts/log-viewer.js --type=http --last=20
237
+ node scripts/log-viewer.js --errors
238
+ node scripts/log-viewer.js --symbol=BTC-INR --watch
239
+ node scripts/log-viewer.js --tool=zebpay_spot_placeMarketOrder
240
+ node scripts/log-viewer.js --correlation=abc123def456
241
+ `);
242
+ }
243
+
244
+ // Main
245
+ if (hasFlag('help')) {
246
+ printHelp();
247
+ process.exit(0);
248
+ }
249
+
250
+ if (!existsSync(logPath)) {
251
+ console.error(colorize(`Error: Log file not found at ${logPath}`, 'red'));
252
+ console.error(colorize('Make sure the server has been started and logs are being written.', 'yellow'));
253
+ process.exit(1);
254
+ }
255
+
256
+ if (options.watch) {
257
+ console.log(colorize(`Watching ${logPath}...`, 'cyan'));
258
+ console.log(colorize('Press Ctrl+C to exit\n', 'gray'));
259
+
260
+ let lastSize = 0;
261
+
262
+ watchFile(logPath, { interval: 500 }, (curr, prev) => {
263
+ if (curr.size > lastSize) {
264
+ const content = readFileSync(logPath, 'utf-8');
265
+ const newContent = content.slice(lastSize);
266
+ lastSize = curr.size;
267
+
268
+ const entries = processLogs(newContent);
269
+ if (entries.length > 0) {
270
+ for (const entry of entries) {
271
+ console.log(formatLog(entry));
272
+ console.log('');
273
+ }
274
+ }
275
+ }
276
+ });
277
+
278
+ // Display existing logs first
279
+ const content = readFileSync(logPath, 'utf-8');
280
+ lastSize = content.length;
281
+ const entries = processLogs(content);
282
+ displayLogs(entries);
283
+ } else {
284
+ const content = readFileSync(logPath, 'utf-8');
285
+ const entries = processLogs(content);
286
+ displayLogs(entries);
287
+ }
288
+
package/server.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.zebpay/zebpay-mcp-server",
4
+ "title": "ZebPay MCP Server",
5
+ "description": "MCP server for ZebPay crypto exchange — fetch prices, balances, and manage orders",
6
+ "repository": {
7
+ "url": "https://github.com/zebpay/zebpay-mcp-server",
8
+ "source": "github",
9
+ "id": "YOUR_GITHUB_REPO_ID"
10
+ },
11
+ "version": "1.0.0",
12
+ "packages": [
13
+ {
14
+ "registryType": "npm",
15
+ "identifier": "@zebpay_rajesh/zebpay-mcp-server",
16
+ "version": "1.0.0",
17
+ "transport": {
18
+ "type": "stdio"
19
+ },
20
+ "environmentVariables": [
21
+ {
22
+ "name": "ZEBPAY_API_KEY",
23
+ "description": "Your ZebPay API key from the developer portal",
24
+ "isRequired": true,
25
+ "format": "string",
26
+ "isSecret": true
27
+ }
28
+ ]
29
+ }
30
+ ]
31
+ }
@@ -0,0 +1,180 @@
1
+ /*
2
+ Error handling tests for MCP error utilities.
3
+ */
4
+
5
+ import { createInvalidParamsError, createInternalError, convertHttpErrorToMcpError } from "../mcp/errors.js";
6
+ import { ErrorCode } from "@modelcontextprotocol/sdk/types.js";
7
+
8
+ // Basic test suite (requires jest to be installed)
9
+ describe("MCP Error Handling", () => {
10
+ describe("createInvalidParamsError", () => {
11
+ it("should create an InvalidParams error with message", () => {
12
+ const error = createInvalidParamsError("Invalid symbol format");
13
+
14
+ if (!(error instanceof Error)) {
15
+ throw new Error("Expected error to be instance of Error");
16
+ }
17
+ if (error.code !== ErrorCode.InvalidParams) {
18
+ throw new Error(`Expected error code ${ErrorCode.InvalidParams}, got ${error.code}`);
19
+ }
20
+ // MCP error messages include code prefix: "MCP error -32602: <message>"
21
+ if (!error.message.includes("Invalid symbol format")) {
22
+ throw new Error(`Expected error message to contain "Invalid symbol format", got "${error.message}"`);
23
+ }
24
+ });
25
+
26
+ it("should include data when provided", () => {
27
+ const data = { symbol: "BTC-INR" };
28
+ const error = createInvalidParamsError("Invalid symbol", data);
29
+
30
+ if (JSON.stringify(error.data) !== JSON.stringify(data)) {
31
+ throw new Error(`Expected error data to match ${JSON.stringify(data)}`);
32
+ }
33
+ });
34
+ });
35
+
36
+ describe("createInternalError", () => {
37
+ it("should create an InternalError with message", () => {
38
+ const error = createInternalError("Server error occurred");
39
+
40
+ if (!(error instanceof Error)) {
41
+ throw new Error("Expected error to be instance of Error");
42
+ }
43
+ if (error.code !== ErrorCode.InternalError) {
44
+ throw new Error(`Expected error code ${ErrorCode.InternalError}, got ${error.code}`);
45
+ }
46
+ // MCP error messages include code prefix: "MCP error -32603: <message>"
47
+ if (!error.message.includes("Server error occurred")) {
48
+ throw new Error(`Expected error message to contain "Server error occurred", got "${error.message}"`);
49
+ }
50
+ });
51
+
52
+ it("should include data when provided", () => {
53
+ const data = { status: 500 };
54
+ const error = createInternalError("Server error", data);
55
+
56
+ if (JSON.stringify(error.data) !== JSON.stringify(data)) {
57
+ throw new Error(`Expected error data to match ${JSON.stringify(data)}`);
58
+ }
59
+ });
60
+ });
61
+
62
+ describe("convertHttpErrorToMcpError", () => {
63
+ it("should convert 400 to InvalidParams", () => {
64
+ const error = convertHttpErrorToMcpError(400, "Bad request", {});
65
+
66
+ if (error.code !== ErrorCode.InvalidParams) {
67
+ throw new Error(`Expected InvalidParams, got ${error.code}`);
68
+ }
69
+ // The message should contain the original message or a transformed version
70
+ if (!error.message.toLowerCase().includes("bad request") && !error.message.toLowerCase().includes("invalid")) {
71
+ throw new Error(`Expected message to contain "bad request" or "invalid", got "${error.message}"`);
72
+ }
73
+ });
74
+
75
+ it("should convert 401 to InvalidParams with auth message", () => {
76
+ const error = convertHttpErrorToMcpError(401, "Unauthorized", {});
77
+
78
+ if (error.code !== ErrorCode.InvalidParams) {
79
+ throw new Error(`Expected InvalidParams, got ${error.code}`);
80
+ }
81
+ if (!error.message.toLowerCase().includes("credential")) {
82
+ throw new Error(`Expected message to contain "credential", got "${error.message}"`);
83
+ }
84
+ });
85
+
86
+ it("should convert 403 to InvalidParams with auth message", () => {
87
+ const error = convertHttpErrorToMcpError(403, "Forbidden", {});
88
+
89
+ if (error.code !== ErrorCode.InvalidParams) {
90
+ throw new Error(`Expected InvalidParams, got ${error.code}`);
91
+ }
92
+ if (!error.message.toLowerCase().includes("credential")) {
93
+ throw new Error(`Expected message to contain "credential", got "${error.message}"`);
94
+ }
95
+ });
96
+
97
+ it("should convert 404 to InvalidParams", () => {
98
+ const error = convertHttpErrorToMcpError(404, "Not found", {});
99
+
100
+ if (error.code !== ErrorCode.InvalidParams) {
101
+ throw new Error(`Expected InvalidParams, got ${error.code}`);
102
+ }
103
+ if (!error.message.toLowerCase().includes("not found")) {
104
+ throw new Error(`Expected message to contain "not found", got "${error.message}"`);
105
+ }
106
+ });
107
+
108
+ it("should convert 429 to InternalError with retryable flag", () => {
109
+ const error = convertHttpErrorToMcpError(429, "Rate limit", {});
110
+
111
+ if (error.code !== ErrorCode.InternalError) {
112
+ throw new Error(`Expected InternalError, got ${error.code}`);
113
+ }
114
+ if (!error.message.toLowerCase().includes("rate limit")) {
115
+ throw new Error(`Expected message to contain "rate limit", got "${error.message}"`);
116
+ }
117
+ const data = error.data as { retryable?: boolean };
118
+ if (!data?.retryable) {
119
+ throw new Error("Expected retryable flag to be true");
120
+ }
121
+ });
122
+
123
+ it("should convert 500 to InternalError with retryable flag", () => {
124
+ const error = convertHttpErrorToMcpError(500, "Server error", {});
125
+
126
+ if (error.code !== ErrorCode.InternalError) {
127
+ throw new Error(`Expected InternalError, got ${error.code}`);
128
+ }
129
+ if (!error.message.toLowerCase().includes("server error")) {
130
+ throw new Error(`Expected message to contain "server error", got "${error.message}"`);
131
+ }
132
+ const data = error.data as { retryable?: boolean };
133
+ if (!data?.retryable) {
134
+ throw new Error("Expected retryable flag to be true");
135
+ }
136
+ });
137
+
138
+ it("should convert network error (status 0) to InternalError", () => {
139
+ const error = convertHttpErrorToMcpError(0, "Network error", {});
140
+
141
+ if (error.code !== ErrorCode.InternalError) {
142
+ throw new Error(`Expected InternalError, got ${error.code}`);
143
+ }
144
+ if (!error.message.toLowerCase().includes("network")) {
145
+ throw new Error(`Expected message to contain "network", got "${error.message}"`);
146
+ }
147
+ const data = error.data as { retryable?: boolean };
148
+ if (!data?.retryable) {
149
+ throw new Error("Expected retryable flag to be true");
150
+ }
151
+ });
152
+
153
+ it("should handle generic HTTP error messages", () => {
154
+ const error = convertHttpErrorToMcpError(400, "HTTP 400", {});
155
+
156
+ if (error.code !== ErrorCode.InvalidParams) {
157
+ throw new Error(`Expected InvalidParams, got ${error.code}`);
158
+ }
159
+ if (error.message === "HTTP 400") {
160
+ throw new Error("Expected message to be transformed, not generic HTTP 400");
161
+ }
162
+ });
163
+
164
+ it("should preserve detailed error messages", () => {
165
+ const detailedMessage = "Symbol BTC-INR not found in exchange";
166
+ const error = convertHttpErrorToMcpError(404, detailedMessage, {});
167
+
168
+ if (!error.message.includes(detailedMessage)) {
169
+ throw new Error(`Expected message to contain "${detailedMessage}", got "${error.message}"`);
170
+ }
171
+ });
172
+ });
173
+ });
174
+
175
+ // Helper functions for tests (if jest is not available, these provide basic functionality)
176
+ declare global {
177
+ function describe(name: string, fn: () => void): void;
178
+ function it(name: string, fn: () => void): void;
179
+ }
180
+
@@ -0,0 +1,89 @@
1
+ /*
2
+ Tests for MCP prompts registration and functionality.
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, afterEach, jest } from "@jest/globals";
6
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
+ import { PublicClient } from "../public/PublicClient.js";
8
+ import { SpotClient } from "../private/SpotClient.js";
9
+ import { AppConfig } from "../config.js";
10
+ import { registerPrompts } from "../mcp/prompts.js";
11
+
12
+ describe("MCP Prompts", () => {
13
+ let mockServer: McpServer;
14
+ let mockPublicClient: PublicClient;
15
+ let mockSpotClient: SpotClient | null;
16
+ let mockConfig: AppConfig;
17
+ let originalConsoleError: typeof console.error;
18
+
19
+ beforeEach(() => {
20
+ // Suppress console.error during tests
21
+ originalConsoleError = console.error;
22
+ console.error = jest.fn();
23
+
24
+ // Create mock server
25
+ mockServer = new McpServer({
26
+ name: "test-server",
27
+ version: "1.0.0",
28
+ });
29
+
30
+ // Create mock public client
31
+ mockPublicClient = {} as unknown as PublicClient;
32
+
33
+ // Create mock spot client
34
+ mockSpotClient = {} as unknown as SpotClient;
35
+
36
+ // Create mock config
37
+ mockConfig = {
38
+ spotBaseUrl: "https://test.api",
39
+ futuresBaseUrl: "https://test.futures.api",
40
+ marketBaseUrl: "https://test.market.api",
41
+ transports: ["stdio"],
42
+ logLevel: "info",
43
+ signingHeaders: {
44
+ apiKeyHeader: "X-AUTH-APIKEY",
45
+ signatureHeader: "X-AUTH-SIGNATURE",
46
+ timestampHeader: "",
47
+ },
48
+ timeoutMs: 15000,
49
+ retryCount: 2,
50
+ };
51
+ });
52
+
53
+ afterEach(() => {
54
+ // Restore console.error after tests
55
+ console.error = originalConsoleError;
56
+ });
57
+
58
+ it("should register check-balance-before-trade prompt when spot client is available", () => {
59
+ expect(() => {
60
+ registerPrompts(mockServer, mockSpotClient, mockPublicClient, mockConfig);
61
+ }).not.toThrow();
62
+ });
63
+
64
+ it("should register analyze-market prompt", () => {
65
+ expect(() => {
66
+ registerPrompts(mockServer, null, mockPublicClient, mockConfig);
67
+ }).not.toThrow();
68
+ });
69
+
70
+ it("should register compare-trading-pairs prompt", () => {
71
+ expect(() => {
72
+ registerPrompts(mockServer, null, mockPublicClient, mockConfig);
73
+ }).not.toThrow();
74
+ });
75
+
76
+ it("should register get-exchange-info prompt", () => {
77
+ expect(() => {
78
+ registerPrompts(mockServer, null, mockPublicClient, mockConfig);
79
+ }).not.toThrow();
80
+ });
81
+
82
+ it("should not register authenticated prompts when spot client is null", () => {
83
+ // Public prompts should still register successfully without spot client
84
+ expect(() => {
85
+ registerPrompts(mockServer, null, mockPublicClient, mockConfig);
86
+ }).not.toThrow();
87
+ });
88
+ });
89
+
@@ -0,0 +1,95 @@
1
+ /*
2
+ Tests for MCP resources registration and functionality.
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, afterEach, jest } from "@jest/globals";
6
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
+ import { PublicClient } from "../public/PublicClient.js";
8
+ import { SpotClient } from "../private/SpotClient.js";
9
+ import { AppConfig } from "../config.js";
10
+ import { registerResources } from "../mcp/resources.js";
11
+
12
+ describe("MCP Resources", () => {
13
+ let mockServer: McpServer;
14
+ let mockPublicClient: PublicClient;
15
+ let mockSpotClient: SpotClient | null;
16
+ let mockConfig: AppConfig;
17
+ let originalConsoleError: typeof console.error;
18
+
19
+ beforeEach(() => {
20
+ // Suppress console.error during tests
21
+ originalConsoleError = console.error;
22
+ console.error = jest.fn();
23
+
24
+ // Create mock server
25
+ mockServer = new McpServer({
26
+ name: "test-server",
27
+ version: "1.0.0",
28
+ });
29
+
30
+ // Create mock public client
31
+ mockPublicClient = {
32
+ getExchangeInfo: jest.fn<() => Promise<unknown>>().mockResolvedValue({ symbols: [] }),
33
+ getCurrencies: jest.fn<() => Promise<unknown>>().mockResolvedValue({ currencies: [] }),
34
+ getAllTickers: jest.fn<() => Promise<unknown>>().mockResolvedValue({ tickers: [] }),
35
+ } as unknown as PublicClient;
36
+
37
+ // Create mock spot client
38
+ mockSpotClient = {
39
+ getBalance: jest.fn<(currencies?: string) => Promise<unknown>>().mockResolvedValue({ balances: [] }),
40
+ } as unknown as SpotClient;
41
+
42
+ // Create mock config
43
+ mockConfig = {
44
+ spotBaseUrl: "https://test.api",
45
+ futuresBaseUrl: "https://test.futures.api",
46
+ marketBaseUrl: "https://test.market.api",
47
+ transports: ["stdio"],
48
+ logLevel: "info",
49
+ signingHeaders: {
50
+ apiKeyHeader: "X-AUTH-APIKEY",
51
+ signatureHeader: "X-AUTH-SIGNATURE",
52
+ timestampHeader: "",
53
+ },
54
+ timeoutMs: 15000,
55
+ retryCount: 2,
56
+ };
57
+ });
58
+
59
+ afterEach(() => {
60
+ // Restore console.error after tests
61
+ console.error = originalConsoleError;
62
+ });
63
+
64
+ it("should register exchange-info resource", () => {
65
+ expect(() => {
66
+ registerResources(mockServer, mockPublicClient, null, mockConfig);
67
+ }).not.toThrow();
68
+ });
69
+
70
+ it("should register currencies resource", () => {
71
+ expect(() => {
72
+ registerResources(mockServer, mockPublicClient, null, mockConfig);
73
+ }).not.toThrow();
74
+ });
75
+
76
+ it("should register all-tickers resource", () => {
77
+ expect(() => {
78
+ registerResources(mockServer, mockPublicClient, null, mockConfig);
79
+ }).not.toThrow();
80
+ });
81
+
82
+ it("should register balance resource when spot client is available", () => {
83
+ expect(() => {
84
+ registerResources(mockServer, mockPublicClient, mockSpotClient, mockConfig);
85
+ }).not.toThrow();
86
+ });
87
+
88
+ it("should not register balance resource when spot client is null", () => {
89
+ // Resources should still register successfully without spot client
90
+ expect(() => {
91
+ registerResources(mockServer, mockPublicClient, null, mockConfig);
92
+ }).not.toThrow();
93
+ });
94
+ });
95
+