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,450 @@
1
+ /**
2
+ * Logger Utility
3
+ *
4
+ * Structured logging system with different log levels and development/production modes.
5
+ * Provides consistent logging interface across all modules with proper error handling.
6
+ */
7
+
8
+ import { inspect } from 'util';
9
+
10
+ /**
11
+ * Check if console output should be suppressed
12
+ * @returns {boolean} True if console output should be suppressed
13
+ */
14
+ function shouldSuppressConsole() {
15
+ return process.env.LOG_LEVEL === 'silent' ||
16
+ process.env.NODE_ENV === 'test' ||
17
+ process.env.MCP_TRANSPORT === 'stdio';
18
+ }
19
+
20
+ /**
21
+ * Log levels in order of severity (lower number = higher severity)
22
+ */
23
+ const LOG_LEVELS = {
24
+ silent: -1,
25
+ error: 0,
26
+ warn: 1,
27
+ info: 2,
28
+ debug: 3,
29
+ trace: 4
30
+ };
31
+
32
+ /**
33
+ * Logger configuration
34
+ */
35
+ let loggerConfig = {
36
+ level: process.env.LOG_LEVEL || 'info',
37
+ isDevelopment: process.env.NODE_ENV === 'development',
38
+ enableColors: process.env.NODE_ENV !== 'production',
39
+ enableTimestamps: true,
40
+ enableStackTrace: process.env.NODE_ENV === 'development'
41
+ };
42
+
43
+ /**
44
+ * ANSI color codes for console output
45
+ */
46
+ const COLORS = {
47
+ reset: '\x1b[0m',
48
+ bright: '\x1b[1m',
49
+ dim: '\x1b[2m',
50
+ red: '\x1b[31m',
51
+ green: '\x1b[32m',
52
+ yellow: '\x1b[33m',
53
+ blue: '\x1b[34m',
54
+ magenta: '\x1b[35m',
55
+ cyan: '\x1b[36m',
56
+ white: '\x1b[37m',
57
+ gray: '\x1b[90m'
58
+ };
59
+
60
+ /**
61
+ * Log level color mapping
62
+ */
63
+ const LEVEL_COLORS = {
64
+ error: COLORS.red,
65
+ warn: COLORS.yellow,
66
+ info: COLORS.green,
67
+ debug: COLORS.blue,
68
+ trace: COLORS.gray
69
+ };
70
+
71
+ /**
72
+ * Format timestamp for log entries
73
+ * @returns {string} Formatted timestamp
74
+ */
75
+ function formatTimestamp() {
76
+ return new Date().toISOString();
77
+ }
78
+
79
+ /**
80
+ * Format log level for display
81
+ * @param {string} level - Log level
82
+ * @returns {string} Formatted log level
83
+ */
84
+ function formatLevel(level) {
85
+ const upperLevel = level.toUpperCase().padEnd(5);
86
+ if (loggerConfig.enableColors) {
87
+ const color = LEVEL_COLORS[level] || COLORS.white;
88
+ return `${color}${upperLevel}${COLORS.reset}`;
89
+ }
90
+ return upperLevel;
91
+ }
92
+
93
+ /**
94
+ * Format context information
95
+ * @param {string} module - Module name
96
+ * @param {string} operation - Operation name
97
+ * @returns {string} Formatted context
98
+ */
99
+ function formatContext(module, operation) {
100
+ if (!module && !operation) return '';
101
+
102
+ const context = [module, operation].filter(Boolean).join(':');
103
+ if (loggerConfig.enableColors) {
104
+ return `${COLORS.dim}[${context}]${COLORS.reset}`;
105
+ }
106
+ return `[${context}]`;
107
+ }
108
+
109
+ /**
110
+ * Format error objects for logging
111
+ * @param {Error} error - Error object
112
+ * @returns {object} Formatted error information
113
+ */
114
+ function formatError(error) {
115
+ if (!(error instanceof Error)) {
116
+ return { error: error };
117
+ }
118
+
119
+ const errorInfo = {
120
+ name: error.name,
121
+ message: error.message,
122
+ code: error.code,
123
+ details: error.details
124
+ };
125
+
126
+ if (loggerConfig.enableStackTrace && error.stack) {
127
+ errorInfo.stack = error.stack;
128
+ }
129
+
130
+ return errorInfo;
131
+ }
132
+
133
+ /**
134
+ * Format log data for output
135
+ * @param {any} data - Data to format
136
+ * @returns {string} Formatted data
137
+ */
138
+ function formatData(data) {
139
+ if (data === null || data === undefined) {
140
+ return '';
141
+ }
142
+
143
+ if (typeof data === 'string') {
144
+ return data;
145
+ }
146
+
147
+ if (data instanceof Error) {
148
+ const errorInfo = formatError(data);
149
+ if (loggerConfig.isDevelopment) {
150
+ return inspect(errorInfo, { depth: 3, colors: loggerConfig.enableColors });
151
+ }
152
+ return JSON.stringify(errorInfo);
153
+ }
154
+
155
+ if (typeof data === 'object') {
156
+ if (loggerConfig.isDevelopment) {
157
+ return inspect(data, { depth: 2, colors: loggerConfig.enableColors });
158
+ }
159
+ return JSON.stringify(data);
160
+ }
161
+
162
+ return String(data);
163
+ }
164
+
165
+ /**
166
+ * Check if log level should be output
167
+ * @param {string} level - Log level to check
168
+ * @returns {boolean} True if should log
169
+ */
170
+ function shouldLog(level) {
171
+ const currentLevel = LOG_LEVELS[loggerConfig.level] || LOG_LEVELS.info;
172
+ const messageLevel = LOG_LEVELS[level] || LOG_LEVELS.info;
173
+ return messageLevel <= currentLevel;
174
+ }
175
+
176
+ /**
177
+ * Core logging function
178
+ * @param {string} level - Log level
179
+ * @param {string} message - Log message
180
+ * @param {object} metadata - Additional metadata
181
+ */
182
+ function log(level, message, metadata = {}) {
183
+ if (!shouldLog(level)) {
184
+ return;
185
+ }
186
+
187
+ const timestamp = loggerConfig.enableTimestamps ? formatTimestamp() : null;
188
+ const formattedLevel = formatLevel(level);
189
+ const context = formatContext(metadata.module, metadata.operation);
190
+
191
+ // Build log parts
192
+ const parts = [
193
+ timestamp,
194
+ formattedLevel,
195
+ context,
196
+ message
197
+ ].filter(Boolean);
198
+
199
+ // Output main log line
200
+ const logLine = parts.join(' ');
201
+
202
+ if (!shouldSuppressConsole()) {
203
+ if (level === 'error') {
204
+ console.error(logLine);
205
+ } else {
206
+ console.log(logLine);
207
+ }
208
+ }
209
+
210
+ // Output additional data if provided
211
+ if (metadata.data !== undefined) {
212
+ const formattedData = formatData(metadata.data);
213
+ if (formattedData) {
214
+ if (!shouldSuppressConsole()) {
215
+ if (level === 'error') {
216
+ console.error(formattedData);
217
+ } else {
218
+ console.log(formattedData);
219
+ }
220
+ }
221
+ }
222
+ }
223
+
224
+ // Output error details if provided
225
+ if (metadata.error && metadata.error instanceof Error) {
226
+ const errorInfo = formatError(metadata.error);
227
+ const formattedError = formatData(errorInfo);
228
+ if (level === 'error') {
229
+ if (!shouldSuppressConsole()) {
230
+ console.error(formattedError);
231
+ }
232
+ } else {
233
+ if (!shouldSuppressConsole()) {
234
+ console.log(formattedError);
235
+ }
236
+ }
237
+ }
238
+ }
239
+
240
+ /**
241
+ * Create logger instance for a specific module
242
+ * @param {string} moduleName - Name of the module
243
+ * @returns {object} Logger instance
244
+ */
245
+ export function createLogger(moduleName) {
246
+ return {
247
+ /**
248
+ * Log error message
249
+ * @param {string} message - Error message
250
+ * @param {object} metadata - Additional metadata
251
+ */
252
+ error(message, metadata = {}) {
253
+ log('error', message, { ...metadata, module: moduleName });
254
+ },
255
+
256
+ /**
257
+ * Log warning message
258
+ * @param {string} message - Warning message
259
+ * @param {object} metadata - Additional metadata
260
+ */
261
+ warn(message, metadata = {}) {
262
+ log('warn', message, { ...metadata, module: moduleName });
263
+ },
264
+
265
+ /**
266
+ * Log info message
267
+ * @param {string} message - Info message
268
+ * @param {object} metadata - Additional metadata
269
+ */
270
+ info(message, metadata = {}) {
271
+ log('info', message, { ...metadata, module: moduleName });
272
+ },
273
+
274
+ /**
275
+ * Log debug message
276
+ * @param {string} message - Debug message
277
+ * @param {object} metadata - Additional metadata
278
+ */
279
+ debug(message, metadata = {}) {
280
+ log('debug', message, { ...metadata, module: moduleName });
281
+ },
282
+
283
+ /**
284
+ * Log trace message
285
+ * @param {string} message - Trace message
286
+ * @param {object} metadata - Additional metadata
287
+ */
288
+ trace(message, metadata = {}) {
289
+ log('trace', message, { ...metadata, module: moduleName });
290
+ },
291
+
292
+ /**
293
+ * Log with custom operation context
294
+ * @param {string} operation - Operation name
295
+ * @returns {object} Logger with operation context
296
+ */
297
+ operation(operation) {
298
+ return {
299
+ error: (message, metadata = {}) => log('error', message, { ...metadata, module: moduleName, operation }),
300
+ warn: (message, metadata = {}) => log('warn', message, { ...metadata, module: moduleName, operation }),
301
+ info: (message, metadata = {}) => log('info', message, { ...metadata, module: moduleName, operation }),
302
+ debug: (message, metadata = {}) => log('debug', message, { ...metadata, module: moduleName, operation }),
303
+ trace: (message, metadata = {}) => log('trace', message, { ...metadata, module: moduleName, operation })
304
+ };
305
+ }
306
+ };
307
+ }
308
+
309
+ /**
310
+ * Configure logger settings
311
+ * @param {object} config - Logger configuration
312
+ */
313
+ export function configureLogger(config) {
314
+ loggerConfig = { ...loggerConfig, ...config };
315
+ }
316
+
317
+ /**
318
+ * Get current logger configuration
319
+ * @returns {object} Current configuration
320
+ */
321
+ export function getLoggerConfig() {
322
+ return { ...loggerConfig };
323
+ }
324
+
325
+ /**
326
+ * Global logger instance
327
+ */
328
+ export const logger = createLogger('global');
329
+
330
+ /**
331
+ * Create structured error for logging
332
+ * @param {string} message - Error message
333
+ * @param {string} code - Error code
334
+ * @param {object} details - Error details
335
+ * @param {Error} originalError - Original error
336
+ * @returns {Error} Structured error
337
+ */
338
+ export function createStructuredError(message, code = 'UNKNOWN_ERROR', details = {}, originalError = null) {
339
+ const error = new Error(message);
340
+ error.code = code;
341
+ error.details = details;
342
+
343
+ if (originalError) {
344
+ error.originalError = originalError;
345
+ error.cause = originalError;
346
+ }
347
+
348
+ return error;
349
+ }
350
+
351
+ /**
352
+ * Log performance timing
353
+ * @param {string} operation - Operation name
354
+ * @param {number} startTime - Start time in milliseconds
355
+ * @param {object} metadata - Additional metadata
356
+ */
357
+ export function logTiming(operation, startTime, metadata = {}) {
358
+ const duration = Date.now() - startTime;
359
+
360
+ if (duration > 1000) {
361
+ logger.warn(`Slow operation: ${operation}`, {
362
+ ...metadata,
363
+ data: { duration: `${duration}ms` }
364
+ });
365
+ } else if (duration > 100) {
366
+ logger.info(`Operation completed: ${operation}`, {
367
+ ...metadata,
368
+ data: { duration: `${duration}ms` }
369
+ });
370
+ } else {
371
+ logger.debug(`Operation completed: ${operation}`, {
372
+ ...metadata,
373
+ data: { duration: `${duration}ms` }
374
+ });
375
+ }
376
+ }
377
+
378
+ /**
379
+ * Performance timer utility
380
+ * @param {string} operation - Operation name
381
+ * @param {string} module - Module name
382
+ * @returns {function} Function to end timing
383
+ */
384
+ export function startTimer(operation, module = 'timer') {
385
+ const startTime = Date.now();
386
+ const moduleLogger = createLogger(module);
387
+
388
+ moduleLogger.debug(`Starting: ${operation}`);
389
+
390
+ return (result = 'completed', metadata = {}) => {
391
+ const duration = Date.now() - startTime;
392
+ moduleLogger.debug(`${result}: ${operation}`, {
393
+ ...metadata,
394
+ data: { duration: `${duration}ms` }
395
+ });
396
+ return duration;
397
+ };
398
+ }
399
+
400
+ /**
401
+ * Log function entry and exit (for debugging)
402
+ * @param {string} functionName - Function name
403
+ * @param {string} module - Module name
404
+ * @returns {function} Function to end logging
405
+ */
406
+ export function logFunction(functionName, module = 'function') {
407
+ const moduleLogger = createLogger(module);
408
+ const startTime = Date.now();
409
+
410
+ moduleLogger.trace(`Entering: ${functionName}`);
411
+
412
+ return (result = 'completed', error = null) => {
413
+ const duration = Date.now() - startTime;
414
+
415
+ if (error) {
416
+ moduleLogger.trace(`Exiting with error: ${functionName}`, {
417
+ data: { duration: `${duration}ms` },
418
+ error
419
+ });
420
+ } else {
421
+ moduleLogger.trace(`Exiting: ${functionName}`, {
422
+ data: { duration: `${duration}ms`, result }
423
+ });
424
+ }
425
+ };
426
+ }
427
+
428
+ /**
429
+ * Express error for consistent error logging across modules
430
+ */
431
+ export class LoggedError extends Error {
432
+ constructor(message, code = 'LOGGED_ERROR', details = {}, logger = null) {
433
+ super(message);
434
+ this.name = 'LoggedError';
435
+ this.code = code;
436
+ this.details = details;
437
+ this.timestamp = new Date().toISOString();
438
+
439
+ // Log the error immediately
440
+ if (logger) {
441
+ logger.error(`${this.name}: ${message}`, {
442
+ data: { code, details }
443
+ });
444
+ } else {
445
+ global.logger?.error(`${this.name}: ${message}`, {
446
+ data: { code, details }
447
+ });
448
+ }
449
+ }
450
+ }
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Token Limiter Utility
3
+ *
4
+ * Implements token limiting for MCP tool responses to prevent excessive output.
5
+ * Based on the Python implementation's token limiting functionality.
6
+ */
7
+
8
+ import { createLogger } from './logger.js';
9
+
10
+ const logger = createLogger('tokenLimiter');
11
+
12
+ /**
13
+ * Simple token estimation based on character count
14
+ * Rough approximation: 1 token ≈ 4 characters for English text
15
+ * @param {string} text - Text to estimate tokens for
16
+ * @returns {number} Estimated token count
17
+ */
18
+ function estimateTokens(text) {
19
+ if (!text || typeof text !== 'string') {
20
+ return 0;
21
+ }
22
+ // Average of 4 characters per token for English text
23
+ return Math.ceil(text.length / 4);
24
+ }
25
+
26
+ /**
27
+ * Truncates text to fit within token limit while preserving structure
28
+ * @param {string} text - Text to truncate
29
+ * @param {number} maxTokens - Maximum allowed tokens
30
+ * @returns {object} Object with truncated text and metadata
31
+ */
32
+ function truncateToTokenLimit(text, maxTokens) {
33
+ if (!text || typeof text !== 'string') {
34
+ return {
35
+ content: text || '',
36
+ truncated: false,
37
+ originalTokens: 0,
38
+ finalTokens: 0,
39
+ truncationReason: null
40
+ };
41
+ }
42
+
43
+ const originalTokens = estimateTokens(text);
44
+
45
+ if (originalTokens <= maxTokens) {
46
+ return {
47
+ content: text,
48
+ truncated: false,
49
+ originalTokens,
50
+ finalTokens: originalTokens,
51
+ truncationReason: null
52
+ };
53
+ }
54
+
55
+ // Calculate maximum characters to keep (with some buffer for safety)
56
+ const maxChars = Math.floor(maxTokens * 3.8); // Slightly less than 4 chars per token
57
+
58
+ // Find a good truncation point (prefer complete sentences or paragraphs)
59
+ let truncationPoint = maxChars;
60
+ const text_substring = text.substring(0, maxChars + 200); // Look ahead a bit
61
+
62
+ // Try to find sentence endings
63
+ const sentenceEndings = ['. ', '.\n', '!\n', '?\n', '! ', '? '];
64
+ let bestTruncation = maxChars;
65
+
66
+ for (const ending of sentenceEndings) {
67
+ const lastIndex = text_substring.lastIndexOf(ending);
68
+ if (lastIndex > maxChars * 0.8 && lastIndex < maxChars) {
69
+ bestTruncation = lastIndex + ending.length;
70
+ break;
71
+ }
72
+ }
73
+
74
+ // If no good sentence ending, try paragraph breaks
75
+ if (bestTruncation === maxChars) {
76
+ const paragraphBreak = text_substring.lastIndexOf('\n\n');
77
+ if (paragraphBreak > maxChars * 0.8 && paragraphBreak < maxChars) {
78
+ bestTruncation = paragraphBreak + 2;
79
+ }
80
+ }
81
+
82
+ // If still no good break, try line breaks
83
+ if (bestTruncation === maxChars) {
84
+ const lineBreak = text_substring.lastIndexOf('\n');
85
+ if (lineBreak > maxChars * 0.9 && lineBreak < maxChars) {
86
+ bestTruncation = lineBreak + 1;
87
+ }
88
+ }
89
+
90
+ const truncatedText = text.substring(0, bestTruncation);
91
+ const finalTokens = estimateTokens(truncatedText);
92
+
93
+ const truncationMessage = `\n\n[Response truncated due to length. Original: ~${originalTokens} tokens, Truncated: ~${finalTokens} tokens, Limit: ${maxTokens} tokens]`;
94
+
95
+ return {
96
+ content: truncatedText + truncationMessage,
97
+ truncated: true,
98
+ originalTokens,
99
+ finalTokens: finalTokens + estimateTokens(truncationMessage),
100
+ truncationReason: 'Exceeded maximum token limit'
101
+ };
102
+ }
103
+
104
+ /**
105
+ * Validates and applies token limits to MCP tool responses
106
+ * @param {any} response - The response content to validate
107
+ * @param {number} maxTokens - Maximum allowed tokens
108
+ * @returns {object} Processed response with token limiting applied
109
+ */
110
+ export function applyTokenLimit(response, maxTokens) {
111
+ if (!maxTokens || maxTokens <= 0) {
112
+ return {
113
+ content: response,
114
+ metadata: {
115
+ tokenLimitApplied: false,
116
+ originalTokens: 0,
117
+ finalTokens: 0
118
+ }
119
+ };
120
+ }
121
+
122
+ // Handle different response types
123
+ let textContent = '';
124
+ let originalResponse = response;
125
+
126
+ if (typeof response === 'string') {
127
+ textContent = response;
128
+ } else if (response && typeof response === 'object') {
129
+ // Handle structured responses (like consensus tool)
130
+ if (response.content && typeof response.content === 'string') {
131
+ textContent = response.content;
132
+ } else {
133
+ // For complex objects, serialize to JSON for token counting
134
+ textContent = JSON.stringify(response, null, 2);
135
+ }
136
+ } else {
137
+ // Convert other types to string
138
+ textContent = String(response);
139
+ }
140
+
141
+ const result = truncateToTokenLimit(textContent, maxTokens);
142
+
143
+ if (result.truncated) {
144
+ logger.warn('Response truncated due to token limit', {
145
+ originalTokens: result.originalTokens,
146
+ finalTokens: result.finalTokens,
147
+ maxTokens,
148
+ truncationRatio: (result.finalTokens / result.originalTokens).toFixed(3)
149
+ });
150
+ }
151
+
152
+ // Return the processed response
153
+ if (typeof originalResponse === 'string') {
154
+ return {
155
+ content: result.content,
156
+ metadata: {
157
+ tokenLimitApplied: result.truncated,
158
+ originalTokens: result.originalTokens,
159
+ finalTokens: result.finalTokens,
160
+ truncationReason: result.truncationReason
161
+ }
162
+ };
163
+ } else if (originalResponse && typeof originalResponse === 'object') {
164
+ // For structured responses, update the content field
165
+ return {
166
+ ...originalResponse,
167
+ content: result.content,
168
+ metadata: {
169
+ ...originalResponse.metadata,
170
+ tokenLimitApplied: result.truncated,
171
+ originalTokens: result.originalTokens,
172
+ finalTokens: result.finalTokens,
173
+ truncationReason: result.truncationReason
174
+ }
175
+ };
176
+ } else {
177
+ return {
178
+ content: result.content,
179
+ metadata: {
180
+ tokenLimitApplied: result.truncated,
181
+ originalTokens: result.originalTokens,
182
+ finalTokens: result.finalTokens,
183
+ truncationReason: result.truncationReason
184
+ }
185
+ };
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Gets the configured token limit from environment or config
191
+ * @param {object} config - Configuration object
192
+ * @returns {number} Maximum token limit
193
+ */
194
+ export function getTokenLimit(config) {
195
+ if (config && config.mcp && config.mcp.max_mcp_output_tokens) {
196
+ return config.mcp.max_mcp_output_tokens;
197
+ }
198
+
199
+ // Fallback to environment variable
200
+ const envLimit = process.env.MAX_MCP_OUTPUT_TOKENS;
201
+ if (envLimit) {
202
+ const limit = parseInt(envLimit, 10);
203
+ if (!isNaN(limit) && limit > 0) {
204
+ return limit;
205
+ }
206
+ }
207
+
208
+ // Default limit
209
+ return 25000;
210
+ }
211
+
212
+ /**
213
+ * Estimates token count for a given text (exported for testing)
214
+ * @param {string} text - Text to estimate
215
+ * @returns {number} Estimated token count
216
+ */
217
+ export { estimateTokens };