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,475 @@
1
+ /**
2
+ * Context Processor Utilities
3
+ *
4
+ * Unified interface for handling files, images, and web search context processing.
5
+ * Uses Node.js built-in modules with security validation and comprehensive error handling.
6
+ * Includes placeholders for advanced features that can be enhanced later.
7
+ */
8
+
9
+ import { readFile, stat, access } from 'fs/promises';
10
+ import { extname, resolve, isAbsolute } from 'path';
11
+ import { constants } from 'fs';
12
+ import { fileURLToPath } from 'url';
13
+ import { dirname } from 'path';
14
+
15
+ // Security: Define allowed directories for file access
16
+ const __filename = fileURLToPath(import.meta.url);
17
+ const __dirname = dirname(__filename);
18
+ const PROJECT_ROOT = resolve(__dirname, '../../..');
19
+
20
+ /**
21
+ * Custom error class for context processing operations
22
+ */
23
+ export class ContextProcessorError extends Error {
24
+ constructor(message, code = 'CONTEXT_ERROR', details = {}) {
25
+ super(message);
26
+ this.name = 'ContextProcessorError';
27
+ this.code = code;
28
+ this.details = details;
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Supported file types for context processing
34
+ */
35
+ const SUPPORTED_TEXT_EXTENSIONS = [
36
+ '.txt', '.md', '.js', '.ts', '.json', '.yaml', '.yml',
37
+ '.py', '.java', '.c', '.cpp', '.h', '.css', '.html',
38
+ '.xml', '.csv', '.sql', '.sh', '.bat', '.log'
39
+ ];
40
+
41
+ const SUPPORTED_IMAGE_EXTENSIONS = [
42
+ '.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp'
43
+ ];
44
+
45
+ /**
46
+ * Security validation for file paths
47
+ * @param {string} filePath - Path to validate
48
+ * @param {object} options - Validation options
49
+ * @returns {Promise<string>} Validated absolute path
50
+ * @throws {ContextProcessorError} If path is invalid or unsafe
51
+ */
52
+ async function validateFilePath(filePath, options = {}) {
53
+ // Check if path is provided
54
+ if (!filePath || typeof filePath !== 'string') {
55
+ throw new ContextProcessorError(
56
+ 'File path must be a non-empty string',
57
+ 'INVALID_PATH'
58
+ );
59
+ }
60
+
61
+ // Convert to absolute path
62
+ const absolutePath = isAbsolute(filePath) ? filePath : resolve(process.cwd(), filePath);
63
+
64
+ // Security: Check if path is within allowed directories
65
+ const allowedDirs = options.allowedDirectories || [process.cwd(), PROJECT_ROOT];
66
+ const isAllowed = allowedDirs.some(dir => {
67
+ const resolvedDir = resolve(dir);
68
+ return absolutePath.startsWith(resolvedDir);
69
+ });
70
+
71
+ if (!isAllowed && !options.skipSecurityCheck) {
72
+ throw new ContextProcessorError(
73
+ 'File access denied: path outside allowed directories',
74
+ 'SECURITY_VIOLATION',
75
+ { path: absolutePath, allowedDirs }
76
+ );
77
+ }
78
+
79
+ // Check if file exists and is readable
80
+ try {
81
+ await access(absolutePath, constants.R_OK);
82
+ } catch (error) {
83
+ throw new ContextProcessorError(
84
+ `File not accessible: ${error.message}`,
85
+ 'FILE_ACCESS_ERROR',
86
+ { path: absolutePath }
87
+ );
88
+ }
89
+
90
+ return absolutePath;
91
+ }
92
+
93
+ /**
94
+ * Process file content for inclusion in AI context
95
+ * @param {string} filePath - Absolute or relative path to the file
96
+ * @param {object} options - Processing options
97
+ * @param {string[]} options.allowedDirectories - Allowed directories for security
98
+ * @param {number} options.maxTextSize - Maximum text file size in bytes
99
+ * @param {number} options.maxImageSize - Maximum image file size in bytes
100
+ * @param {boolean} options.skipSecurityCheck - Skip security validation (for testing)
101
+ * @returns {Promise<object>} Processed content with metadata
102
+ */
103
+ export async function processFileContent(filePath, options = {}) {
104
+ try {
105
+ // Security validation
106
+ const validatedPath = await validateFilePath(filePath, options);
107
+
108
+ const fileStats = await stat(validatedPath);
109
+ const extension = extname(validatedPath).toLowerCase();
110
+
111
+ // Check if it's actually a file (not a directory)
112
+ if (!fileStats.isFile()) {
113
+ throw new ContextProcessorError(
114
+ 'Path is not a file',
115
+ 'NOT_A_FILE',
116
+ { path: validatedPath }
117
+ );
118
+ }
119
+
120
+ const result = {
121
+ path: validatedPath,
122
+ originalPath: filePath,
123
+ size: fileStats.size,
124
+ extension,
125
+ type: 'unknown',
126
+ content: null,
127
+ error: null,
128
+ lastModified: fileStats.mtime,
129
+ encoding: null,
130
+ };
131
+
132
+ // Check file size limits
133
+ const maxTextSize = options.maxTextSize || 1024 * 1024; // 1MB default
134
+ const maxImageSize = options.maxImageSize || 10 * 1024 * 1024; // 10MB default
135
+
136
+ if (SUPPORTED_TEXT_EXTENSIONS.includes(extension)) {
137
+ result.type = 'text';
138
+
139
+ if (fileStats.size > maxTextSize) {
140
+ result.error = `File too large (${fileStats.size} bytes, max ${maxTextSize})`;
141
+ return result;
142
+ }
143
+
144
+ const content = await readFile(validatedPath, 'utf8');
145
+ result.content = content;
146
+ result.lineCount = content.split('\n').length;
147
+ result.encoding = 'utf8';
148
+ result.charCount = content.length;
149
+
150
+ } else if (SUPPORTED_IMAGE_EXTENSIONS.includes(extension)) {
151
+ result.type = 'image';
152
+
153
+ if (fileStats.size > maxImageSize) {
154
+ result.error = `Image too large (${fileStats.size} bytes, max ${maxImageSize})`;
155
+ return result;
156
+ }
157
+
158
+ // For images, read as base64 for AI processing (placeholder for advanced features)
159
+ const buffer = await readFile(validatedPath);
160
+ result.content = buffer.toString('base64');
161
+ result.mimeType = getMimeType(extension);
162
+ result.encoding = 'base64';
163
+
164
+ // Placeholder: Advanced image processing could be added here
165
+ // - Image resizing, format conversion
166
+ // - EXIF data extraction
167
+ // - Image analysis/description generation
168
+
169
+ } else {
170
+ result.error = `Unsupported file type: ${extension}`;
171
+ }
172
+
173
+ return result;
174
+
175
+ } catch (error) {
176
+ return {
177
+ path: filePath,
178
+ originalPath: filePath,
179
+ type: 'error',
180
+ error: error instanceof ContextProcessorError ? error.message : `Unexpected error: ${error.message}`,
181
+ errorCode: error.code || 'UNKNOWN_ERROR',
182
+ content: null,
183
+ lastModified: null,
184
+ };
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Process multiple files for context with error isolation
190
+ * @param {string[]} filePaths - Array of file paths (absolute or relative)
191
+ * @param {object} options - Processing options
192
+ * @returns {Promise<object[]>} Array of processed file contents
193
+ */
194
+ export async function processMultipleFiles(filePaths, options = {}) {
195
+ if (!Array.isArray(filePaths)) {
196
+ throw new ContextProcessorError(
197
+ 'filePaths must be an array',
198
+ 'INVALID_INPUT'
199
+ );
200
+ }
201
+
202
+ // Process files in parallel but isolate errors
203
+ const results = await Promise.allSettled(
204
+ filePaths.map(path => processFileContent(path, options))
205
+ );
206
+
207
+ // Convert Promise.allSettled results to consistent format
208
+ return results.map((result, index) => {
209
+ if (result.status === 'fulfilled') {
210
+ return result.value;
211
+ } else {
212
+ return {
213
+ path: filePaths[index],
214
+ originalPath: filePaths[index],
215
+ type: 'error',
216
+ error: result.reason.message || 'Unknown processing error',
217
+ errorCode: result.reason.code || 'PROCESSING_ERROR',
218
+ content: null,
219
+ lastModified: null,
220
+ };
221
+ }
222
+ });
223
+ }
224
+
225
+ /**
226
+ * Web search context integration (placeholder)
227
+ * @param {string} query - Search query
228
+ * @param {object} options - Search options
229
+ * @returns {Promise<object>} Web search results for context
230
+ */
231
+ export async function processWebSearchContext(query, options = {}) {
232
+ // Placeholder implementation - can be enhanced with actual web search API
233
+ return {
234
+ type: 'web_search',
235
+ query,
236
+ results: [],
237
+ error: null,
238
+ timestamp: new Date().toISOString(),
239
+ // Placeholder: Future implementation could integrate with:
240
+ // - Google Search API
241
+ // - Bing Search API
242
+ // - DuckDuckGo API
243
+ // - Custom search engines
244
+ placeholder: true,
245
+ message: 'Web search integration placeholder - not yet implemented'
246
+ };
247
+ }
248
+
249
+ /**
250
+ * Unified context processor - handles all context types
251
+ * @param {object} contextRequest - Context processing request
252
+ * @param {string[]} contextRequest.files - Array of file paths
253
+ * @param {string[]} contextRequest.images - Array of image paths (for explicit image processing)
254
+ * @param {string} contextRequest.webSearch - Web search query
255
+ * @param {object} options - Processing options
256
+ * @returns {Promise<object>} Unified context result
257
+ */
258
+ export async function processUnifiedContext(contextRequest, options = {}) {
259
+ const result = {
260
+ files: [],
261
+ images: [],
262
+ webSearch: null,
263
+ errors: [],
264
+ timestamp: new Date().toISOString(),
265
+ };
266
+
267
+ try {
268
+ // Process files if provided
269
+ if (contextRequest.files && Array.isArray(contextRequest.files)) {
270
+ result.files = await processMultipleFiles(contextRequest.files, options);
271
+ }
272
+
273
+ // Process images if provided (currently same as files, placeholder for advanced features)
274
+ if (contextRequest.images && Array.isArray(contextRequest.images)) {
275
+ result.images = await processMultipleFiles(contextRequest.images, {
276
+ ...options,
277
+ imageProcessingMode: true // Placeholder for future image-specific processing
278
+ });
279
+ }
280
+
281
+ // Process web search if provided
282
+ if (contextRequest.webSearch && typeof contextRequest.webSearch === 'string') {
283
+ result.webSearch = await processWebSearchContext(contextRequest.webSearch, options);
284
+ }
285
+
286
+ } catch (error) {
287
+ result.errors.push({
288
+ type: 'unified_processing_error',
289
+ message: error.message,
290
+ code: error.code || 'UNKNOWN_ERROR'
291
+ });
292
+ }
293
+
294
+ return result;
295
+ }
296
+
297
+ /**
298
+ * Create context message from file contents
299
+ * @param {object[]} processedFiles - Array of processed file contents
300
+ * @param {object} options - Message creation options
301
+ * @returns {object|null} Context message for AI
302
+ */
303
+ export function createFileContext(processedFiles, options = {}) {
304
+ if (!Array.isArray(processedFiles)) {
305
+ return null;
306
+ }
307
+
308
+ const textFiles = processedFiles.filter(f => f.type === 'text' && !f.error);
309
+ const imageFiles = processedFiles.filter(f => f.type === 'image' && !f.error);
310
+ const errors = processedFiles.filter(f => f.error);
311
+
312
+ const includeErrors = options.includeErrors !== false; // Default to true
313
+
314
+ let contextText = '';
315
+
316
+ if (textFiles.length > 0) {
317
+ contextText += '=== FILE CONTEXT ===\n\n';
318
+ for (const file of textFiles) {
319
+ contextText += `--- ${file.originalPath || file.path} ---\n`;
320
+ if (options.includeMetadata) {
321
+ contextText += `Size: ${file.size} bytes, Lines: ${file.lineCount || 'N/A'}\n`;
322
+ contextText += `Last Modified: ${file.lastModified || 'N/A'}\n`;
323
+ }
324
+ contextText += `${file.content}\n\n`;
325
+ }
326
+ }
327
+
328
+ if (errors.length > 0 && includeErrors) {
329
+ contextText += '=== FILE ERRORS ===\n';
330
+ for (const error of errors) {
331
+ contextText += `${error.originalPath || error.path}: ${error.error}`;
332
+ if (error.errorCode) {
333
+ contextText += ` (${error.errorCode})`;
334
+ }
335
+ contextText += '\n';
336
+ }
337
+ contextText += '\n';
338
+ }
339
+
340
+ const message = {
341
+ role: 'user',
342
+ content: [],
343
+ };
344
+
345
+ if (contextText) {
346
+ message.content.push({
347
+ type: 'text',
348
+ text: contextText,
349
+ });
350
+ }
351
+
352
+ // Add images with metadata
353
+ for (const image of imageFiles) {
354
+ message.content.push({
355
+ type: 'image',
356
+ source: {
357
+ type: 'base64',
358
+ media_type: image.mimeType,
359
+ data: image.content,
360
+ },
361
+ // Add metadata for debugging
362
+ metadata: options.includeMetadata ? {
363
+ path: image.originalPath || image.path,
364
+ size: image.size,
365
+ lastModified: image.lastModified
366
+ } : undefined
367
+ });
368
+ }
369
+
370
+ return message.content.length > 0 ? message : null;
371
+ }
372
+
373
+ /**
374
+ * Get MIME type for file extension
375
+ * @param {string} extension - File extension
376
+ * @returns {string} MIME type
377
+ */
378
+ function getMimeType(extension) {
379
+ const mimeTypes = {
380
+ // Images
381
+ '.jpg': 'image/jpeg',
382
+ '.jpeg': 'image/jpeg',
383
+ '.png': 'image/png',
384
+ '.gif': 'image/gif',
385
+ '.webp': 'image/webp',
386
+ '.bmp': 'image/bmp',
387
+ // Text files
388
+ '.txt': 'text/plain',
389
+ '.md': 'text/markdown',
390
+ '.js': 'text/javascript',
391
+ '.ts': 'text/typescript',
392
+ '.json': 'application/json',
393
+ '.yaml': 'text/yaml',
394
+ '.yml': 'text/yaml',
395
+ '.html': 'text/html',
396
+ '.css': 'text/css',
397
+ '.xml': 'text/xml',
398
+ };
399
+
400
+ return mimeTypes[extension] || 'application/octet-stream';
401
+ }
402
+
403
+ /**
404
+ * Validate file paths with security checks
405
+ * @param {string[]} filePaths - Array of file paths to validate
406
+ * @param {object} options - Validation options
407
+ * @returns {Promise<object>} Validation result with valid/invalid paths
408
+ */
409
+ export async function validateFilePaths(filePaths, options = {}) {
410
+ if (!Array.isArray(filePaths)) {
411
+ throw new ContextProcessorError(
412
+ 'filePaths must be an array',
413
+ 'INVALID_INPUT'
414
+ );
415
+ }
416
+
417
+ const results = {
418
+ valid: [],
419
+ invalid: [],
420
+ securityViolations: [],
421
+ };
422
+
423
+ for (const path of filePaths) {
424
+ try {
425
+ const validatedPath = await validateFilePath(path, options);
426
+ results.valid.push({
427
+ originalPath: path,
428
+ validatedPath,
429
+ isValid: true
430
+ });
431
+ } catch (error) {
432
+ const errorInfo = {
433
+ path,
434
+ error: error.message,
435
+ code: error.code
436
+ };
437
+
438
+ if (error.code === 'SECURITY_VIOLATION') {
439
+ results.securityViolations.push(errorInfo);
440
+ } else {
441
+ results.invalid.push(errorInfo);
442
+ }
443
+ }
444
+ }
445
+
446
+ return results;
447
+ }
448
+
449
+ /**
450
+ * Get supported file extensions
451
+ * @returns {object} Object containing supported extensions by type
452
+ */
453
+ export function getSupportedExtensions() {
454
+ return {
455
+ text: [...SUPPORTED_TEXT_EXTENSIONS],
456
+ image: [...SUPPORTED_IMAGE_EXTENSIONS],
457
+ all: [...SUPPORTED_TEXT_EXTENSIONS, ...SUPPORTED_IMAGE_EXTENSIONS]
458
+ };
459
+ }
460
+
461
+ /**
462
+ * Check if file type is supported
463
+ * @param {string} filePath - Path to check
464
+ * @returns {object} Support information
465
+ */
466
+ export function isFileTypeSupported(filePath) {
467
+ const extension = extname(filePath).toLowerCase();
468
+
469
+ return {
470
+ extension,
471
+ isSupported: SUPPORTED_TEXT_EXTENSIONS.includes(extension) || SUPPORTED_IMAGE_EXTENSIONS.includes(extension),
472
+ type: SUPPORTED_TEXT_EXTENSIONS.includes(extension) ? 'text' :
473
+ SUPPORTED_IMAGE_EXTENSIONS.includes(extension) ? 'image' : 'unknown'
474
+ };
475
+ }