converse-mcp-server 1.11.1 → 1.12.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "converse-mcp-server",
3
- "version": "1.11.1",
3
+ "version": "1.12.0",
4
4
  "description": "Converse MCP Server - Converse with other LLMs with chat and consensus tools",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -6,7 +6,7 @@
6
6
  * Provides a consistent interface (get/set/delete) for state management.
7
7
  */
8
8
 
9
- import { randomUUID } from 'crypto';
9
+ import { nanoid } from 'nanoid';
10
10
  import { debugLog, debugError } from './utils/console.js';
11
11
 
12
12
  /**
@@ -302,11 +302,12 @@ export function setContinuationStore(store) {
302
302
  }
303
303
 
304
304
  /**
305
- * Generate a new UUID-based continuation ID
306
- * @returns {string} Unique continuation ID
305
+ * Generate a new short continuation ID
306
+ * @returns {string} Unique continuation ID (format: conv_XXXXXXXXXX)
307
307
  */
308
308
  export function generateContinuationId() {
309
- return `conv_${randomUUID()}`;
309
+ // Generate a 10-character nanoid for short but unique IDs
310
+ return `conv_${nanoid(10)}`;
310
311
  }
311
312
 
312
313
  /**
@@ -319,9 +320,14 @@ export function isValidContinuationId(continuationId) {
319
320
  return false;
320
321
  }
321
322
 
322
- // Check for conv_ prefix and UUID format
323
+ // Check for conv_ prefix and nanoid format (10 characters, URL-safe)
324
+ // nanoid uses URL-safe alphabet: A-Za-z0-9_-
325
+ const nanoidPattern = /^conv_[A-Za-z0-9_-]{10}$/;
326
+
327
+ // Also accept legacy UUID format for backward compatibility
323
328
  const uuidPattern = /^conv_[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
324
- return uuidPattern.test(continuationId);
329
+
330
+ return nanoidPattern.test(continuationId) || uuidPattern.test(continuationId);
325
331
  }
326
332
 
327
333
  /**
package/src/tools/chat.js CHANGED
@@ -5,7 +5,7 @@
5
5
  * Handles context processing, provider calls, and state management.
6
6
  */
7
7
 
8
- import { createToolResponse, createToolError } from './index.js';
8
+ import { createToolResponse, createToolError, formatMetadataDisplay } from './index.js';
9
9
  import { processUnifiedContext, createFileContext } from '../utils/contextProcessor.js';
10
10
  import { generateContinuationId, addMessageToHistory } from '../continuationStore.js';
11
11
  import { debugLog, debugError } from '../utils/console.js';
