@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.
@@ -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();