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 +1 -1
- package/src/continuationStore.js +12 -6
- package/src/tools/chat.js +22 -4
- package/src/tools/consensus.js +64 -5
- package/src/tools/index.js +75 -2
- package/src/utils/fileValidator.js +8 -5
package/package.json
CHANGED
package/src/continuationStore.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Provides a consistent interface (get/set/delete) for state management.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
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:
|
|
247
|
+
content: responseContent,
|
|
230
248
|
continuation: {
|
|
231
249
|
id: continuationId,
|
|
232
250
|
provider: providerName,
|
package/src/tools/consensus.js
CHANGED
|
@@ -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:
|
|
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) {
|
package/src/tools/index.js
CHANGED
|
@@ -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:
|
|
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
|
}
|