@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,88 @@
1
+ /*
2
+ Basic test setup for ZebPay MCP Server
3
+ Note: Install jest dependencies first: npm install --save-dev jest @types/jest ts-jest
4
+ Then run: npm test
5
+ */
6
+
7
+ import { validateSymbol, validateQuantity } from "../validation/validators.js";
8
+ import { createInvalidParamsError } from "../mcp/errors.js";
9
+
10
+ // Basic test suite (requires jest to be installed)
11
+ describe("Validation", () => {
12
+ describe("validateSymbol", () => {
13
+ it("should accept valid symbols", () => {
14
+ expect(() => validateSymbol("BTC-INR")).not.toThrow();
15
+ expect(() => validateSymbol("ETH-INR")).not.toThrow();
16
+ expect(() => validateSymbol("BTC-USDT")).not.toThrow();
17
+ });
18
+
19
+ it("should reject invalid symbols", () => {
20
+ expect(() => validateSymbol("BTCINR")).toThrow();
21
+ expect(() => validateSymbol("BTC/INR")).toThrow();
22
+ // Note: "btc-inr" is converted to uppercase and becomes valid "BTC-INR"
23
+ // So we test with a symbol that's invalid even after conversion
24
+ expect(() => validateSymbol("")).toThrow();
25
+ expect(() => validateSymbol("BTC-INR-EXTRA")).toThrow(); // Too many parts
26
+ });
27
+
28
+ it("should accept lowercase symbols (converts to uppercase)", () => {
29
+ // The function converts to uppercase, so lowercase should work
30
+ expect(() => validateSymbol("btc-inr")).not.toThrow();
31
+ expect(() => validateSymbol("eth-inr")).not.toThrow();
32
+ });
33
+ });
34
+
35
+ describe("validateQuantity", () => {
36
+ it("should accept valid quantities", () => {
37
+ expect(() => validateQuantity("0.001")).not.toThrow();
38
+ expect(() => validateQuantity("100")).not.toThrow();
39
+ expect(() => validateQuantity("0.5")).not.toThrow();
40
+ });
41
+
42
+ it("should reject invalid quantities", () => {
43
+ expect(() => validateQuantity("")).toThrow();
44
+ expect(() => validateQuantity("0")).toThrow();
45
+ expect(() => validateQuantity("-1")).toThrow();
46
+ expect(() => validateQuantity("abc")).toThrow();
47
+ });
48
+ });
49
+ });
50
+
51
+ describe("Error Handling", () => {
52
+ it("should create invalid params error with correct code", () => {
53
+ const error = createInvalidParamsError("Test error");
54
+ // Note: These assertions require jest to be installed
55
+ // expect(error).toBeInstanceOf(Error);
56
+ // expect(error.code).toBe(-32602);
57
+ // expect(error.message).toBe("Test error");
58
+
59
+ // Basic validation without jest
60
+ if (!(error instanceof Error)) {
61
+ throw new Error("Expected error to be instance of Error");
62
+ }
63
+ if (error.code !== -32602) {
64
+ throw new Error(`Expected error code -32602, got ${error.code}`);
65
+ }
66
+ // MCP error messages include code prefix: "MCP error -32602: <message>"
67
+ if (!error.message.includes("Test error")) {
68
+ throw new Error(`Expected error message to contain "Test error", got "${error.message}"`);
69
+ }
70
+ });
71
+ });
72
+
73
+ // Helper functions for tests (if jest is not available, these provide basic functionality)
74
+ declare global {
75
+ function describe(name: string, fn: () => void): void;
76
+ function it(name: string, fn: () => void): void;
77
+ function expect(fn: () => void): {
78
+ not: { toThrow: () => void };
79
+ toThrow: () => void;
80
+ };
81
+ function expect<T>(value: T): {
82
+ toBeInstanceOf: (constructor: new (...args: any[]) => T) => void;
83
+ toBe: (expected: T) => void;
84
+ };
85
+ }
86
+
87
+
88
+
package/src/config.ts ADDED
@@ -0,0 +1,108 @@
1
+ /*
2
+ Centralized configuration loading and validation.
3
+ Secrets are never logged; use redact() when including values in logs.
4
+ */
5
+
6
+ export type TransportKind = "stdio";
7
+
8
+ export interface SigningHeaderNames {
9
+ apiKeyHeader: string;
10
+ signatureHeader: string;
11
+ timestampHeader: string;
12
+ }
13
+
14
+ const SIGNING_HEADERS: SigningHeaderNames = {
15
+ apiKeyHeader: "X-AUTH-APIKEY",
16
+ signatureHeader: "X-AUTH-SIGNATURE",
17
+ timestampHeader: "",
18
+ };
19
+
20
+ export interface AppConfig {
21
+ spotBaseUrl: string;
22
+ futuresBaseUrl: string;
23
+ marketBaseUrl: string;
24
+ transports: TransportKind[];
25
+ logLevel: "debug" | "info" | "warn" | "error";
26
+ logFile?: string; // Optional log file path
27
+ signingHeaders: SigningHeaderNames;
28
+ timeoutMs: number;
29
+ retryCount: number;
30
+ }
31
+
32
+ export function redact(value: string | undefined | null, show: number = 4): string {
33
+ if (!value) return "<redacted>";
34
+ if (value.length <= show) return "*".repeat(value.length);
35
+ return `${value.slice(0, show)}${"*".repeat(Math.max(4, value.length - show))}`;
36
+ }
37
+
38
+ function getRequiredEnv(name: string): string {
39
+ const value = process.env[name];
40
+ if (value === undefined) {
41
+ throw new Error(`Missing required environment variable: ${name}`);
42
+ }
43
+ return value;
44
+ }
45
+
46
+ function parseTransports(input: string | undefined): TransportKind[] {
47
+ const raw = (input ?? "stdio").split(",").map((s) => s.trim()).filter(Boolean);
48
+ const valid: TransportKind[] = [];
49
+ for (const t of raw) {
50
+ if (t === "stdio") valid.push(t);
51
+ }
52
+
53
+ if (!valid.length) {
54
+ throw new Error('Invalid MCP_TRANSPORTS. Only "stdio" is supported.');
55
+ }
56
+ return valid;
57
+ }
58
+
59
+ function parseLogLevel(input: string | undefined): AppConfig["logLevel"] {
60
+ if (input === undefined) return "info";
61
+ if (input === "debug" || input === "info" || input === "warn" || input === "error") {
62
+ return input;
63
+ }
64
+ throw new Error('Invalid LOG_LEVEL. Use one of: "debug", "info", "warn", "error".');
65
+ }
66
+
67
+ export function getConfig(): AppConfig {
68
+ const spotBaseUrl = getRequiredEnv("ZEBPAY_SPOT_BASE_URL") || 'https://www.zebapi.com/api/v2';
69
+ const futuresBaseUrl = getRequiredEnv("ZEBPAY_FUTURES_BASE_URL") || 'https://futures-api.zebpay.com/api/v1';
70
+ const marketBaseUrl = getRequiredEnv("ZEBPAY_MARKET_BASE_URL") || 'https://www.zebapi.com/api/v1/market';
71
+ const transports = parseTransports(process.env.MCP_TRANSPORTS);
72
+ const logLevel = parseLogLevel(process.env.LOG_LEVEL);
73
+ const logFile = process.env.LOG_FILE; // Optional log file path
74
+ const timeoutMs = Number(process.env.HTTP_TIMEOUT_MS ?? 15000);
75
+ const retryCount = Number(process.env.HTTP_RETRY_COUNT ?? 2);
76
+
77
+ const signingHeaders: SigningHeaderNames = SIGNING_HEADERS;
78
+
79
+ if (!spotBaseUrl.startsWith("http")) {
80
+ throw new Error("Invalid spot base URL");
81
+ }
82
+ if (!futuresBaseUrl.startsWith("http")) {
83
+ throw new Error("Invalid futures base URL");
84
+ }
85
+ if (!marketBaseUrl.startsWith("http")) {
86
+ throw new Error("Invalid market base URL");
87
+ }
88
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
89
+ throw new Error("Invalid HTTP_TIMEOUT_MS");
90
+ }
91
+ if (!Number.isFinite(retryCount) || retryCount < 0) {
92
+ throw new Error("Invalid HTTP_RETRY_COUNT");
93
+ }
94
+
95
+ return {
96
+ spotBaseUrl,
97
+ futuresBaseUrl,
98
+ marketBaseUrl,
99
+ transports,
100
+ logLevel,
101
+ logFile,
102
+ signingHeaders,
103
+ timeoutMs,
104
+ retryCount,
105
+ };
106
+ }
107
+
108
+
@@ -0,0 +1,398 @@
1
+ /*
2
+ Minimal HTTP client with retries, timeout, structured errors, and redacted logs.
3
+ */
4
+
5
+ import { setTimeout as delay } from "node:timers/promises";
6
+ import { redact } from "../config.js";
7
+ import { fetch } from "undici";
8
+ import { fileLogger } from "../utils/fileLogger.js";
9
+
10
+ export interface HttpRequestOptions {
11
+ method: string;
12
+ url: string;
13
+ headers?: Record<string, string>;
14
+ body?: unknown;
15
+ timeoutMs: number;
16
+ retryCount: number;
17
+ }
18
+
19
+ export interface HttpResponse<T = unknown> {
20
+ status: number;
21
+ headers: Record<string, string>;
22
+ data: T;
23
+ }
24
+
25
+ export class HttpError extends Error {
26
+ public readonly status: number;
27
+ public readonly details?: unknown;
28
+ public readonly isHtmlResponse?: boolean;
29
+ constructor(message: string, status: number, details?: unknown, isHtmlResponse?: boolean) {
30
+ super(message);
31
+ this.status = status;
32
+ this.details = details;
33
+ this.isHtmlResponse = isHtmlResponse;
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Parses HTML error pages (like Cloudflare) to extract user-friendly error messages.
39
+ *
40
+ * NOTE: This is transport-agnostic - it parses responses from Zebpay API (external),
41
+ * not from MCP clients. Works identically for both stdio and HTTP transports.
42
+ */
43
+ function parseHtmlError(html: string): string | null {
44
+ if (!html || typeof html !== "string") {
45
+ return null;
46
+ }
47
+
48
+ // Check if it's HTML
49
+ if (!html.trim().toLowerCase().startsWith("<!doctype") && !html.trim().toLowerCase().startsWith("<html")) {
50
+ return null;
51
+ }
52
+
53
+ // Extract Cloudflare error messages
54
+ const cloudflarePatterns = [
55
+ // Cloudflare Error 1006 - Access denied
56
+ /<h2[^>]*>Access denied<\/h2>/i,
57
+ /The owner of this website.*?has banned your IP address[^<]*/i,
58
+ /<p[^>]*>The owner of this website[^<]*<\/p>/i,
59
+ // Cloudflare Error 1020 - Access denied
60
+ /<h1[^>]*>Error[^<]*<\/h1>/i,
61
+ // Generic error extraction from title
62
+ /<title>([^<]+)<\/title>/i,
63
+ // Extract error messages from h1/h2 tags
64
+ /<h[12][^>]*>([^<]+)<\/h[12]>/i,
65
+ ];
66
+
67
+ for (const pattern of cloudflarePatterns) {
68
+ const match = html.match(pattern);
69
+ if (match) {
70
+ let message = match[1] || match[0];
71
+ // Clean up HTML entities and tags
72
+ message = message
73
+ .replace(/<[^>]+>/g, "")
74
+ .replace(/&nbsp;/g, " ")
75
+ .replace(/&amp;/g, "&")
76
+ .replace(/&lt;/g, "<")
77
+ .replace(/&gt;/g, ">")
78
+ .replace(/&quot;/g, '"')
79
+ .replace(/\s+/g, " ")
80
+ .trim();
81
+
82
+ if (message && message.length > 10) {
83
+ // Check for IP ban message
84
+ if (html.includes("banned your IP address")) {
85
+ const ipMatch = html.match(/IP address[^<]*\(([^)]+)\)/i);
86
+ if (ipMatch) {
87
+ return `Access denied: Your IP address has been banned by the website. Please contact support or try again later.`;
88
+ }
89
+ return `Access denied: Your IP address has been restricted. Please contact support or try again later.`;
90
+ }
91
+
92
+ // Check for Cloudflare error codes
93
+ const errorCodeMatch = html.match(/Error\s*(\d+)/i);
94
+ if (errorCodeMatch) {
95
+ const errorCode = errorCodeMatch[1];
96
+ if (errorCode === "1006") {
97
+ return `Access denied (Error 1006): Your IP address has been banned. Please contact support or try again later.`;
98
+ }
99
+ return `Cloudflare error (${errorCode}): ${message}`;
100
+ }
101
+
102
+ return message;
103
+ }
104
+ }
105
+ }
106
+
107
+ // Fallback: try to extract any meaningful text
108
+ const textMatch = html.match(/<body[^>]*>([\s\S]{100,500})<\/body>/i);
109
+ if (textMatch) {
110
+ let text = textMatch[1]
111
+ .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "")
112
+ .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "")
113
+ .replace(/<[^>]+>/g, " ")
114
+ .replace(/\s+/g, " ")
115
+ .trim();
116
+
117
+ if (text.length > 20 && text.length < 200) {
118
+ return text;
119
+ }
120
+ }
121
+
122
+ return null;
123
+ }
124
+
125
+ export class HttpClient {
126
+ constructor(private readonly logLevel: "debug" | "info" | "warn" | "error" = "info") {}
127
+
128
+ async request<T = unknown>(opts: HttpRequestOptions): Promise<HttpResponse<T>> {
129
+ const { method, url, headers = {}, body, timeoutMs, retryCount } = opts;
130
+ const isIdempotent = method.toUpperCase() === "GET";
131
+ const maxAttempts = isIdempotent ? retryCount + 1 : 1;
132
+
133
+ const payload = body === undefined || body === null ? undefined : (typeof body === "string" ? body : JSON.stringify(body));
134
+
135
+ // Prepare headers for request (with content-type)
136
+ const requestHeaders: Record<string, string> = {
137
+ ...headers,
138
+ };
139
+ if (payload) {
140
+ requestHeaders["content-type"] = "application/json";
141
+ }
142
+
143
+ // Log HTTP request
144
+ const requestTimestamp = new Date().toISOString();
145
+ const requestLogMessage = JSON.stringify({
146
+ level: "info",
147
+ type: "http_request",
148
+ timestamp: requestTimestamp,
149
+ method: method.toUpperCase(),
150
+ url,
151
+ headers: this.sanitizeHeaders(requestHeaders),
152
+ body: this.sanitizeBody(payload),
153
+ });
154
+ fileLogger.log(requestLogMessage);
155
+
156
+ let attempt = 0;
157
+ let lastError: unknown;
158
+ while (attempt < maxAttempts) {
159
+ attempt++;
160
+ const controller = new AbortController();
161
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
162
+ const attemptStartTime = Date.now();
163
+ try {
164
+ const res = await fetch(url, {
165
+ method,
166
+ headers: requestHeaders,
167
+ body: payload,
168
+ signal: controller.signal,
169
+ } as any);
170
+ clearTimeout(timer);
171
+
172
+ const text = await res.text();
173
+ const contentType = res.headers.get("content-type") || "";
174
+ const isHtml = contentType.includes("text/html") || text.trim().toLowerCase().startsWith("<!doctype") || text.trim().toLowerCase().startsWith("<html");
175
+
176
+ let data: any = undefined;
177
+ let parsedError: string | null = null;
178
+
179
+ if (isHtml && !res.ok) {
180
+ // Try to parse HTML error page
181
+ parsedError = parseHtmlError(text);
182
+ data = text; // Keep original HTML for logging
183
+ } else {
184
+ // Try to parse as JSON
185
+ try {
186
+ data = text ? JSON.parse(text) : undefined;
187
+ } catch {
188
+ data = text;
189
+ }
190
+ }
191
+
192
+ const response: HttpResponse<T> = {
193
+ status: res.status,
194
+ headers: Object.fromEntries(res.headers.entries()),
195
+ data,
196
+ };
197
+
198
+ const durationMs = Date.now() - attemptStartTime;
199
+
200
+ // Log HTTP response with user-readable format
201
+ const responseTimestamp = new Date().toISOString();
202
+ const responseBody = this.formatResponseBody(data, res.status);
203
+
204
+ const responseLogMessage = JSON.stringify({
205
+ level: res.ok ? "info" : "warn",
206
+ type: "http_response",
207
+ timestamp: responseTimestamp,
208
+ method: method.toUpperCase(),
209
+ url,
210
+ status: res.status,
211
+ headers: this.sanitizeHeaders(response.headers),
212
+ body: responseBody,
213
+ durationMs,
214
+ attempt,
215
+ isHtmlResponse: isHtml,
216
+ });
217
+ fileLogger.log(responseLogMessage);
218
+
219
+ if (res.ok) {
220
+ return response;
221
+ }
222
+
223
+ // Retry on 429/5xx when idempotent
224
+ if (isIdempotent && (res.status === 429 || (res.status >= 500 && res.status < 600)) && attempt < maxAttempts) {
225
+ const retryAfter = Number(response.headers["retry-after"] || 0);
226
+ const backoffMs = retryAfter > 0 ? retryAfter * 1000 : 250 * attempt;
227
+ await delay(backoffMs);
228
+ continue;
229
+ }
230
+
231
+ // Use parsed error message if available, otherwise use default
232
+ const errorMessage = parsedError || `HTTP ${res.status}`;
233
+ throw new HttpError(errorMessage, res.status, data, isHtml);
234
+ } catch (err) {
235
+ clearTimeout(timer);
236
+ lastError = err;
237
+ const durationMs = Date.now() - attemptStartTime;
238
+
239
+ // AbortError or network error
240
+ if (attempt >= maxAttempts) {
241
+ // Log HTTP error
242
+ const errorTimestamp = new Date().toISOString();
243
+ const errorMessage = err instanceof Error ? err.message : String(err);
244
+ const errorStack = err instanceof Error ? err.stack : undefined;
245
+
246
+ const errorLogMessage = JSON.stringify({
247
+ level: "error",
248
+ type: "http_error",
249
+ timestamp: errorTimestamp,
250
+ method: method.toUpperCase(),
251
+ url,
252
+ headers: this.sanitizeHeaders(requestHeaders),
253
+ body: this.sanitizeBody(payload),
254
+ durationMs,
255
+ attempt,
256
+ error: errorMessage,
257
+ stack: this.sanitizeStack(errorStack),
258
+ });
259
+ fileLogger.log(errorLogMessage);
260
+
261
+ if (err instanceof HttpError) throw err;
262
+ throw new HttpError((err as Error).message || "Network error", 0);
263
+ }
264
+ await delay(200 * attempt);
265
+ }
266
+ }
267
+
268
+ throw lastError instanceof Error ? lastError : new Error("Unknown HTTP error");
269
+ }
270
+
271
+ /**
272
+ * Sanitizes stack traces by removing file paths, keeping only function names and line numbers
273
+ */
274
+ private sanitizeStack(stack: string | undefined): string | undefined {
275
+ if (!stack) return undefined;
276
+
277
+ try {
278
+ // Remove file:// URLs and absolute paths, keep only filename and line numbers
279
+ const lines = stack.split('\n');
280
+ const sanitizedLines = lines.map(line => {
281
+ let sanitized = line;
282
+
283
+ // Remove file:// URLs (e.g., file:///Users/path/to/file.js:123:45)
284
+ sanitized = sanitized.replace(/file:\/\/\/[^\s\)]+/g, (match) => {
285
+ // Extract just the filename and line numbers
286
+ const filenameMatch = match.match(/([^\/]+\.(js|ts|mjs|cjs)):(\d+):(\d+)/);
287
+ if (filenameMatch) {
288
+ return `${filenameMatch[1]}:${filenameMatch[3]}:${filenameMatch[4]}`;
289
+ }
290
+ return '';
291
+ });
292
+
293
+ // Remove absolute paths (e.g., /Users/path/to/file.js:123:45)
294
+ sanitized = sanitized.replace(/\/(?:[^\/\s]+\/)+([^\/\s]+\.(js|ts|mjs|cjs)):(\d+):(\d+)/g, '$1:$3:$4');
295
+
296
+ // Simplify node_modules paths (e.g., node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js)
297
+ sanitized = sanitized.replace(/node_modules\/([^\/\s]+)\/[^\s\)]+/g, 'node_modules/$1');
298
+
299
+ return sanitized.trim();
300
+ });
301
+
302
+ return sanitizedLines.join('\n');
303
+ } catch {
304
+ // If sanitization fails, return a minimal version without paths
305
+ return stack.split('\n').slice(0, 3).join('\n'); // Keep only first 3 lines
306
+ }
307
+ }
308
+
309
+ /**
310
+ * Formats response body to be more user-readable
311
+ */
312
+ private formatResponseBody(body: unknown, status: number): unknown {
313
+ if (body === undefined || body === null) {
314
+ return body;
315
+ }
316
+
317
+ // For error responses, extract meaningful information
318
+ if (status >= 400 && typeof body === "object" && body !== null) {
319
+ const bodyObj = body as Record<string, unknown>;
320
+ const formatted: Record<string, unknown> = {};
321
+
322
+ // Extract common error fields
323
+ if (typeof bodyObj.statusCode === "number") {
324
+ formatted.statusCode = bodyObj.statusCode;
325
+ }
326
+ if (typeof bodyObj.statusDescription === "string") {
327
+ formatted.statusDescription = bodyObj.statusDescription;
328
+ }
329
+ if (typeof bodyObj.error === "string") {
330
+ formatted.error = bodyObj.error;
331
+ }
332
+ if (typeof bodyObj.message === "string") {
333
+ formatted.message = bodyObj.message;
334
+ }
335
+
336
+ // Include data if present
337
+ if (bodyObj.data !== undefined) {
338
+ formatted.data = bodyObj.data;
339
+ }
340
+
341
+ // If we extracted meaningful fields, return formatted version
342
+ if (Object.keys(formatted).length > 0) {
343
+ return formatted;
344
+ }
345
+ }
346
+
347
+ // For non-error responses or if formatting didn't extract anything, use sanitized body
348
+ return this.sanitizeBody(body);
349
+ }
350
+
351
+ /**
352
+ * Sanitizes headers to redact sensitive information
353
+ */
354
+ private sanitizeHeaders(headers: Record<string, string | undefined>): Record<string, string> {
355
+ const sanitized: Record<string, string> = {};
356
+ const sensitiveKeys = ["key", "sign", "secret", "password", "token", "authorization", "auth"];
357
+
358
+ for (const [key, value] of Object.entries(headers)) {
359
+ if (value === undefined) continue;
360
+ const lowerKey = key.toLowerCase();
361
+ if (sensitiveKeys.some((sk) => lowerKey.includes(sk))) {
362
+ sanitized[key] = redact(value);
363
+ } else {
364
+ sanitized[key] = value;
365
+ }
366
+ }
367
+
368
+ return sanitized;
369
+ }
370
+
371
+ /**
372
+ * Sanitizes body to prevent logging huge payloads
373
+ */
374
+ private sanitizeBody(body: unknown): unknown {
375
+ if (body === undefined || body === null) {
376
+ return body;
377
+ }
378
+
379
+ // If body is a string, check size
380
+ if (typeof body === "string") {
381
+ if (body.length > 5000) {
382
+ return body.substring(0, 5000) + "... [truncated]";
383
+ }
384
+ return body;
385
+ }
386
+
387
+ // If body is an object, stringify and check size
388
+ try {
389
+ const jsonStr = JSON.stringify(body);
390
+ if (jsonStr.length > 5000) {
391
+ return JSON.parse(jsonStr.substring(0, 5000) + '..."}');
392
+ }
393
+ return body;
394
+ } catch {
395
+ return body;
396
+ }
397
+ }
398
+ }
package/src/index.ts ADDED
@@ -0,0 +1,71 @@
1
+ import { config as dotenvConfig } from "dotenv";
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+
5
+ import { getConfig } from "./config.js";
6
+ import { EnvCredentialsProvider } from "./security/credentials.js";
7
+ import { HttpClient } from "./http/httpClient.js";
8
+ import { ZebpayAPI } from "./private/ZebpayAPI.js";
9
+ import { SpotClient } from "./private/SpotClient.js";
10
+ import { FuturesClient } from "./private/FuturesClient.js";
11
+ import { PublicClient } from "./public/PublicClient.js";
12
+ import { PublicFuturesClient } from "./public/PublicFuturesClient.js";
13
+ import { registerSpotTools } from "./mcp/tools_spot.js";
14
+ import { registerFuturesTools } from "./mcp/tools_futures.js";
15
+ import { registerResources } from "./mcp/resources.js";
16
+ import { registerPrompts } from "./mcp/prompts.js";
17
+ import { fileLogger } from "./utils/fileLogger.js";
18
+
19
+ dotenvConfig();
20
+
21
+ function createServer(): McpServer {
22
+ return new McpServer(
23
+ {
24
+ name: "zebpay",
25
+ version: "1.0.0",
26
+ },
27
+ {
28
+ capabilities: {
29
+ tools: {},
30
+ logging: {},
31
+ resources: {},
32
+ prompts: {},
33
+ },
34
+ }
35
+ );
36
+ }
37
+
38
+ async function main(): Promise<void> {
39
+ const cfg = getConfig();
40
+ if (cfg.logFile) {
41
+ await fileLogger.initialize(cfg.logFile);
42
+ }
43
+
44
+ const creds = new EnvCredentialsProvider();
45
+ const http = new HttpClient(cfg.logLevel);
46
+ const api = new ZebpayAPI(cfg, creds, http);
47
+ const spot = new SpotClient(api);
48
+ const futuresPrivateClient = new FuturesClient(api);
49
+ const publicClient = new PublicClient(cfg, http);
50
+ const futuresPublicClient = new PublicFuturesClient(cfg, http);
51
+
52
+ const mcp = createServer();
53
+ registerSpotTools(mcp, spot, publicClient, cfg);
54
+ registerFuturesTools(mcp, futuresPublicClient, futuresPrivateClient, cfg);
55
+ registerResources(mcp, publicClient, spot, cfg);
56
+ registerPrompts(mcp, spot, publicClient, cfg);
57
+
58
+ const transport = new StdioServerTransport();
59
+ await mcp.connect(transport);
60
+
61
+ process.on("SIGINT", async () => {
62
+ await fileLogger.close();
63
+ process.exit(0);
64
+ });
65
+ }
66
+
67
+ main().catch(async (error) => {
68
+ console.error("FATAL: Failed to start Zebpay MCP stdio server", error);
69
+ await fileLogger.close();
70
+ process.exit(1);
71
+ });