@myatkyawthu/mcp-connect 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +308 -0
- package/package.json +63 -0
- package/src/cli.js +170 -0
- package/src/defineMCP.js +72 -0
- package/src/index.js +35 -0
- package/src/server/mcpServer.js +236 -0
- package/src/types/mcp.js +108 -0
- package/src/utils/configValidation.js +381 -0
- package/src/utils/logger.js +290 -0
- package/src/utils/validation.js +78 -0
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comprehensive configuration validation for MCP-Connect
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {Object} ValidationError
|
|
7
|
+
* @property {string} field - The field that has an error
|
|
8
|
+
* @property {string} message - Error message
|
|
9
|
+
* @property {any} [value] - The invalid value
|
|
10
|
+
* @property {string} [suggestion] - Suggestion for fixing the error
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {Object} ValidationResult
|
|
15
|
+
* @property {boolean} isValid - Whether the configuration is valid
|
|
16
|
+
* @property {ValidationError[]} errors - Array of validation errors
|
|
17
|
+
* @property {ValidationError[]} warnings - Array of validation warnings
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* JSON Schema-like validation for MCP configuration
|
|
22
|
+
*/
|
|
23
|
+
export class ConfigValidator {
|
|
24
|
+
constructor() {
|
|
25
|
+
/** @type {ValidationError[]} */
|
|
26
|
+
this.errors = [];
|
|
27
|
+
/** @type {ValidationError[]} */
|
|
28
|
+
this.warnings = [];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Validate MCP configuration
|
|
33
|
+
* @param {any} config - Configuration to validate
|
|
34
|
+
* @returns {ValidationResult} Validation result
|
|
35
|
+
*/
|
|
36
|
+
validate(config) {
|
|
37
|
+
this.errors = [];
|
|
38
|
+
this.warnings = [];
|
|
39
|
+
|
|
40
|
+
// Basic structure validation
|
|
41
|
+
this.validateBasicStructure(config);
|
|
42
|
+
|
|
43
|
+
if (this.errors.length === 0) {
|
|
44
|
+
// Detailed field validation
|
|
45
|
+
this.validateName(config.name);
|
|
46
|
+
this.validateVersion(config.version);
|
|
47
|
+
this.validateDescription(config.description);
|
|
48
|
+
this.validateTools(config.tools);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
isValid: this.errors.length === 0,
|
|
53
|
+
errors: this.errors,
|
|
54
|
+
warnings: this.warnings,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Add validation error
|
|
60
|
+
* @param {string} field - Field name
|
|
61
|
+
* @param {string} message - Error message
|
|
62
|
+
* @param {any} [value] - Invalid value
|
|
63
|
+
* @param {string} [suggestion] - Suggestion for fix
|
|
64
|
+
*/
|
|
65
|
+
addError(field, message, value, suggestion) {
|
|
66
|
+
this.errors.push({ field, message, value, suggestion });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Add validation warning
|
|
71
|
+
* @param {string} field - Field name
|
|
72
|
+
* @param {string} message - Warning message
|
|
73
|
+
* @param {any} [value] - Value that triggered warning
|
|
74
|
+
* @param {string} [suggestion] - Suggestion for improvement
|
|
75
|
+
*/
|
|
76
|
+
addWarning(field, message, value, suggestion) {
|
|
77
|
+
this.warnings.push({ field, message, value, suggestion });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Validate basic configuration structure
|
|
82
|
+
* @param {any} config - Configuration to validate
|
|
83
|
+
*/
|
|
84
|
+
validateBasicStructure(config) {
|
|
85
|
+
if (!config || typeof config !== "object") {
|
|
86
|
+
this.addError("config", "Configuration must be an object", config, "Use defineMCP({ ... })");
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (Array.isArray(config)) {
|
|
91
|
+
this.addError("config", "Configuration cannot be an array", config, "Use defineMCP({ ... }) instead of [...]");
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Check for required fields
|
|
96
|
+
const requiredFields = ["name", "version", "tools"];
|
|
97
|
+
for (const field of requiredFields) {
|
|
98
|
+
if (!(field in config)) {
|
|
99
|
+
this.addError(field, `Required field "${field}" is missing`, undefined, `Add ${field}: "..."`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Validate name field
|
|
106
|
+
* @param {any} name - Name to validate
|
|
107
|
+
*/
|
|
108
|
+
validateName(name) {
|
|
109
|
+
if (name === undefined) return; // Already handled in basic structure
|
|
110
|
+
|
|
111
|
+
if (typeof name !== "string") {
|
|
112
|
+
this.addError("name", "Name must be a string", name, 'Use name: "My App"');
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (name.trim() === "") {
|
|
117
|
+
this.addError("name", "Name cannot be empty", name, 'Use name: "My App"');
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (name.length > 100) {
|
|
122
|
+
this.addWarning("name", "Name is very long (>100 chars)", name, "Consider a shorter name");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!/^[a-zA-Z0-9\s\-_\.]+$/.test(name)) {
|
|
126
|
+
this.addWarning("name", "Name contains special characters", name, "Use only letters, numbers, spaces, hyphens, underscores, and dots");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Validate version field
|
|
132
|
+
* @param {any} version - Version to validate
|
|
133
|
+
*/
|
|
134
|
+
validateVersion(version) {
|
|
135
|
+
if (version === undefined) return; // Already handled in basic structure
|
|
136
|
+
|
|
137
|
+
if (typeof version !== "string") {
|
|
138
|
+
this.addError("version", "Version must be a string", version, 'Use version: "1.0.0"');
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (version.trim() === "") {
|
|
143
|
+
this.addError("version", "Version cannot be empty", version, 'Use version: "1.0.0"');
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Check for semantic versioning
|
|
148
|
+
const semverRegex = /^\d+\.\d+\.\d+(-[a-zA-Z0-9\-\.]+)?(\+[a-zA-Z0-9\-\.]+)?$/;
|
|
149
|
+
if (!semverRegex.test(version)) {
|
|
150
|
+
this.addWarning("version", "Version doesn't follow semantic versioning", version, 'Use format: "1.0.0"');
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Validate description field
|
|
156
|
+
* @param {any} description - Description to validate
|
|
157
|
+
*/
|
|
158
|
+
validateDescription(description) {
|
|
159
|
+
if (description === undefined) return; // Optional field
|
|
160
|
+
|
|
161
|
+
if (typeof description !== "string") {
|
|
162
|
+
this.addError("description", "Description must be a string", description, 'Use description: "My app description"');
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (description.length > 500) {
|
|
167
|
+
this.addWarning("description", "Description is very long (>500 chars)", description, "Consider a shorter description");
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Validate tools array
|
|
175
|
+
* @param {any} tools - Tools to validate
|
|
176
|
+
*/
|
|
177
|
+
validateTools(tools) {
|
|
178
|
+
if (tools === undefined) return; // Already handled in basic structure
|
|
179
|
+
|
|
180
|
+
if (!Array.isArray(tools)) {
|
|
181
|
+
this.addError("tools", "Tools must be an array", tools, "Use tools: [...]");
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (tools.length === 0) {
|
|
186
|
+
this.addError("tools", "At least one tool must be defined", tools, 'Add tools: [["myTool", handler]]');
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (tools.length > 50) {
|
|
191
|
+
this.addWarning("tools", "Large number of tools (>50)", tools, "Consider grouping related functionality");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Validate each tool
|
|
195
|
+
const toolNames = new Set();
|
|
196
|
+
tools.forEach((tool, index) => {
|
|
197
|
+
this.validateTool(tool, index, toolNames);
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Validate individual tool
|
|
203
|
+
* @param {any} tool - Tool to validate
|
|
204
|
+
* @param {number} index - Tool index
|
|
205
|
+
* @param {Set<string>} toolNames - Set of existing tool names
|
|
206
|
+
*/
|
|
207
|
+
validateTool(tool, index, toolNames) {
|
|
208
|
+
const fieldPrefix = `tools[${index}]`;
|
|
209
|
+
|
|
210
|
+
if (Array.isArray(tool)) {
|
|
211
|
+
// Tuple format: [name, handler]
|
|
212
|
+
if (tool.length !== 2) {
|
|
213
|
+
this.addError(fieldPrefix, "Tuple format must have exactly 2 elements [name, handler]", tool, '["toolName", handlerFunction]');
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const [name, handler] = tool;
|
|
218
|
+
this.validateToolName(name, `${fieldPrefix}.name`, toolNames);
|
|
219
|
+
this.validateToolHandler(handler, `${fieldPrefix}.handler`);
|
|
220
|
+
} else if (typeof tool === "object" && tool !== null) {
|
|
221
|
+
// Object format: { name, handler, description?, schema? }
|
|
222
|
+
this.validateToolName(tool.name, `${fieldPrefix}.name`, toolNames);
|
|
223
|
+
this.validateToolHandler(tool.handler, `${fieldPrefix}.handler`);
|
|
224
|
+
this.validateToolDescription(tool.description, `${fieldPrefix}.description`);
|
|
225
|
+
this.validateToolSchema(tool.schema, `${fieldPrefix}.schema`);
|
|
226
|
+
|
|
227
|
+
// Check for unknown properties
|
|
228
|
+
const knownProps = ["name", "handler", "description", "schema"];
|
|
229
|
+
const unknownProps = Object.keys(tool).filter(prop => !knownProps.includes(prop));
|
|
230
|
+
if (unknownProps.length > 0) {
|
|
231
|
+
this.addWarning(fieldPrefix, `Unknown properties: ${unknownProps.join(", ")}`, unknownProps, "Remove unknown properties");
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
this.addError(fieldPrefix, "Tool must be [name, handler] or {name, handler, ...}", tool, '["toolName", handler] or {name: "toolName", handler}');
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Validate tool name
|
|
240
|
+
* @param {any} name - Tool name to validate
|
|
241
|
+
* @param {string} fieldPath - Field path for error reporting
|
|
242
|
+
* @param {Set<string>} toolNames - Set of existing tool names
|
|
243
|
+
*/
|
|
244
|
+
validateToolName(name, fieldPath, toolNames) {
|
|
245
|
+
if (typeof name !== "string") {
|
|
246
|
+
this.addError(fieldPath, "Tool name must be a string", name, '"myToolName"');
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (name.trim() === "") {
|
|
251
|
+
this.addError(fieldPath, "Tool name cannot be empty", name, '"myToolName"');
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const trimmedName = name.trim();
|
|
256
|
+
if (toolNames.has(trimmedName)) {
|
|
257
|
+
this.addError(fieldPath, `Duplicate tool name "${trimmedName}"`, name, "Each tool must have a unique name");
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
toolNames.add(trimmedName);
|
|
261
|
+
|
|
262
|
+
// Validate name format
|
|
263
|
+
if (!/^[a-zA-Z][a-zA-Z0-9_]*$/.test(trimmedName)) {
|
|
264
|
+
this.addWarning(fieldPath, "Tool name should start with letter and contain only letters, numbers, underscores", name, '"myToolName" or "my_tool_name"');
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (trimmedName.length > 50) {
|
|
268
|
+
this.addWarning(fieldPath, "Tool name is very long (>50 chars)", name, "Consider a shorter name");
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Validate tool handler function
|
|
274
|
+
* @param {any} handler - Handler to validate
|
|
275
|
+
* @param {string} fieldPath - Field path for error reporting
|
|
276
|
+
*/
|
|
277
|
+
validateToolHandler(handler, fieldPath) {
|
|
278
|
+
if (typeof handler !== "function") {
|
|
279
|
+
this.addError(fieldPath, "Tool handler must be a function", typeof handler, "async (args) => { ... }");
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Check if it's an async function or returns a promise
|
|
284
|
+
const isAsync = handler.constructor.name === "AsyncFunction";
|
|
285
|
+
const handlerString = handler.toString();
|
|
286
|
+
|
|
287
|
+
if (!isAsync && !handlerString.includes("Promise") && !handlerString.includes("await")) {
|
|
288
|
+
this.addWarning(fieldPath, "Consider making tool handler async", undefined, "async (args) => { ... }");
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Validate tool description
|
|
294
|
+
* @param {any} description - Description to validate
|
|
295
|
+
* @param {string} fieldPath - Field path for error reporting
|
|
296
|
+
*/
|
|
297
|
+
validateToolDescription(description, fieldPath) {
|
|
298
|
+
if (description === undefined) return; // Optional
|
|
299
|
+
|
|
300
|
+
if (typeof description !== "string") {
|
|
301
|
+
this.addError(fieldPath, "Tool description must be a string", description, '"Description of what this tool does"');
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (description.length > 200) {
|
|
306
|
+
this.addWarning(fieldPath, "Tool description is very long (>200 chars)", description, "Consider a shorter description");
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Validate tool schema
|
|
312
|
+
* @param {any} schema - Schema to validate
|
|
313
|
+
* @param {string} fieldPath - Field path for error reporting
|
|
314
|
+
*/
|
|
315
|
+
validateToolSchema(schema, fieldPath) {
|
|
316
|
+
if (schema === undefined) return; // Optional
|
|
317
|
+
|
|
318
|
+
if (typeof schema !== "object" || schema === null || Array.isArray(schema)) {
|
|
319
|
+
this.addError(fieldPath, "Tool schema must be an object", schema, "{ type: 'object', properties: {...} }");
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Basic JSON Schema validation
|
|
324
|
+
if (schema.type && typeof schema.type !== "string") {
|
|
325
|
+
this.addError(`${fieldPath}.type`, "Schema type must be a string", schema.type, '"object"');
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (schema.properties && (typeof schema.properties !== "object" || Array.isArray(schema.properties))) {
|
|
329
|
+
this.addError(`${fieldPath}.properties`, "Schema properties must be an object", schema.properties, "{ prop1: { type: 'string' } }");
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (schema.required && !Array.isArray(schema.required)) {
|
|
333
|
+
this.addError(`${fieldPath}.required`, "Schema required must be an array", schema.required, '["prop1", "prop2"]');
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Validate MCP configuration with detailed error reporting
|
|
340
|
+
* @param {any} config - Configuration to validate
|
|
341
|
+
* @returns {ValidationResult} Validation result
|
|
342
|
+
*/
|
|
343
|
+
export function validateConfig(config) {
|
|
344
|
+
const validator = new ConfigValidator();
|
|
345
|
+
return validator.validate(config);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Format validation errors for user-friendly display
|
|
350
|
+
* @param {ValidationResult} result - Validation result to format
|
|
351
|
+
* @returns {string} Formatted error message
|
|
352
|
+
*/
|
|
353
|
+
export function formatValidationErrors(result) {
|
|
354
|
+
const lines = [];
|
|
355
|
+
|
|
356
|
+
if (result.errors.length > 0) {
|
|
357
|
+
lines.push("❌ Configuration Errors:");
|
|
358
|
+
result.errors.forEach((error, index) => {
|
|
359
|
+
lines.push(` ${index + 1}. ${error.field}: ${error.message}`);
|
|
360
|
+
if (error.value !== undefined) {
|
|
361
|
+
lines.push(` Current value: ${JSON.stringify(error.value)}`);
|
|
362
|
+
}
|
|
363
|
+
if (error.suggestion) {
|
|
364
|
+
lines.push(` Suggestion: ${error.suggestion}`);
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (result.warnings.length > 0) {
|
|
370
|
+
if (lines.length > 0) lines.push("");
|
|
371
|
+
lines.push("⚠️ Configuration Warnings:");
|
|
372
|
+
result.warnings.forEach((warning, index) => {
|
|
373
|
+
lines.push(` ${index + 1}. ${warning.field}: ${warning.message}`);
|
|
374
|
+
if (warning.suggestion) {
|
|
375
|
+
lines.push(` Suggestion: ${warning.suggestion}`);
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return lines.join("\n");
|
|
381
|
+
}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhanced structured logger for MCP-Connect
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {'debug'|'info'|'warn'|'error'} LogLevel
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {Object} LogContext
|
|
11
|
+
* @property {string} timestamp - ISO timestamp
|
|
12
|
+
* @property {LogLevel} level - Log level
|
|
13
|
+
* @property {string} message - Log message
|
|
14
|
+
* @property {any} [data] - Additional data
|
|
15
|
+
* @property {number} [duration] - Duration in milliseconds
|
|
16
|
+
* @property {string} [toolName] - Tool name
|
|
17
|
+
* @property {string} [requestId] - Request ID
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {Object} LogMeta
|
|
22
|
+
* @property {number} [duration] - Duration in milliseconds
|
|
23
|
+
* @property {string} [toolName] - Tool name
|
|
24
|
+
* @property {string} [requestId] - Request ID
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
class MCPLogger {
|
|
28
|
+
constructor() {
|
|
29
|
+
/** @type {LogLevel} */
|
|
30
|
+
this.logLevel = 'info';
|
|
31
|
+
/** @type {boolean} */
|
|
32
|
+
this.debugMode = process.env.MCP_DEBUG === "1" || process.env.MCP_DEBUG === "true";
|
|
33
|
+
/** @type {boolean} */
|
|
34
|
+
this.performanceTracking = process.env.MCP_PERF === "1" || process.env.MCP_PERF === "true" || this.debugMode;
|
|
35
|
+
|
|
36
|
+
// Set log level based on environment
|
|
37
|
+
if (this.debugMode) {
|
|
38
|
+
this.logLevel = "debug";
|
|
39
|
+
} else if (process.env.MCP_LOG_LEVEL) {
|
|
40
|
+
this.logLevel = /** @type {LogLevel} */ (process.env.MCP_LOG_LEVEL);
|
|
41
|
+
} else {
|
|
42
|
+
this.logLevel = "info";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Check if we should log at this level
|
|
48
|
+
* @param {LogLevel} level - Log level to check
|
|
49
|
+
* @returns {boolean} Whether to log
|
|
50
|
+
*/
|
|
51
|
+
shouldLog(level) {
|
|
52
|
+
/** @type {Record<LogLevel, number>} */
|
|
53
|
+
const levels = {
|
|
54
|
+
debug: 0,
|
|
55
|
+
info: 1,
|
|
56
|
+
warn: 2,
|
|
57
|
+
error: 3,
|
|
58
|
+
};
|
|
59
|
+
return levels[level] >= levels[this.logLevel];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Format log message
|
|
64
|
+
* @param {LogContext} context - Log context
|
|
65
|
+
* @returns {string} Formatted log message
|
|
66
|
+
*/
|
|
67
|
+
formatLog(context) {
|
|
68
|
+
const { timestamp, level, message, duration, toolName, requestId } = context;
|
|
69
|
+
|
|
70
|
+
let logMessage = `[MCP-${level.toUpperCase()}] ${timestamp} ${message}`;
|
|
71
|
+
|
|
72
|
+
if (toolName) {
|
|
73
|
+
logMessage += ` [tool:${toolName}]`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (requestId) {
|
|
77
|
+
logMessage += ` [req:${requestId}]`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (duration !== undefined) {
|
|
81
|
+
logMessage += ` (${duration}ms)`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return logMessage;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Internal log method
|
|
89
|
+
* @param {LogLevel} level - Log level
|
|
90
|
+
* @param {string} message - Log message
|
|
91
|
+
* @param {any} [data] - Additional data
|
|
92
|
+
* @param {LogMeta} [meta] - Metadata
|
|
93
|
+
*/
|
|
94
|
+
log(level, message, data, meta) {
|
|
95
|
+
if (!this.shouldLog(level)) return;
|
|
96
|
+
|
|
97
|
+
/** @type {LogContext} */
|
|
98
|
+
const context = {
|
|
99
|
+
timestamp: new Date().toISOString(),
|
|
100
|
+
level,
|
|
101
|
+
message,
|
|
102
|
+
data,
|
|
103
|
+
...meta,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const logMessage = this.formatLog(context);
|
|
107
|
+
|
|
108
|
+
// Output to stderr (MCP requirement - stdout is reserved for JSON protocol)
|
|
109
|
+
const output = console.error;
|
|
110
|
+
|
|
111
|
+
if (data && this.debugMode) {
|
|
112
|
+
output(logMessage);
|
|
113
|
+
output("Data:", typeof data === "string" ? data : JSON.stringify(data, null, 2));
|
|
114
|
+
} else {
|
|
115
|
+
output(logMessage);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Log debug message
|
|
121
|
+
* @param {string} message - Debug message
|
|
122
|
+
* @param {any} [data] - Additional data
|
|
123
|
+
* @param {LogMeta} [meta] - Metadata
|
|
124
|
+
*/
|
|
125
|
+
debug(message, data, meta) {
|
|
126
|
+
this.log("debug", message, data, meta);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Log info message
|
|
131
|
+
* @param {string} message - Info message
|
|
132
|
+
* @param {any} [data] - Additional data
|
|
133
|
+
* @param {LogMeta} [meta] - Metadata
|
|
134
|
+
*/
|
|
135
|
+
info(message, data, meta) {
|
|
136
|
+
this.log("info", message, data, meta);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Log warning message
|
|
141
|
+
* @param {string} message - Warning message
|
|
142
|
+
* @param {any} [data] - Additional data
|
|
143
|
+
* @param {LogMeta} [meta] - Metadata
|
|
144
|
+
*/
|
|
145
|
+
warn(message, data, meta) {
|
|
146
|
+
this.log("warn", message, data, meta);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Log error message
|
|
151
|
+
* @param {string} message - Error message
|
|
152
|
+
* @param {any} [error] - Error object or data
|
|
153
|
+
* @param {LogMeta} [meta] - Metadata
|
|
154
|
+
*/
|
|
155
|
+
error(message, error, meta) {
|
|
156
|
+
this.log("error", message, error, meta);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Start performance timer
|
|
161
|
+
* @param {string} operation - Operation name
|
|
162
|
+
* @returns {Function} Function to call when operation completes
|
|
163
|
+
*/
|
|
164
|
+
startTimer(operation) {
|
|
165
|
+
if (!this.performanceTracking) {
|
|
166
|
+
return () => { }; // No-op if performance tracking disabled
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const startTime = Date.now();
|
|
170
|
+
return () => {
|
|
171
|
+
const duration = Date.now() - startTime;
|
|
172
|
+
this.debug(`${operation} completed`, undefined, { duration });
|
|
173
|
+
return duration;
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Time an async operation
|
|
179
|
+
* @template T
|
|
180
|
+
* @param {string} operation - Operation name
|
|
181
|
+
* @param {Promise<T>} promise - Promise to time
|
|
182
|
+
* @param {string} [toolName] - Tool name
|
|
183
|
+
* @returns {Promise<T>} The original promise
|
|
184
|
+
*/
|
|
185
|
+
timeAsync(operation, promise, toolName) {
|
|
186
|
+
if (!this.performanceTracking) {
|
|
187
|
+
return promise;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const startTime = Date.now();
|
|
191
|
+
return promise
|
|
192
|
+
.then((result) => {
|
|
193
|
+
const duration = Date.now() - startTime;
|
|
194
|
+
this.info(`${operation} completed successfully`, undefined, { duration, toolName });
|
|
195
|
+
return result;
|
|
196
|
+
})
|
|
197
|
+
.catch((error) => {
|
|
198
|
+
const duration = Date.now() - startTime;
|
|
199
|
+
this.error(`${operation} failed`, error, { duration, toolName });
|
|
200
|
+
throw error;
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Trace MCP request
|
|
206
|
+
* @param {string} method - MCP method
|
|
207
|
+
* @param {any} [params] - Request parameters
|
|
208
|
+
* @param {string} [requestId] - Request ID
|
|
209
|
+
*/
|
|
210
|
+
traceRequest(method, params, requestId) {
|
|
211
|
+
this.debug(`MCP Request: ${method}`, params, { requestId });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Trace MCP response
|
|
216
|
+
* @param {string} method - MCP method
|
|
217
|
+
* @param {any} [result] - Response result
|
|
218
|
+
* @param {string} [requestId] - Request ID
|
|
219
|
+
*/
|
|
220
|
+
traceResponse(method, result, requestId) {
|
|
221
|
+
this.debug(`MCP Response: ${method}`, result, { requestId });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Trace MCP error
|
|
226
|
+
* @param {string} method - MCP method
|
|
227
|
+
* @param {any} error - Error object
|
|
228
|
+
* @param {string} [requestId] - Request ID
|
|
229
|
+
*/
|
|
230
|
+
traceError(method, error, requestId) {
|
|
231
|
+
this.error(`MCP Error: ${method}`, error, { requestId });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Log server startup
|
|
236
|
+
* @param {string} serverName - Server name
|
|
237
|
+
* @param {string} transport - Transport type
|
|
238
|
+
* @param {number} toolCount - Number of tools
|
|
239
|
+
*/
|
|
240
|
+
serverStarted(serverName, transport, toolCount) {
|
|
241
|
+
this.info(`MCP server "${serverName}" started`, {
|
|
242
|
+
transport,
|
|
243
|
+
toolCount,
|
|
244
|
+
debugMode: this.debugMode,
|
|
245
|
+
performanceTracking: this.performanceTracking,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Log server shutdown
|
|
251
|
+
* @param {string} reason - Shutdown reason
|
|
252
|
+
*/
|
|
253
|
+
serverShutdown(reason) {
|
|
254
|
+
this.info(`MCP server shutting down: ${reason}`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Log tool execution start
|
|
259
|
+
* @param {string} toolName - Tool name
|
|
260
|
+
* @param {any} args - Tool arguments
|
|
261
|
+
* @param {string} [requestId] - Request ID
|
|
262
|
+
*/
|
|
263
|
+
toolExecutionStart(toolName, args, requestId) {
|
|
264
|
+
this.debug(`Tool execution started: ${toolName}`, this.debugMode ? args : undefined, { toolName, requestId });
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Log tool execution completion
|
|
269
|
+
* @param {string} toolName - Tool name
|
|
270
|
+
* @param {number} duration - Execution duration
|
|
271
|
+
* @param {string} [requestId] - Request ID
|
|
272
|
+
*/
|
|
273
|
+
toolExecutionEnd(toolName, duration, requestId) {
|
|
274
|
+
this.info(`Tool execution completed: ${toolName}`, undefined, { toolName, duration, requestId });
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Log tool execution error
|
|
279
|
+
* @param {string} toolName - Tool name
|
|
280
|
+
* @param {any} error - Error object
|
|
281
|
+
* @param {number} duration - Execution duration
|
|
282
|
+
* @param {string} [requestId] - Request ID
|
|
283
|
+
*/
|
|
284
|
+
toolExecutionError(toolName, error, duration, requestId) {
|
|
285
|
+
this.error(`Tool execution failed: ${toolName}`, error, { toolName, duration, requestId });
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Export singleton instance
|
|
290
|
+
export const logger = new MCPLogger();
|