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.
- package/.env.example +177 -0
- package/README.md +425 -0
- package/bin/converse.js +45 -0
- package/docs/API.md +897 -0
- package/docs/ARCHITECTURE.md +552 -0
- package/docs/EXAMPLES.md +736 -0
- package/package.json +101 -0
- package/src/config.js +521 -0
- package/src/continuationStore.js +340 -0
- package/src/index.js +216 -0
- package/src/providers/google.js +441 -0
- package/src/providers/index.js +87 -0
- package/src/providers/openai.js +348 -0
- package/src/providers/xai.js +305 -0
- package/src/router.js +497 -0
- package/src/systemPrompts.js +90 -0
- package/src/tools/chat.js +336 -0
- package/src/tools/consensus.js +478 -0
- package/src/tools/index.js +156 -0
- package/src/transport/httpTransport.js +548 -0
- package/src/utils/console.js +64 -0
- package/src/utils/contextProcessor.js +475 -0
- package/src/utils/errorHandler.js +555 -0
- package/src/utils/logger.js +450 -0
- package/src/utils/tokenLimiter.js +217 -0
|
@@ -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 };
|