@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,262 @@
1
+ /*
2
+ MCP error handling utilities using MCP SDK error types.
3
+ */
4
+
5
+ import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
6
+
7
+ /**
8
+ * Creates an MCP error for invalid parameters with helpful messages
9
+ */
10
+ export function createInvalidParamsError(message: string, data?: unknown): McpError {
11
+ return new McpError(ErrorCode.InvalidParams, message, data);
12
+ }
13
+
14
+ /**
15
+ * Creates an MCP error for internal/server errors
16
+ */
17
+ export function createInternalError(message: string, data?: unknown): McpError {
18
+ return new McpError(ErrorCode.InternalError, message, data);
19
+ }
20
+
21
+ /**
22
+ * Converts HTTP errors to appropriate MCP errors.
23
+ *
24
+ * NOTE: This is transport-agnostic - converts errors from Zebpay API (external)
25
+ * to MCP-compliant errors. Works identically for both stdio and HTTP transports.
26
+ *
27
+ * @param status HTTP status code from Zebpay API
28
+ * @param message Error message (may be HTML if from Cloudflare)
29
+ * @param details Additional error details
30
+ * @param isHtmlResponse Whether the response was HTML (for Cloudflare errors)
31
+ */
32
+ export function convertHttpErrorToMcpError(
33
+ status: number,
34
+ message: string,
35
+ details?: unknown,
36
+ isHtmlResponse?: boolean
37
+ ): McpError {
38
+ // If message is HTML or contains HTML tags, try to extract meaningful error
39
+ if (isHtmlResponse || (typeof message === "string" && (message.includes("<!doctype") || message.includes("<html")))) {
40
+ // Try to parse HTML error
41
+ const htmlText = typeof message === "string" ? message : String(details || "");
42
+ const parsedMessage = parseHtmlError(htmlText);
43
+
44
+ if (parsedMessage) {
45
+ message = parsedMessage;
46
+ } else {
47
+ // Fallback for HTML errors
48
+ if (htmlText.includes("Access denied") || htmlText.includes("banned")) {
49
+ message = "Access denied: Your IP address has been restricted. Please contact support or try again later.";
50
+ } else if (htmlText.includes("Cloudflare")) {
51
+ message = "Cloudflare error: Access to the API has been restricted. Please try again later or contact support.";
52
+ } else {
53
+ message = "API access error: The server returned an HTML error page. Please check your API credentials and try again.";
54
+ }
55
+ }
56
+ }
57
+
58
+ // 4xx errors are typically invalid parameters or requests
59
+ if (status >= 400 && status < 500) {
60
+ // 401/403 are authentication errors - treat as invalid params
61
+ if (status === 401 || status === 403) {
62
+ const userMessage = message && message !== `HTTP ${status}` && !message.startsWith("HTTP")
63
+ ? `${message}. Please check your API credentials.`
64
+ : "Authentication failed. Please check your API credentials and try again.";
65
+
66
+ return createInvalidParamsError(
67
+ userMessage,
68
+ { status, details }
69
+ );
70
+ }
71
+ // 400 is bad request - invalid params
72
+ if (status === 400) {
73
+ // Extract additional error details from response
74
+ let enhancedMessage = message;
75
+ let statusCode: number | undefined;
76
+ let statusDescription: string | undefined;
77
+
78
+ if (details && typeof details === "object") {
79
+ const detailsObj = details as Record<string, unknown>;
80
+ statusCode = typeof detailsObj.statusCode === "number" ? detailsObj.statusCode : undefined;
81
+ statusDescription = typeof detailsObj.statusDescription === "string"
82
+ ? detailsObj.statusDescription
83
+ : undefined;
84
+
85
+ // Handle specific API error codes
86
+ if (statusCode === 77 && statusDescription) {
87
+ // Market order minimum value error
88
+ // Extract minimum value from the error message if present
89
+ const minValueMatch = statusDescription.match(/minimum\s+([\d,]+\.?\d*)\s+(\w+)/i);
90
+ if (minValueMatch) {
91
+ const minValue = minValueMatch[1];
92
+ const currency = minValueMatch[2];
93
+ enhancedMessage = `${statusDescription} Please ensure your order value meets the minimum requirement of ${minValue} ${currency}.`;
94
+ } else {
95
+ enhancedMessage = statusDescription;
96
+ }
97
+ } else if (statusCode && statusDescription) {
98
+ // Use the status description with code for context
99
+ enhancedMessage = `[Error ${statusCode}] ${statusDescription}`;
100
+ } else if (statusDescription) {
101
+ // Use the status description if available
102
+ enhancedMessage = statusDescription;
103
+ }
104
+ }
105
+
106
+ // If message already contains useful information, use it directly
107
+ // Otherwise, provide a generic message
108
+ const userMessage = enhancedMessage && enhancedMessage !== "HTTP 400" && !enhancedMessage.startsWith("HTTP")
109
+ ? enhancedMessage
110
+ : message && message !== "HTTP 400" && !message.startsWith("HTTP")
111
+ ? message
112
+ : "The request was invalid. Please check your parameters and try again.";
113
+
114
+ return createInvalidParamsError(
115
+ userMessage,
116
+ { status, statusCode, statusDescription, details }
117
+ );
118
+ }
119
+ // 404 is not found - could be invalid symbol or resource
120
+ if (status === 404) {
121
+ const userMessage = message && message !== "HTTP 404" && !message.startsWith("HTTP")
122
+ ? `${message}. Please check the symbol or resource identifier.`
123
+ : "Resource not found. Please check the symbol or resource identifier and try again.";
124
+
125
+ return createInvalidParamsError(
126
+ userMessage,
127
+ { status, details }
128
+ );
129
+ }
130
+ // 429 is rate limit - treat as internal error with retry guidance
131
+ if (status === 429) {
132
+ const userMessage = message && message !== "HTTP 429" && !message.startsWith("HTTP")
133
+ ? `Rate limit exceeded: ${message}. Please retry after a short delay.`
134
+ : "Rate limit exceeded. Please wait a moment and try again.";
135
+
136
+ return createInternalError(
137
+ userMessage,
138
+ { status, details, retryable: true }
139
+ );
140
+ }
141
+ // Other 4xx errors
142
+ const userMessage = message && message !== `HTTP ${status}` && !message.startsWith("HTTP")
143
+ ? message
144
+ : `Request error (${status}). Please check your parameters and try again.`;
145
+
146
+ return createInvalidParamsError(
147
+ userMessage,
148
+ { status, details }
149
+ );
150
+ }
151
+
152
+ // 5xx errors are server/internal errors
153
+ if (status >= 500) {
154
+ const userMessage = message && message !== `HTTP ${status}` && !message.startsWith("HTTP")
155
+ ? `Zebpay API server error: ${message}. Please try again later.`
156
+ : `Zebpay API server error (${status}). The server encountered an issue. Please try again later.`;
157
+
158
+ return createInternalError(
159
+ userMessage,
160
+ { status, details, retryable: true }
161
+ );
162
+ }
163
+
164
+ // Network errors (status 0) or other errors
165
+ const userMessage = message && message !== "Network error" && message !== "Unknown API error"
166
+ ? `Network error: ${message}. Please check your connection and try again.`
167
+ : "Network error. Please check your internet connection and try again.";
168
+
169
+ return createInternalError(
170
+ userMessage,
171
+ { status, details, retryable: true }
172
+ );
173
+ }
174
+
175
+ /**
176
+ * Parses HTML error pages (like Cloudflare) to extract user-friendly error messages.
177
+ *
178
+ * NOTE: Transport-agnostic - parses HTML responses from Zebpay API (external),
179
+ * not from MCP clients. Works for both stdio and HTTP transports.
180
+ */
181
+ function parseHtmlError(html: string): string | null {
182
+ if (!html || typeof html !== "string") {
183
+ return null;
184
+ }
185
+
186
+ // Check if it's HTML
187
+ if (!html.trim().toLowerCase().startsWith("<!doctype") && !html.trim().toLowerCase().startsWith("<html")) {
188
+ return null;
189
+ }
190
+
191
+ // Extract Cloudflare error messages
192
+ const cloudflarePatterns = [
193
+ // Cloudflare Error 1006 - Access denied
194
+ /<h2[^>]*>Access denied<\/h2>/i,
195
+ /The owner of this website.*?has banned your IP address[^<]*/i,
196
+ /<p[^>]*>The owner of this website[^<]*<\/p>/i,
197
+ // Cloudflare Error 1020 - Access denied
198
+ /<h1[^>]*>Error[^<]*<\/h1>/i,
199
+ // Generic error extraction from title
200
+ /<title>([^<]+)<\/title>/i,
201
+ // Extract error messages from h1/h2 tags
202
+ /<h[12][^>]*>([^<]+)<\/h[12]>/i,
203
+ ];
204
+
205
+ for (const pattern of cloudflarePatterns) {
206
+ const match = html.match(pattern);
207
+ if (match) {
208
+ let message = match[1] || match[0];
209
+ // Clean up HTML entities and tags
210
+ message = message
211
+ .replace(/<[^>]+>/g, "")
212
+ .replace(/&nbsp;/g, " ")
213
+ .replace(/&amp;/g, "&")
214
+ .replace(/&lt;/g, "<")
215
+ .replace(/&gt;/g, ">")
216
+ .replace(/&quot;/g, '"')
217
+ .replace(/\s+/g, " ")
218
+ .trim();
219
+
220
+ if (message && message.length > 10) {
221
+ // Check for IP ban message
222
+ if (html.includes("banned your IP address")) {
223
+ const ipMatch = html.match(/IP address[^<]*\(([^)]+)\)/i);
224
+ if (ipMatch) {
225
+ return `Access denied: Your IP address has been banned by the website. Please contact support or try again later.`;
226
+ }
227
+ return `Access denied: Your IP address has been restricted. Please contact support or try again later.`;
228
+ }
229
+
230
+ // Check for Cloudflare error codes
231
+ const errorCodeMatch = html.match(/Error\s*(\d+)/i);
232
+ if (errorCodeMatch) {
233
+ const errorCode = errorCodeMatch[1];
234
+ if (errorCode === "1006") {
235
+ return `Access denied (Error 1006): Your IP address has been banned. Please contact support or try again later.`;
236
+ }
237
+ return `Cloudflare error (${errorCode}): ${message}`;
238
+ }
239
+
240
+ return message;
241
+ }
242
+ }
243
+ }
244
+
245
+ // Fallback: try to extract any meaningful text
246
+ const textMatch = html.match(/<body[^>]*>([\s\S]{100,500})<\/body>/i);
247
+ if (textMatch) {
248
+ let text = textMatch[1]
249
+ .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "")
250
+ .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "")
251
+ .replace(/<[^>]+>/g, " ")
252
+ .replace(/\s+/g, " ")
253
+ .trim();
254
+
255
+ if (text.length > 20 && text.length < 200) {
256
+ return text;
257
+ }
258
+ }
259
+
260
+ return null;
261
+ }
262
+
@@ -0,0 +1,284 @@
1
+ /*
2
+ MCP request/response logging utility for tracing and debugging.
3
+ */
4
+
5
+ import { fileLogger } from "../utils/fileLogger.js";
6
+ import { generateCorrelationId } from "../utils/responseFormatter.js";
7
+ import { McpError } from "@modelcontextprotocol/sdk/types.js";
8
+
9
+ export interface LogContext {
10
+ toolName: string;
11
+ logLevel: "debug" | "info" | "warn" | "error";
12
+ correlationId?: string;
13
+ }
14
+
15
+ /**
16
+ * Logs MCP tool request with parameters
17
+ */
18
+ export function logMcpRequest(context: LogContext, params: unknown): void {
19
+ try {
20
+ const { toolName, logLevel, correlationId } = context;
21
+ const timestamp = new Date().toISOString();
22
+
23
+ const logMessage = JSON.stringify({
24
+ level: "info",
25
+ type: "mcp_request",
26
+ timestamp,
27
+ tool: toolName,
28
+ correlationId: correlationId || generateCorrelationId(),
29
+ params: sanitizeParams(params),
30
+ });
31
+
32
+ fileLogger.log(logMessage);
33
+ } catch (error) {
34
+ // Silently fail logging to prevent breaking the application
35
+ console.error(`[LOG ERROR] Failed to log MCP request: ${error instanceof Error ? error.message : String(error)}`);
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Logs MCP tool response (success)
41
+ */
42
+ export function logMcpResponse(
43
+ context: LogContext,
44
+ params: unknown,
45
+ result: unknown,
46
+ durationMs: number
47
+ ): void {
48
+ try {
49
+ const { toolName, logLevel, correlationId } = context;
50
+ const timestamp = new Date().toISOString();
51
+
52
+ const logMessage = JSON.stringify({
53
+ level: "info",
54
+ type: "mcp_response",
55
+ timestamp,
56
+ tool: toolName,
57
+ correlationId: correlationId || generateCorrelationId(),
58
+ params: sanitizeParams(params),
59
+ success: true,
60
+ durationMs,
61
+ result: sanitizeResult(result),
62
+ });
63
+
64
+ fileLogger.log(logMessage);
65
+ } catch (error) {
66
+ // Silently fail logging to prevent breaking the application
67
+ console.error(`[LOG ERROR] Failed to log MCP response: ${error instanceof Error ? error.message : String(error)}`);
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Sanitizes stack traces by removing file paths, keeping only function names and line numbers
73
+ */
74
+ function sanitizeStack(stack: string | undefined): string | undefined {
75
+ if (!stack) return undefined;
76
+
77
+ try {
78
+ // Remove file:// URLs and absolute paths, keep only filename and line numbers
79
+ const lines = stack.split('\n');
80
+ const sanitizedLines = lines.map(line => {
81
+ let sanitized = line;
82
+
83
+ // Remove file:// URLs (e.g., file:///Users/path/to/file.js:123:45)
84
+ sanitized = sanitized.replace(/file:\/\/\/[^\s\)]+/g, (match) => {
85
+ // Extract just the filename and line numbers
86
+ const filenameMatch = match.match(/([^\/]+\.(js|ts|mjs|cjs)):(\d+):(\d+)/);
87
+ if (filenameMatch) {
88
+ return `${filenameMatch[1]}:${filenameMatch[3]}:${filenameMatch[4]}`;
89
+ }
90
+ return '';
91
+ });
92
+
93
+ // Remove absolute paths (e.g., /Users/path/to/file.js:123:45)
94
+ sanitized = sanitized.replace(/\/(?:[^\/\s]+\/)+([^\/\s]+\.(js|ts|mjs|cjs)):(\d+):(\d+)/g, '$1:$3:$4');
95
+
96
+ // Simplify node_modules paths (e.g., node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js)
97
+ sanitized = sanitized.replace(/node_modules\/([^\/\s]+)\/[^\s\)]+/g, 'node_modules/$1');
98
+
99
+ return sanitized.trim();
100
+ });
101
+
102
+ return sanitizedLines.join('\n');
103
+ } catch {
104
+ // If sanitization fails, return a minimal version without paths
105
+ return stack.split('\n').slice(0, 3).join('\n'); // Keep only first 3 lines
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Logs MCP tool error response
111
+ */
112
+ export function logMcpError(
113
+ context: LogContext,
114
+ params: unknown,
115
+ error: unknown,
116
+ durationMs: number
117
+ ): void {
118
+ try {
119
+ const { toolName, logLevel, correlationId } = context;
120
+ const timestamp = new Date().toISOString();
121
+
122
+ // Extract error details
123
+ let errorMessage = error instanceof Error ? error.message : String(error);
124
+ let errorStack = error instanceof Error ? error.stack : undefined;
125
+ let errorCode: number | undefined;
126
+ let errorData: unknown;
127
+
128
+ // If it's an MCP error, extract additional details
129
+ if (error instanceof McpError) {
130
+ errorCode = error.code;
131
+ errorData = error.data;
132
+ // MCP errors have a formatted message, use it directly
133
+ errorMessage = error.message;
134
+ } else if (error && typeof error === "object" && "code" in error) {
135
+ // Check if it's an MCP error-like object (from SDK)
136
+ errorCode = typeof error.code === "number" ? error.code : undefined;
137
+ if ("data" in error) {
138
+ errorData = error.data;
139
+ }
140
+ }
141
+
142
+ const logMessage = JSON.stringify({
143
+ level: "error",
144
+ type: "mcp_error",
145
+ timestamp,
146
+ tool: toolName,
147
+ correlationId: correlationId || generateCorrelationId(),
148
+ params: sanitizeParams(params),
149
+ success: false,
150
+ durationMs,
151
+ error: errorMessage,
152
+ ...(errorCode !== undefined && { errorCode }),
153
+ ...(errorData !== undefined && { errorData: sanitizeParams(errorData) }),
154
+ ...(errorStack && { stack: sanitizeStack(errorStack) }),
155
+ });
156
+
157
+ fileLogger.log(logMessage);
158
+ } catch (logError) {
159
+ // Silently fail logging to prevent breaking the application
160
+ console.error(`[LOG ERROR] Failed to log MCP error: ${logError instanceof Error ? logError.message : String(logError)}`);
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Wraps an MCP tool handler with request/response logging
166
+ */
167
+ export function withLogging<TParams, TResult>(
168
+ toolName: string,
169
+ logLevel: "debug" | "info" | "warn" | "error",
170
+ handler: (params: TParams) => Promise<TResult>
171
+ ): (params: TParams) => Promise<TResult> {
172
+ return async (params: TParams): Promise<TResult> => {
173
+ const correlationId = generateCorrelationId();
174
+ const context: LogContext = { toolName, logLevel, correlationId };
175
+ const startTime = Date.now();
176
+
177
+ // Log request
178
+ logMcpRequest(context, params);
179
+
180
+ try {
181
+ // Execute handler
182
+ const result = await handler(params);
183
+ const durationMs = Date.now() - startTime;
184
+
185
+ // Log successful response
186
+ logMcpResponse(context, params, result, durationMs);
187
+
188
+ return result;
189
+ } catch (error) {
190
+ const durationMs = Date.now() - startTime;
191
+
192
+ // Log error response
193
+ logMcpError(context, params, error, durationMs);
194
+
195
+ // Re-throw error
196
+ throw error;
197
+ }
198
+ };
199
+ }
200
+
201
+ /**
202
+ * Sanitizes parameters to remove sensitive data
203
+ */
204
+ function sanitizeParams(params: unknown): unknown {
205
+ try {
206
+ if (!params || typeof params !== "object") {
207
+ return params;
208
+ }
209
+
210
+ const sanitized: Record<string, unknown> = {};
211
+ const sensitiveKeys = ["apiKey", "secret", "password", "token", "authorization"];
212
+
213
+ for (const [key, value] of Object.entries(params as Record<string, unknown>)) {
214
+ const lowerKey = key.toLowerCase();
215
+ if (sensitiveKeys.some((sk) => lowerKey.includes(sk))) {
216
+ sanitized[key] = "<redacted>";
217
+ } else {
218
+ sanitized[key] = value;
219
+ }
220
+ }
221
+
222
+ return sanitized;
223
+ } catch (error) {
224
+ // If sanitization fails, return a safe fallback
225
+ return { error: "Failed to sanitize params", original: String(params) };
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Sanitizes result to prevent logging huge responses
231
+ */
232
+ function sanitizeResult(result: unknown): unknown {
233
+ try {
234
+ if (!result) {
235
+ return result;
236
+ }
237
+
238
+ // If result is a string, check if it's JSON and limit size
239
+ if (typeof result === "string") {
240
+ if (result.length > 10000) {
241
+ return result.substring(0, 10000) + "... [truncated]";
242
+ }
243
+ return result;
244
+ }
245
+
246
+ // If result is an object with content array (MCP response format)
247
+ if (typeof result === "object" && result !== null) {
248
+ const obj = result as Record<string, unknown>;
249
+ if (Array.isArray(obj.content)) {
250
+ const sanitizedContent = obj.content.map((item: unknown) => {
251
+ if (typeof item === "object" && item !== null) {
252
+ const itemObj = item as Record<string, unknown>;
253
+ if (typeof itemObj.text === "string" && itemObj.text.length > 10000) {
254
+ return {
255
+ ...itemObj,
256
+ text: itemObj.text.substring(0, 10000) + "... [truncated]",
257
+ };
258
+ }
259
+ }
260
+ return item;
261
+ });
262
+ return {
263
+ ...obj,
264
+ content: sanitizedContent,
265
+ };
266
+ }
267
+ }
268
+
269
+ // For other objects, stringify and limit size
270
+ try {
271
+ const jsonStr = JSON.stringify(result);
272
+ if (jsonStr.length > 10000) {
273
+ return JSON.parse(jsonStr.substring(0, 10000) + '..."}');
274
+ }
275
+ return result;
276
+ } catch {
277
+ return result;
278
+ }
279
+ } catch (error) {
280
+ // If sanitization fails, return a safe fallback
281
+ return { error: "Failed to sanitize result", type: typeof result };
282
+ }
283
+ }
284
+