@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.
- package/.env.example +14 -0
- package/README.md +223 -0
- package/dist/__tests__/errors.test.d.ts +5 -0
- package/dist/__tests__/errors.test.js +147 -0
- package/dist/__tests__/errors.test.js.map +1 -0
- package/dist/__tests__/prompts.test.d.ts +1 -0
- package/dist/__tests__/prompts.test.js +73 -0
- package/dist/__tests__/prompts.test.js.map +1 -0
- package/dist/__tests__/resources.test.d.ts +1 -0
- package/dist/__tests__/resources.test.js +79 -0
- package/dist/__tests__/resources.test.js.map +1 -0
- package/dist/__tests__/validation.test.d.ts +15 -0
- package/dist/__tests__/validation.test.js +64 -0
- package/dist/__tests__/validation.test.js.map +1 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.js +81 -0
- package/dist/config.js.map +1 -0
- package/dist/http/httpClient.d.ts +40 -0
- package/dist/http/httpClient.js +341 -0
- package/dist/http/httpClient.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +60 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/errors.d.ts +21 -0
- package/dist/mcp/errors.js +214 -0
- package/dist/mcp/errors.js.map +1 -0
- package/dist/mcp/logging.d.ts +21 -0
- package/dist/mcp/logging.js +241 -0
- package/dist/mcp/logging.js.map +1 -0
- package/dist/mcp/prompts.d.ts +9 -0
- package/dist/mcp/prompts.js +165 -0
- package/dist/mcp/prompts.js.map +1 -0
- package/dist/mcp/resources.d.ts +9 -0
- package/dist/mcp/resources.js +125 -0
- package/dist/mcp/resources.js.map +1 -0
- package/dist/mcp/tools_futures.d.ts +5 -0
- package/dist/mcp/tools_futures.js +694 -0
- package/dist/mcp/tools_futures.js.map +1 -0
- package/dist/mcp/tools_spot.d.ts +11 -0
- package/dist/mcp/tools_spot.js +2225 -0
- package/dist/mcp/tools_spot.js.map +1 -0
- package/dist/private/FuturesClient.d.ts +57 -0
- package/dist/private/FuturesClient.js +181 -0
- package/dist/private/FuturesClient.js.map +1 -0
- package/dist/private/SpotClient.d.ts +44 -0
- package/dist/private/SpotClient.js +201 -0
- package/dist/private/SpotClient.js.map +1 -0
- package/dist/private/ZebpayAPI.d.ts +19 -0
- package/dist/private/ZebpayAPI.js +172 -0
- package/dist/private/ZebpayAPI.js.map +1 -0
- package/dist/public/PublicClient.d.ts +79 -0
- package/dist/public/PublicClient.js +283 -0
- package/dist/public/PublicClient.js.map +1 -0
- package/dist/public/PublicFuturesClient.d.ts +27 -0
- package/dist/public/PublicFuturesClient.js +187 -0
- package/dist/public/PublicFuturesClient.js.map +1 -0
- package/dist/security/credentials.d.ts +42 -0
- package/dist/security/credentials.js +80 -0
- package/dist/security/credentials.js.map +1 -0
- package/dist/security/signing.d.ts +33 -0
- package/dist/security/signing.js +56 -0
- package/dist/security/signing.js.map +1 -0
- package/dist/types/responses.d.ts +130 -0
- package/dist/types/responses.js +6 -0
- package/dist/types/responses.js.map +1 -0
- package/dist/utils/cache.d.ts +29 -0
- package/dist/utils/cache.js +72 -0
- package/dist/utils/cache.js.map +1 -0
- package/dist/utils/fileLogger.d.ts +10 -0
- package/dist/utils/fileLogger.js +81 -0
- package/dist/utils/fileLogger.js.map +1 -0
- package/dist/utils/metrics.d.ts +35 -0
- package/dist/utils/metrics.js +94 -0
- package/dist/utils/metrics.js.map +1 -0
- package/dist/utils/responseFormatter.d.ts +93 -0
- package/dist/utils/responseFormatter.js +268 -0
- package/dist/utils/responseFormatter.js.map +1 -0
- package/dist/validation/schemas.d.ts +70 -0
- package/dist/validation/schemas.js +48 -0
- package/dist/validation/schemas.js.map +1 -0
- package/dist/validation/validators.d.ts +28 -0
- package/dist/validation/validators.js +129 -0
- package/dist/validation/validators.js.map +1 -0
- package/docs/LOGGING.md +371 -0
- package/docs/zebpay-ai-trading-beginner.png +0 -0
- package/mcp-config.json.example +20 -0
- package/package.json +54 -0
- package/scripts/README.md +103 -0
- package/scripts/clear-logs.js +52 -0
- package/scripts/log-stats.js +264 -0
- package/scripts/log-viewer.js +288 -0
- package/server.json +31 -0
- package/src/__tests__/errors.test.ts +180 -0
- package/src/__tests__/prompts.test.ts +89 -0
- package/src/__tests__/resources.test.ts +95 -0
- package/src/__tests__/validation.test.ts +88 -0
- package/src/config.ts +108 -0
- package/src/http/httpClient.ts +398 -0
- package/src/index.ts +71 -0
- package/src/mcp/errors.ts +262 -0
- package/src/mcp/logging.ts +284 -0
- package/src/mcp/prompts.ts +206 -0
- package/src/mcp/resources.ts +163 -0
- package/src/mcp/tools_futures.ts +874 -0
- package/src/mcp/tools_spot.ts +2702 -0
- package/src/private/FuturesClient.ts +189 -0
- package/src/private/SpotClient.ts +250 -0
- package/src/private/ZebpayAPI.ts +205 -0
- package/src/public/PublicClient.ts +381 -0
- package/src/public/PublicFuturesClient.ts +228 -0
- package/src/security/credentials.ts +114 -0
- package/src/security/signing.ts +98 -0
- package/src/types/responses.ts +146 -0
- package/src/utils/cache.ts +90 -0
- package/src/utils/fileLogger.ts +88 -0
- package/src/utils/metrics.ts +135 -0
- package/src/utils/responseFormatter.ts +361 -0
- package/src/validation/schemas.ts +66 -0
- package/src/validation/validators.ts +189 -0
- 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(/ /g, " ")
|
|
213
|
+
.replace(/&/g, "&")
|
|
214
|
+
.replace(/</g, "<")
|
|
215
|
+
.replace(/>/g, ">")
|
|
216
|
+
.replace(/"/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
|
+
|