@@ -69,7 +69,7 @@ export async function chatTool(args, dependencies) {
69
69
 
70
70
  // Validate file paths before processing
71
71
  if (files.length > 0 || images.length > 0) {
72
- const validation = await validateAllPaths({ files, images });
72
+ const validation = await validateAllPaths({ files, images }, { clientCwd: config.server?.client_cwd });
73
73
  if (!validation.valid) {
74
74
  logger.error('File validation failed', { errors: validation.errors });
75
75
  return validation.errorResponse;
@@ -189,12 +189,14 @@ export async function chatTool(args, dependencies) {
189
189
 
190
190
  // Call provider
191
191
  let response;
192
+ const startTime = Date.now();
192
193
  try {
193
194
  response = await selectedProvider.invoke(messages, providerOptions);
194
195
  } catch (error) {
195
196
  logger.error('Provider error', { error, data: { provider: providerName } });
196
197
  return createToolError(`Provider error: ${error.message}`);
197
198
  }
199
+ const executionTime = (Date.now() - startTime) / 1000; // Convert to seconds
198
200
 
199
201
  // Validate response
200
202
  if (!response || !response.content) {
@@ -224,9 +226,25 @@ export async function chatTool(args, dependencies) {
224
226
  // Continue even if save fails
225
227
  }
226
228
 
227
- // Create response with continuation
229
+ // Prepare metadata for display
230
+ const displayMetadata = {
231
+ continuation_id: continuationId,
232
+ provider: providerName,
233
+ model: resolvedModel,
234
+ execution_time: executionTime,
235
+ message_count: updatedMessages.filter(msg => msg.role !== 'system').length,
236
+ ...response.metadata
237
+ };
238
+
239
+ // Format metadata display (disable in test environment)
240
+ const enableMetadataDisplay = config.environment?.nodeEnv !== 'test';
241
+ const metadataDisplay = formatMetadataDisplay(displayMetadata, 'chat', executionTime, enableMetadataDisplay);
242
+
243
+ // Create response with continuation and metadata display
244
+ const responseContent = metadataDisplay ? `${metadataDisplay}\n\n${response.content}` : response.content;
245
+
228
246
  const result = {
229
- content: response.content,
247
+ content: responseContent,
230
248
  continuation: {
231
249
  id: continuationId,
232
250
  provider: providerName,
@@ -5,7 +5,7 @@
5
5
  * Calls all available providers simultaneously and aggregates responses.
6
6
  */
7
7
 
8
- import { createToolResponse, createToolError } from './index.js';
8
+ import { createToolResponse, createToolError, formatMetadataDisplay, formatFailureDetails } from './index.js';
9
9
  import { processUnifiedContext, createFileContext } from '../utils/contextProcessor.js';
10
10
  import { generateContinuationId, addMessageToHistory } from '../continuationStore.js';
11
11
  import { debugLog, debugError } from '../utils/console.js';
@@ -77,7 +77,7 @@ export async function consensusTool(args, dependencies) {
77
77
  const validation = await validateAllPaths({
78
78
  files,
79
79
  images
80
- });
80
+ }, { clientCwd: config.server?.client_cwd });
81
81
  if (!validation.valid) {
82
82
  logger.error('File validation failed', { errors: validation.errors });
83
83
  return validation.errorResponse;
@@ -234,6 +234,7 @@ export async function consensusTool(args, dependencies) {
234
234
 
235
235
  // Phase 1: Initial parallel provider calls
236
236
  logger.debug('Calling providers in parallel', { data: { providerCount: providerCalls.length } });
237
+ const consensusStartTime = Date.now();
237
238
  const initialResults = await Promise.allSettled(
238
239
  providerCalls.map(async (call) => {
239
240
  try {
@@ -388,6 +389,54 @@ Please provide your refined response:`;
388
389
  // Continue even if save fails
389
390
  }
390
391
 
392
+ const consensusExecutionTime = (Date.now() - consensusStartTime) / 1000; // Convert to seconds
393
+
394
+ // Calculate final success count and collect failure details
395
+ let finalSuccessCount;
396
+ let failureDetails = [];
397
+
398
+ if (enable_cross_feedback && refinedPhase) {
399
+ // When cross-feedback is enabled, count only models that succeeded in both phases
400
+ finalSuccessCount = refinedPhase.filter(r => r.status === 'success').length;
401
+
402
+ // Collect detailed failure information
403
+ refinedPhase.forEach(result => {
404
+ if (result.status === 'partial') {
405
+ failureDetails.push(`${result.model} (refinement failed)`);
406
+ }
407
+ });
408
+
409
+ // Add models that failed in initial phase
410
+ initialPhase.failed.forEach(failure => {
411
+ failureDetails.push(`${failure.model} (initial failed)`);
412
+ });
413
+ } else {
414
+ // When cross-feedback is disabled, count initial successes
415
+ finalSuccessCount = initialPhase.successful.length;
416
+
417
+ // Collect initial failure information
418
+ initialPhase.failed.forEach(failure => {
419
+ failureDetails.push(`${failure.model} (${failure.error})`);
420
+ });
421
+ }
422
+
423
+ // Prepare metadata for display
424
+ const displayMetadata = {
425
+ continuation_id: continuationId,
426
+ successful_models: finalSuccessCount,
427
+ total_models: models.length,
428
+ execution_time: consensusExecutionTime,
429
+ cross_feedback: enable_cross_feedback,
430
+ refined_responses: refinedPhase ? refinedPhase.filter(r => r.status === 'success').length : 0,
431
+ initial_successes: initialPhase.successful.length,
432
+ partial_successes: refinedPhase ? refinedPhase.filter(r => r.status === 'partial').length : 0,
433
+ failure_details: failureDetails
434
+ };
435
+
436
+ // Format metadata display (disable in test environment)
437
+ const enableMetadataDisplay = config.environment?.nodeEnv !== 'test';
438
+ const metadataDisplay = formatMetadataDisplay(displayMetadata, 'consensus', consensusExecutionTime, enableMetadataDisplay);
439
+
391
440
  // Build result object keeping backward compatibility but removing rawResponse
392
441
  const result = {
393
442
  status: 'consensus_complete',
@@ -408,7 +457,8 @@ Please provide your refined response:`;
408
457
  enable_cross_feedback,
409
458
  temperature,
410
459
  models_requested: models
411
- }
460
+ },
461
+ metadata_display: metadataDisplay
412
462
  };
413
463
 
414
464
  // Apply token limiting to the final response
@@ -416,13 +466,22 @@ Please provide your refined response:`;
416
466
  const resultStr = JSON.stringify(result, null, 2);
417
467
  const limitedResult = applyTokenLimit(resultStr, tokenLimit);
418
468
 
469
+ // Add failure details to the response content if there are failures and display is enabled
470
+ let finalContent = limitedResult.content;
471
+ if (enableMetadataDisplay && failureDetails.length > 0) {
472
+ const failureInfo = formatFailureDetails(failureDetails);
473
+ finalContent = limitedResult.content + failureInfo;
474
+ }
475
+
419
476
  // Return with continuation at top level for test compatibility
477
+ // For consensus, we'll add metadata display to a separate field to maintain JSON structure
420
478
  return createToolResponse({
421
- content: limitedResult.content,
479
+ content: finalContent,
422
480
  continuation: {
423
481
  id: continuationId,
424
482
  messageCount: messages.length + 1
425
- }
483
+ },
484
+ metadata_display: metadataDisplay
426
485
  });
427
486
 
428
487
  } catch (error) {
@@ -66,6 +66,73 @@ export function getAvailableTools() {
66
66
  return Object.keys(tools);
67
67
  }
68
68
 
69
+ /**
70
+ * Format metadata for display in tool responses
71
+ * @param {object} metadata - Metadata to format
72
+ * @param {string} toolName - Name of the tool
73
+ * @param {number} executionTime - Execution time in seconds
74
+ * @param {boolean} enableDisplay - Whether to enable metadata display
75
+ * @returns {string} Formatted metadata string
76
+ */
77
+ export function formatMetadataDisplay(metadata = {}, toolName = '', executionTime = null, enableDisplay = true) {
78
+ // Return empty string if display is disabled (useful for testing)
79
+ if (!enableDisplay) {
80
+ return '';
81
+ }
82
+
83
+ const parts = [];
84
+
85
+ if (executionTime !== null) {
86
+ // Format time appropriately based on duration
87
+ let timeDisplay;
88
+ if (executionTime >= 60) {
89
+ // Show minutes and seconds for long requests
90
+ const minutes = Math.floor(executionTime / 60);
91
+ const seconds = Math.round(executionTime % 60);
92
+ timeDisplay = `${minutes}m${seconds}s`;
93
+ } else if (executionTime >= 1) {
94
+ // Show seconds with 1 decimal place for requests over 1 second
95
+ timeDisplay = `${executionTime.toFixed(1)}s`;
96
+ } else {
97
+ // Show seconds with 2 decimal places for sub-second requests
98
+ timeDisplay = `${executionTime.toFixed(2)}s`;
99
+ }
100
+ parts.push(`⏱️ ${timeDisplay}`);
101
+ }
102
+
103
+ if (metadata.successful_models !== undefined) {
104
+ parts.push(`✅ ${metadata.successful_models}/${metadata.total_models} models`);
105
+ }
106
+
107
+ if (metadata.continuation_id) {
108
+ parts.push(`🔗 ${metadata.continuation_id}`);
109
+ }
110
+
111
+ if (metadata.provider) {
112
+ parts.push(`🤖 ${metadata.provider}`);
113
+ }
114
+
115
+ if (metadata.model) {
116
+ parts.push(`📱 ${metadata.model}`);
117
+ }
118
+
119
+ return parts.length > 0 ? `[${parts.join(' | ')}]` : '';
120
+ }
121
+
122
+ /**
123
+ * Format failure details for display in tool responses
124
+ * @param {array} failureDetails - Array of failure detail strings
125
+ * @returns {string} Formatted failure details string
126
+ */
127
+ export function formatFailureDetails(failureDetails = []) {
128
+ if (!failureDetails || failureDetails.length === 0) {
129
+ return '';
130
+ }
131
+
132
+ const failureList = failureDetails.map(detail => `• ${detail}`).join('\n');
133
+ return `\nModel failures:\n${failureList}`;
134
+ }
135
+
69
136
  /**
70
137
  * Create MCP-compatible tool response
71
138
  * @param {string|object} content - Response content (string) or full response object
@@ -86,12 +153,18 @@ export function createToolResponse(content, isError = false, additionalFields =
86
153
  }
87
154
 
88
155
  // If it's a tool result object (has continuation, metadata, etc.) convert to MCP format
89
- if (content.continuation || content.metadata || content.content) {
156
+ if (content.continuation || content.metadata || content.content || content.metadata_display) {
157
+ // Prepare the text content, potentially prefixing with metadata display
158
+ let textContent = content.content || JSON.stringify(content, null, 2);
159
+ if (content.metadata_display) {
160
+ textContent = `${content.metadata_display}\n\n${textContent}`;
161
+ }
162
+
90
163
  const mcpResponse = {
91
164
  content: [
92
165
  {
93
166
  type: 'text',
94
- text: content.content || JSON.stringify(content, null, 2)
167
+ text: textContent
95
168
  }
96
169
  ],
97
170
  isError: isError || content.isError || false,
@@ -13,9 +13,10 @@ import { createToolError } from '../tools/index.js';
13
13
  * Validate that all provided file paths exist
14
14
  * @param {string[]} filePaths - Array of file paths to validate
15
15
  * @param {string} fileType - Type of files being validated (e.g., 'file', 'image')
16
+ * @param {object} options - Validation options including clientCwd
16
17
  * @returns {Promise<{valid: boolean, missingPaths: string[], error?: object}>}
17
18
  */
18
- export async function validateFilePaths(filePaths, fileType = 'file') {
19
+ export async function validateFilePaths(filePaths, fileType = 'file', options = {}) {
19
20
  if (!Array.isArray(filePaths) || filePaths.length === 0) {
20
21
  return { valid: true, missingPaths: [] };
21
22
  }
@@ -34,9 +35,10 @@ export async function validateFilePaths(filePaths, fileType = 'file') {
34
35
  }
35
36
 
36
37
  // Convert to absolute path if needed
38
+ // Use clientCwd if provided (for auto-detected client working directory), otherwise fall back to process.cwd()
37
39
  const absolutePath = isAbsolute(filePath)
38
40
  ? filePath
39
- : resolve(process.cwd(), filePath);
41
+ : resolve(options.clientCwd || process.cwd(), filePath);
40
42
 
41
43
  try {
42
44
  // Check if file exists and is readable
@@ -62,14 +64,15 @@ export async function validateFilePaths(filePaths, fileType = 'file') {
62
64
  /**
63
65
  * Validate both files and images together
64
66
  * @param {object} paths - Object containing files and images arrays
67
+ * @param {object} options - Validation options including clientCwd
65
68
  * @returns {Promise<{valid: boolean, errors: string[], errorResponse?: object}>}
66
69
  */
67
- export async function validateAllPaths({ files = [], images = [] }) {
70
+ export async function validateAllPaths({ files = [], images = [] }, options = {}) {
68
71
  const errors = [];
69
72
 
70
73
  // Validate regular files
71
74
  if (files.length > 0) {
72
- const fileValidation = await validateFilePaths(files, 'file');
75
+ const fileValidation = await validateFilePaths(files, 'file', options);
73
76
  if (!fileValidation.valid) {
74
77
  errors.push(`Files not found: ${fileValidation.missingPaths.join(', ')}`);
75
78
  }
@@ -77,7 +80,7 @@ export async function validateAllPaths({ files = [], images = [] }) {
77
80
 
78
81
  // Validate image files
79
82
  if (images.length > 0) {
80
- const imageValidation = await validateFilePaths(images, 'image');
83
+ const imageValidation = await validateFilePaths(images, 'image', options);
81
84
  if (!imageValidation.valid) {
82
85
  errors.push(`Images not found: ${imageValidation.missingPaths.join(', ')}`);
83
86
  }