converse-mcp-server 1.0.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,555 @@
1
+ /**
2
+ * Error Handler Utility
3
+ *
4
+ * Centralized error handling and response formatting for consistent error management
5
+ * across all modules. Provides structured error responses and proper error propagation.
6
+ */
7
+
8
+ import { createLogger } from './logger.js';
9
+
10
+ const logger = createLogger('errorHandler');
11
+
12
+ /**
13
+ * Standard error codes used throughout the application
14
+ */
15
+ export const ERROR_CODES = {
16
+ // Configuration errors
17
+ CONFIGURATION_ERROR: 'CONFIGURATION_ERROR',
18
+ MISSING_CONFIG: 'MISSING_CONFIG',
19
+ INVALID_CONFIG: 'INVALID_CONFIG',
20
+
21
+ // Provider errors
22
+ PROVIDER_ERROR: 'PROVIDER_ERROR',
23
+ PROVIDER_NOT_FOUND: 'PROVIDER_NOT_FOUND',
24
+ PROVIDER_UNAVAILABLE: 'PROVIDER_UNAVAILABLE',
25
+ INVALID_API_KEY: 'INVALID_API_KEY',
26
+ API_QUOTA_EXCEEDED: 'API_QUOTA_EXCEEDED',
27
+ API_RATE_LIMIT: 'API_RATE_LIMIT',
28
+
29
+ // Tool errors
30
+ TOOL_ERROR: 'TOOL_ERROR',
31
+ TOOL_NOT_FOUND: 'TOOL_NOT_FOUND',
32
+ INVALID_TOOL_ARGS: 'INVALID_TOOL_ARGS',
33
+ TOOL_EXECUTION_FAILED: 'TOOL_EXECUTION_FAILED',
34
+
35
+ // Router errors
36
+ ROUTER_ERROR: 'ROUTER_ERROR',
37
+ INVALID_REQUEST: 'INVALID_REQUEST',
38
+ REQUEST_VALIDATION_FAILED: 'REQUEST_VALIDATION_FAILED',
39
+
40
+ // Context processing errors
41
+ CONTEXT_ERROR: 'CONTEXT_ERROR',
42
+ FILE_NOT_FOUND: 'FILE_NOT_FOUND',
43
+ FILE_ACCESS_DENIED: 'FILE_ACCESS_DENIED',
44
+ INVALID_FILE_TYPE: 'INVALID_FILE_TYPE',
45
+ FILE_TOO_LARGE: 'FILE_TOO_LARGE',
46
+
47
+ // Continuation store errors
48
+ CONTINUATION_ERROR: 'CONTINUATION_ERROR',
49
+ INVALID_CONTINUATION_ID: 'INVALID_CONTINUATION_ID',
50
+ CONTINUATION_NOT_FOUND: 'CONTINUATION_NOT_FOUND',
51
+
52
+ // Generic errors
53
+ UNKNOWN_ERROR: 'UNKNOWN_ERROR',
54
+ INTERNAL_ERROR: 'INTERNAL_ERROR',
55
+ VALIDATION_ERROR: 'VALIDATION_ERROR',
56
+ TIMEOUT_ERROR: 'TIMEOUT_ERROR',
57
+ NETWORK_ERROR: 'NETWORK_ERROR'
58
+ };
59
+
60
+ /**
61
+ * Base error class for structured error handling
62
+ */
63
+ export class ConverseMCPError extends Error {
64
+ constructor(message, code = ERROR_CODES.UNKNOWN_ERROR, details = {}, statusCode = 500) {
65
+ super(message);
66
+ this.name = 'ConverseMCPError';
67
+ this.code = code;
68
+ this.details = details;
69
+ this.statusCode = statusCode;
70
+ this.timestamp = new Date().toISOString();
71
+
72
+ // Capture stack trace
73
+ if (Error.captureStackTrace) {
74
+ Error.captureStackTrace(this, ConverseMCPError);
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Convert error to JSON-serializable object
80
+ * @returns {object} Serializable error object
81
+ */
82
+ toJSON() {
83
+ return {
84
+ name: this.name,
85
+ message: this.message,
86
+ code: this.code,
87
+ details: this.details,
88
+ statusCode: this.statusCode,
89
+ timestamp: this.timestamp,
90
+ stack: this.stack
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Create MCP-compatible error response
96
+ * @returns {object} MCP error response
97
+ */
98
+ toMCPResponse() {
99
+ return {
100
+ content: [
101
+ {
102
+ type: 'text',
103
+ text: this.message
104
+ }
105
+ ],
106
+ isError: true,
107
+ error: {
108
+ type: this.name,
109
+ code: this.code,
110
+ message: this.message,
111
+ details: this.details,
112
+ timestamp: this.timestamp
113
+ }
114
+ };
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Provider-specific error class
120
+ */
121
+ export class ProviderError extends ConverseMCPError {
122
+ constructor(message, code = ERROR_CODES.PROVIDER_ERROR, details = {}, provider = 'unknown') {
123
+ super(message, code, { ...details, provider }, 503);
124
+ this.name = 'ProviderError';
125
+ this.provider = provider;
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Tool-specific error class
131
+ */
132
+ export class ToolError extends ConverseMCPError {
133
+ constructor(message, code = ERROR_CODES.TOOL_ERROR, details = {}, toolName = 'unknown') {
134
+ super(message, code, { ...details, toolName }, 400);
135
+ this.name = 'ToolError';
136
+ this.toolName = toolName;
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Configuration error class
142
+ */
143
+ export class ConfigurationError extends ConverseMCPError {
144
+ constructor(message, code = ERROR_CODES.CONFIGURATION_ERROR, details = {}) {
145
+ super(message, code, details, 500);
146
+ this.name = 'ConfigurationError';
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Validation error class
152
+ */
153
+ export class ValidationError extends ConverseMCPError {
154
+ constructor(message, code = ERROR_CODES.VALIDATION_ERROR, details = {}) {
155
+ super(message, code, details, 400);
156
+ this.name = 'ValidationError';
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Context processing error class
162
+ */
163
+ export class ContextError extends ConverseMCPError {
164
+ constructor(message, code = ERROR_CODES.CONTEXT_ERROR, details = {}) {
165
+ super(message, code, details, 400);
166
+ this.name = 'ContextError';
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Wrap and enhance existing errors
172
+ * @param {Error} originalError - Original error
173
+ * @param {string} message - New error message
174
+ * @param {string} code - Error code
175
+ * @param {object} details - Additional details
176
+ * @returns {ConverseMCPError} Enhanced error
177
+ */
178
+ export function wrapError(originalError, message, code = ERROR_CODES.UNKNOWN_ERROR, details = {}) {
179
+ const enhancedDetails = {
180
+ ...details,
181
+ originalError: {
182
+ name: originalError.name,
183
+ message: originalError.message,
184
+ code: originalError.code,
185
+ stack: originalError.stack
186
+ }
187
+ };
188
+
189
+ const wrappedError = new ConverseMCPError(message, code, enhancedDetails);
190
+ wrappedError.cause = originalError;
191
+
192
+ return wrappedError;
193
+ }
194
+
195
+ /**
196
+ * Handle async function errors with proper logging
197
+ * @param {function} fn - Async function to wrap
198
+ * @param {string} operation - Operation name for logging
199
+ * @param {object} context - Additional context
200
+ * @returns {function} Wrapped function
201
+ */
202
+ export function withErrorHandler(fn, operation = 'unknown', context = {}) {
203
+ return async (...args) => {
204
+ const operationLogger = logger.operation(operation);
205
+
206
+ try {
207
+ operationLogger.debug('Starting operation', { data: context });
208
+ const result = await fn(...args);
209
+ operationLogger.debug('Operation completed successfully');
210
+ return result;
211
+ } catch (error) {
212
+ operationLogger.error('Operation failed', {
213
+ error,
214
+ data: { args: args.length, context }
215
+ });
216
+
217
+ // Re-throw enhanced error if it's already structured
218
+ if (error instanceof ConverseMCPError) {
219
+ throw error;
220
+ }
221
+
222
+ // Wrap unknown errors
223
+ throw wrapError(error, `${operation} failed: ${error.message}`, ERROR_CODES.INTERNAL_ERROR, context);
224
+ }
225
+ };
226
+ }
227
+
228
+ /**
229
+ * Create MCP-compatible error response
230
+ * @param {Error} error - Error object
231
+ * @param {string} toolName - Tool name (optional)
232
+ * @param {object} context - Additional context
233
+ * @returns {object} MCP error response
234
+ */
235
+ export function createMCPErrorResponse(error, toolName = null, context = {}) {
236
+ // If it's already a structured error, use its MCP response
237
+ if (error instanceof ConverseMCPError) {
238
+ const response = error.toMCPResponse();
239
+ if (toolName) {
240
+ response.error.toolName = toolName;
241
+ }
242
+ response.error.context = context;
243
+ return response;
244
+ }
245
+
246
+ // Create structured response for unknown errors
247
+ const errorCode = error.code || ERROR_CODES.UNKNOWN_ERROR;
248
+ const message = toolName ? `Error in ${toolName}: ${error.message}` : error.message;
249
+
250
+ return {
251
+ content: [
252
+ {
253
+ type: 'text',
254
+ text: message
255
+ }
256
+ ],
257
+ isError: true,
258
+ error: {
259
+ type: error.name || 'Error',
260
+ code: errorCode,
261
+ message: error.message,
262
+ toolName,
263
+ context,
264
+ timestamp: new Date().toISOString(),
265
+ ...(process.env.NODE_ENV === 'development' && error.stack && { stack: error.stack })
266
+ }
267
+ };
268
+ }
269
+
270
+ /**
271
+ * Determine if error is recoverable
272
+ * @param {Error} error - Error to check
273
+ * @returns {boolean} True if error is recoverable
274
+ */
275
+ export function isRecoverableError(error) {
276
+ if (error instanceof ConverseMCPError) {
277
+ return error.statusCode < 500 && error.code !== ERROR_CODES.INTERNAL_ERROR;
278
+ }
279
+
280
+ // Check for known recoverable error patterns
281
+ const recoverablePatterns = [
282
+ /network/i,
283
+ /timeout/i,
284
+ /rate limit/i,
285
+ /quota/i,
286
+ /temporary/i
287
+ ];
288
+
289
+ return recoverablePatterns.some(pattern => pattern.test(error.message));
290
+ }
291
+
292
+ /**
293
+ * Log error with appropriate level based on severity
294
+ * @param {Error} error - Error to log
295
+ * @param {string} operation - Operation context
296
+ * @param {object} metadata - Additional metadata
297
+ */
298
+ export function logError(error, operation = 'unknown', metadata = {}) {
299
+ const operationLogger = logger.operation(operation);
300
+
301
+ if (error instanceof ConverseMCPError) {
302
+ if (error.statusCode >= 500) {
303
+ operationLogger.error('Internal error occurred', { error, data: metadata });
304
+ } else if (error.statusCode >= 400) {
305
+ operationLogger.warn('Client error occurred', { error, data: metadata });
306
+ } else {
307
+ operationLogger.info('Handled error occurred', { error, data: metadata });
308
+ }
309
+ } else {
310
+ operationLogger.error('Unhandled error occurred', { error, data: metadata });
311
+ }
312
+ }
313
+
314
+ /**
315
+ * Error aggregation for batch operations
316
+ */
317
+ export class ErrorAggregator {
318
+ constructor(operation = 'batch-operation') {
319
+ this.operation = operation;
320
+ this.errors = [];
321
+ this.successes = [];
322
+ this.logger = logger.operation(operation);
323
+ }
324
+
325
+ /**
326
+ * Add success result
327
+ * @param {any} result - Success result
328
+ * @param {string} identifier - Result identifier
329
+ */
330
+ addSuccess(result, identifier = null) {
331
+ this.successes.push({ result, identifier, timestamp: new Date().toISOString() });
332
+ }
333
+
334
+ /**
335
+ * Add error result
336
+ * @param {Error} error - Error that occurred
337
+ * @param {string} identifier - Error identifier
338
+ */
339
+ addError(error, identifier = null) {
340
+ this.errors.push({ error, identifier, timestamp: new Date().toISOString() });
341
+ this.logger.warn('Batch operation error', {
342
+ error,
343
+ data: { identifier, totalErrors: this.errors.length }
344
+ });
345
+ }
346
+
347
+ /**
348
+ * Get summary of results
349
+ * @returns {object} Results summary
350
+ */
351
+ getSummary() {
352
+ return {
353
+ operation: this.operation,
354
+ total: this.errors.length + this.successes.length,
355
+ successes: this.successes.length,
356
+ errors: this.errors.length,
357
+ hasErrors: this.errors.length > 0,
358
+ successRate: this.successes.length / (this.errors.length + this.successes.length)
359
+ };
360
+ }
361
+
362
+ /**
363
+ * Throw aggregated error if there are any errors
364
+ * @param {string} message - Error message
365
+ */
366
+ throwIfErrors(message = null) {
367
+ if (this.errors.length > 0) {
368
+ const defaultMessage = `${this.operation} completed with ${this.errors.length} errors`;
369
+ const errorMessage = message || defaultMessage;
370
+
371
+ const aggregatedError = new ConverseMCPError(
372
+ errorMessage,
373
+ ERROR_CODES.INTERNAL_ERROR,
374
+ {
375
+ summary: this.getSummary(),
376
+ errors: this.errors.map(e => ({
377
+ identifier: e.identifier,
378
+ error: e.error.message,
379
+ code: e.error.code
380
+ }))
381
+ }
382
+ );
383
+
384
+ throw aggregatedError;
385
+ }
386
+ }
387
+
388
+ /**
389
+ * Log final summary
390
+ */
391
+ logSummary() {
392
+ const summary = this.getSummary();
393
+
394
+ if (summary.hasErrors) {
395
+ this.logger.warn('Batch operation completed with errors', { data: summary });
396
+ } else {
397
+ this.logger.info('Batch operation completed successfully', { data: summary });
398
+ }
399
+ }
400
+ }
401
+
402
+ /**
403
+ * Retry utility with exponential backoff
404
+ * @param {function} fn - Function to retry
405
+ * @param {object} options - Retry options
406
+ * @returns {Promise} Function result
407
+ */
408
+ export async function retryWithBackoff(fn, options = {}) {
409
+ const {
410
+ retries = 3,
411
+ delay = 1000,
412
+ backoffFactor = 2,
413
+ maxDelay = 10000,
414
+ operation = 'retry-operation'
415
+ } = options;
416
+
417
+ const operationLogger = logger.operation(operation);
418
+ let lastError;
419
+
420
+ for (let attempt = 0; attempt <= retries; attempt++) {
421
+ try {
422
+ if (attempt > 0) {
423
+ operationLogger.debug(`Retry attempt ${attempt}/${retries}`);
424
+ }
425
+
426
+ return await fn();
427
+
428
+ } catch (error) {
429
+ lastError = error;
430
+
431
+ if (attempt === retries) {
432
+ operationLogger.error('All retry attempts failed', {
433
+ error,
434
+ data: { attempts: attempt + 1, maxRetries: retries }
435
+ });
436
+ break;
437
+ }
438
+
439
+ if (!isRecoverableError(error)) {
440
+ operationLogger.warn('Non-recoverable error, stopping retries', { error });
441
+ break;
442
+ }
443
+
444
+ const currentDelay = Math.min(delay * Math.pow(backoffFactor, attempt), maxDelay);
445
+ operationLogger.debug(`Retrying in ${currentDelay}ms`, {
446
+ error: error.message,
447
+ data: { attempt: attempt + 1, delay: currentDelay }
448
+ });
449
+
450
+ await new Promise(resolve => setTimeout(resolve, currentDelay));
451
+ }
452
+ }
453
+
454
+ throw lastError;
455
+ }
456
+
457
+ /**
458
+ * Circuit breaker pattern implementation
459
+ */
460
+ export class CircuitBreaker {
461
+ constructor(operation, options = {}) {
462
+ this.operation = operation;
463
+ this.failureThreshold = options.failureThreshold || 5;
464
+ this.resetTimeout = options.resetTimeout || 60000; // 1 minute
465
+ this.monitorTimeout = options.monitorTimeout || 10000; // 10 seconds
466
+
467
+ this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
468
+ this.failures = 0;
469
+ this.lastFailureTime = null;
470
+ this.nextAttempt = null;
471
+
472
+ this.logger = logger.operation(`circuit-breaker:${operation}`);
473
+ }
474
+
475
+ /**
476
+ * Execute function with circuit breaker protection
477
+ * @param {function} fn - Function to execute
478
+ * @returns {Promise} Function result
479
+ */
480
+ async execute(fn) {
481
+ if (this.state === 'OPEN') {
482
+ if (Date.now() < this.nextAttempt) {
483
+ throw new ConverseMCPError(
484
+ `Circuit breaker is OPEN for ${this.operation}`,
485
+ ERROR_CODES.PROVIDER_UNAVAILABLE,
486
+ { state: this.state, nextAttempt: this.nextAttempt }
487
+ );
488
+ }
489
+
490
+ this.state = 'HALF_OPEN';
491
+ this.logger.info('Circuit breaker transitioning to HALF_OPEN');
492
+ }
493
+
494
+ try {
495
+ const result = await fn();
496
+
497
+ if (this.state === 'HALF_OPEN') {
498
+ this.reset();
499
+ }
500
+
501
+ return result;
502
+
503
+ } catch (error) {
504
+ this.recordFailure();
505
+ throw error;
506
+ }
507
+ }
508
+
509
+ /**
510
+ * Record a failure
511
+ */
512
+ recordFailure() {
513
+ this.failures++;
514
+ this.lastFailureTime = Date.now();
515
+
516
+ if (this.failures >= this.failureThreshold) {
517
+ this.state = 'OPEN';
518
+ this.nextAttempt = Date.now() + this.resetTimeout;
519
+
520
+ this.logger.warn('Circuit breaker opened due to failures', {
521
+ data: {
522
+ failures: this.failures,
523
+ threshold: this.failureThreshold,
524
+ resetTime: new Date(this.nextAttempt).toISOString()
525
+ }
526
+ });
527
+ }
528
+ }
529
+
530
+ /**
531
+ * Reset circuit breaker
532
+ */
533
+ reset() {
534
+ this.state = 'CLOSED';
535
+ this.failures = 0;
536
+ this.lastFailureTime = null;
537
+ this.nextAttempt = null;
538
+
539
+ this.logger.info('Circuit breaker reset to CLOSED');
540
+ }
541
+
542
+ /**
543
+ * Get current status
544
+ * @returns {object} Circuit breaker status
545
+ */
546
+ getStatus() {
547
+ return {
548
+ operation: this.operation,
549
+ state: this.state,
550
+ failures: this.failures,
551
+ lastFailureTime: this.lastFailureTime,
552
+ nextAttempt: this.nextAttempt
553
+ };
554
+ }
555
+ }