converse-mcp-server 2.3.1 → 2.4.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/README.md +771 -738
- package/docs/API.md +10 -1
- package/docs/PROVIDERS.md +8 -4
- package/package.json +12 -12
- package/src/async/asyncJobStore.js +82 -52
- package/src/async/eventBus.js +25 -20
- package/src/async/fileCache.js +121 -40
- package/src/async/jobRunner.js +65 -39
- package/src/async/providerStreamNormalizer.js +203 -117
- package/src/config.js +374 -102
- package/src/continuationStore.js +32 -24
- package/src/index.js +45 -25
- package/src/prompts/helpPrompt.js +328 -305
- package/src/providers/anthropic.js +303 -119
- package/src/providers/codex.js +103 -45
- package/src/providers/deepseek.js +24 -8
- package/src/providers/google.js +323 -93
- package/src/providers/index.js +1 -1
- package/src/providers/interface.js +16 -11
- package/src/providers/mistral.js +179 -69
- package/src/providers/openai-compatible.js +231 -94
- package/src/providers/openai.js +1094 -914
- package/src/providers/openrouter-endpoints-client.js +220 -216
- package/src/providers/openrouter.js +426 -381
- package/src/providers/xai.js +153 -56
- package/src/resources/helpResource.js +70 -67
- package/src/router.js +95 -67
- package/src/services/summarizationService.js +51 -24
- package/src/systemPrompts.js +89 -89
- package/src/tools/cancelJob.js +31 -19
- package/src/tools/chat.js +997 -883
- package/src/tools/checkStatus.js +86 -65
- package/src/tools/consensus.js +400 -234
- package/src/tools/index.js +39 -16
- package/src/transport/httpTransport.js +82 -55
- package/src/utils/contextProcessor.js +54 -37
- package/src/utils/errorHandler.js +95 -45
- package/src/utils/fileValidator.js +107 -98
- package/src/utils/formatStatus.js +122 -64
- package/src/utils/logger.js +459 -449
- package/src/utils/pathUtils.js +2 -2
- package/src/utils/tokenLimiter.js +216 -216
package/src/tools/index.js
CHANGED
|
@@ -50,12 +50,12 @@ export function getTools(config = null) {
|
|
|
50
50
|
// Create a modified inputSchema without the async parameter
|
|
51
51
|
const modifiedSchema = {
|
|
52
52
|
...tool.inputSchema,
|
|
53
|
-
properties: { ...tool.inputSchema.properties }
|
|
53
|
+
properties: { ...tool.inputSchema.properties },
|
|
54
54
|
};
|
|
55
55
|
delete modifiedSchema.properties.async;
|
|
56
56
|
|
|
57
57
|
// Create a wrapper function with modified metadata
|
|
58
|
-
const wrappedTool = async function(...args) {
|
|
58
|
+
const wrappedTool = async function (...args) {
|
|
59
59
|
return await tool(...args);
|
|
60
60
|
};
|
|
61
61
|
wrappedTool.description = tool.description;
|
|
@@ -117,7 +117,12 @@ export function getAvailableTools() {
|
|
|
117
117
|
* @param {boolean} enableDisplay - Whether to enable metadata display
|
|
118
118
|
* @returns {string} Formatted metadata string
|
|
119
119
|
*/
|
|
120
|
-
export function formatMetadataDisplay(
|
|
120
|
+
export function formatMetadataDisplay(
|
|
121
|
+
metadata = {},
|
|
122
|
+
toolName = '',
|
|
123
|
+
executionTime = null,
|
|
124
|
+
enableDisplay = true,
|
|
125
|
+
) {
|
|
121
126
|
// Return empty string if display is disabled (useful for testing)
|
|
122
127
|
if (!enableDisplay) {
|
|
123
128
|
return '';
|
|
@@ -126,7 +131,10 @@ export function formatMetadataDisplay(metadata = {}, toolName = '', executionTim
|
|
|
126
131
|
const parts = [];
|
|
127
132
|
|
|
128
133
|
// Use elapsed_seconds from job if available, otherwise use executionTime
|
|
129
|
-
const timeToShow =
|
|
134
|
+
const timeToShow =
|
|
135
|
+
metadata.elapsed_seconds !== undefined
|
|
136
|
+
? metadata.elapsed_seconds
|
|
137
|
+
: executionTime;
|
|
130
138
|
|
|
131
139
|
if (timeToShow !== null) {
|
|
132
140
|
// Format time appropriately based on duration
|
|
@@ -147,7 +155,9 @@ export function formatMetadataDisplay(metadata = {}, toolName = '', executionTim
|
|
|
147
155
|
}
|
|
148
156
|
|
|
149
157
|
if (metadata.successful_models !== undefined) {
|
|
150
|
-
parts.push(
|
|
158
|
+
parts.push(
|
|
159
|
+
`✅ ${metadata.successful_models}/${metadata.total_models} models`,
|
|
160
|
+
);
|
|
151
161
|
}
|
|
152
162
|
|
|
153
163
|
if (metadata.continuation_id) {
|
|
@@ -178,7 +188,7 @@ export function formatFailureDetails(failureDetails = []) {
|
|
|
178
188
|
return '';
|
|
179
189
|
}
|
|
180
190
|
|
|
181
|
-
const failureList = failureDetails.map(detail => `• ${detail}`).join('\n');
|
|
191
|
+
const failureList = failureDetails.map((detail) => `• ${detail}`).join('\n');
|
|
182
192
|
return `\nModel failures:\n${failureList}`;
|
|
183
193
|
}
|
|
184
194
|
|
|
@@ -189,20 +199,33 @@ export function formatFailureDetails(failureDetails = []) {
|
|
|
189
199
|
* @param {object} additionalFields - Additional fields to include in response
|
|
190
200
|
* @returns {object} MCP tool response
|
|
191
201
|
*/
|
|
192
|
-
export function createToolResponse(
|
|
202
|
+
export function createToolResponse(
|
|
203
|
+
content,
|
|
204
|
+
isError = false,
|
|
205
|
+
additionalFields = {},
|
|
206
|
+
) {
|
|
193
207
|
// If content is already a structured response object, use it directly
|
|
194
|
-
if (
|
|
208
|
+
if (
|
|
209
|
+
typeof content === 'object' &&
|
|
210
|
+
content !== null &&
|
|
211
|
+
!Array.isArray(content)
|
|
212
|
+
) {
|
|
195
213
|
// If it's a complete response object with content array, return it directly
|
|
196
214
|
if (content.content && Array.isArray(content.content)) {
|
|
197
215
|
return {
|
|
198
216
|
...content,
|
|
199
217
|
isError: isError || content.isError || false,
|
|
200
|
-
...additionalFields
|
|
218
|
+
...additionalFields,
|
|
201
219
|
};
|
|
202
220
|
}
|
|
203
221
|
|
|
204
222
|
// If it's a tool result object (has continuation, metadata, etc.) convert to MCP format
|
|
205
|
-
if (
|
|
223
|
+
if (
|
|
224
|
+
content.continuation ||
|
|
225
|
+
content.metadata ||
|
|
226
|
+
content.content ||
|
|
227
|
+
content.metadata_display
|
|
228
|
+
) {
|
|
206
229
|
// Prepare the text content, potentially prefixing with metadata display
|
|
207
230
|
let textContent = content.content || JSON.stringify(content, null, 2);
|
|
208
231
|
if (content.metadata_display) {
|
|
@@ -213,11 +236,11 @@ export function createToolResponse(content, isError = false, additionalFields =
|
|
|
213
236
|
content: [
|
|
214
237
|
{
|
|
215
238
|
type: 'text',
|
|
216
|
-
text: textContent
|
|
217
|
-
}
|
|
239
|
+
text: textContent,
|
|
240
|
+
},
|
|
218
241
|
],
|
|
219
242
|
isError: isError || content.isError || false,
|
|
220
|
-
...additionalFields
|
|
243
|
+
...additionalFields,
|
|
221
244
|
};
|
|
222
245
|
|
|
223
246
|
// Preserve continuation and metadata at top level
|
|
@@ -240,7 +263,7 @@ export function createToolResponse(content, isError = false, additionalFields =
|
|
|
240
263
|
},
|
|
241
264
|
],
|
|
242
265
|
isError,
|
|
243
|
-
...additionalFields
|
|
266
|
+
...additionalFields,
|
|
244
267
|
};
|
|
245
268
|
}
|
|
246
269
|
|
|
@@ -253,7 +276,7 @@ export function createToolResponse(content, isError = false, additionalFields =
|
|
|
253
276
|
},
|
|
254
277
|
],
|
|
255
278
|
isError,
|
|
256
|
-
...additionalFields
|
|
279
|
+
...additionalFields,
|
|
257
280
|
};
|
|
258
281
|
}
|
|
259
282
|
|
|
@@ -271,7 +294,7 @@ export function createToolError(message, error = null) {
|
|
|
271
294
|
response.error = {
|
|
272
295
|
message: errorText,
|
|
273
296
|
type: 'ToolError',
|
|
274
|
-
timestamp: new Date().toISOString()
|
|
297
|
+
timestamp: new Date().toISOString(),
|
|
275
298
|
};
|
|
276
299
|
|
|
277
300
|
return response;
|
|
@@ -11,7 +11,10 @@ import { randomUUID } from 'node:crypto';
|
|
|
11
11
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
12
12
|
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
|
|
13
13
|
import { createLogger } from '../utils/logger.js';
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
registerTransportSession,
|
|
16
|
+
clearTransportSession,
|
|
17
|
+
} from '../utils/sessionManager.js';
|
|
15
18
|
|
|
16
19
|
const logger = createLogger('http-transport');
|
|
17
20
|
|
|
@@ -44,13 +47,14 @@ export class HTTPTransportServer {
|
|
|
44
47
|
},
|
|
45
48
|
|
|
46
49
|
// Security settings
|
|
47
|
-
enableDnsRebindingProtection:
|
|
50
|
+
enableDnsRebindingProtection:
|
|
51
|
+
config.enableDnsRebindingProtection || false,
|
|
48
52
|
allowedHosts: config.allowedHosts || ['127.0.0.1', 'localhost'],
|
|
49
53
|
rateLimitEnabled: config.rateLimitEnabled || false,
|
|
50
54
|
rateLimitWindow: config.rateLimitWindow || 900000,
|
|
51
55
|
rateLimitMaxRequests: config.rateLimitMaxRequests || 1000,
|
|
52
56
|
|
|
53
|
-
...config
|
|
57
|
+
...config,
|
|
54
58
|
};
|
|
55
59
|
|
|
56
60
|
this.app = express();
|
|
@@ -78,10 +82,12 @@ export class HTTPTransportServer {
|
|
|
78
82
|
*/
|
|
79
83
|
setupMiddleware() {
|
|
80
84
|
// JSON parsing with size limit
|
|
81
|
-
this.app.use(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
+
this.app.use(
|
|
86
|
+
express.json({
|
|
87
|
+
limit: this.config.maxRequestSize,
|
|
88
|
+
strict: true,
|
|
89
|
+
}),
|
|
90
|
+
);
|
|
85
91
|
|
|
86
92
|
// Request timeout middleware
|
|
87
93
|
this.app.use((req, res, next) => {
|
|
@@ -90,8 +96,8 @@ export class HTTPTransportServer {
|
|
|
90
96
|
data: {
|
|
91
97
|
method: req.method,
|
|
92
98
|
path: req.path,
|
|
93
|
-
timeout: this.config.requestTimeout
|
|
94
|
-
}
|
|
99
|
+
timeout: this.config.requestTimeout,
|
|
100
|
+
},
|
|
95
101
|
});
|
|
96
102
|
if (!res.headersSent) {
|
|
97
103
|
res.status(408).json({
|
|
@@ -118,15 +124,17 @@ export class HTTPTransportServer {
|
|
|
118
124
|
|
|
119
125
|
// Clean old entries
|
|
120
126
|
const clientRequests = rateLimitMap.get(clientId) || [];
|
|
121
|
-
const validRequests = clientRequests.filter(
|
|
127
|
+
const validRequests = clientRequests.filter(
|
|
128
|
+
(time) => time > windowStart,
|
|
129
|
+
);
|
|
122
130
|
|
|
123
131
|
if (validRequests.length >= this.config.rateLimitMaxRequests) {
|
|
124
132
|
logger.warn('Rate limit exceeded', {
|
|
125
133
|
data: {
|
|
126
134
|
clientId,
|
|
127
135
|
requests: validRequests.length,
|
|
128
|
-
limit: this.config.rateLimitMaxRequests
|
|
129
|
-
}
|
|
136
|
+
limit: this.config.rateLimitMaxRequests,
|
|
137
|
+
},
|
|
130
138
|
});
|
|
131
139
|
res.status(429).json({
|
|
132
140
|
jsonrpc: '2.0',
|
|
@@ -147,8 +155,8 @@ export class HTTPTransportServer {
|
|
|
147
155
|
logger.debug('Rate limiting enabled', {
|
|
148
156
|
data: {
|
|
149
157
|
window: this.config.rateLimitWindow,
|
|
150
|
-
maxRequests: this.config.rateLimitMaxRequests
|
|
151
|
-
}
|
|
158
|
+
maxRequests: this.config.rateLimitMaxRequests,
|
|
159
|
+
},
|
|
152
160
|
});
|
|
153
161
|
}
|
|
154
162
|
|
|
@@ -156,7 +164,7 @@ export class HTTPTransportServer {
|
|
|
156
164
|
if (this.config.enableCors) {
|
|
157
165
|
this.app.use(cors(this.config.corsOptions));
|
|
158
166
|
logger.debug('CORS enabled for HTTP transport', {
|
|
159
|
-
data: { corsOptions: this.config.corsOptions }
|
|
167
|
+
data: { corsOptions: this.config.corsOptions },
|
|
160
168
|
});
|
|
161
169
|
}
|
|
162
170
|
|
|
@@ -166,8 +174,8 @@ export class HTTPTransportServer {
|
|
|
166
174
|
data: {
|
|
167
175
|
method: req.method,
|
|
168
176
|
path: req.path,
|
|
169
|
-
sessionId: req.headers['mcp-session-id']
|
|
170
|
-
}
|
|
177
|
+
sessionId: req.headers['mcp-session-id'],
|
|
178
|
+
},
|
|
171
179
|
});
|
|
172
180
|
next();
|
|
173
181
|
});
|
|
@@ -199,7 +207,7 @@ export class HTTPTransportServer {
|
|
|
199
207
|
transport: 'http',
|
|
200
208
|
server: this.mcpServer ? 'connected' : 'disconnected',
|
|
201
209
|
sessions: this.transports.size,
|
|
202
|
-
timestamp: new Date().toISOString()
|
|
210
|
+
timestamp: new Date().toISOString(),
|
|
203
211
|
});
|
|
204
212
|
});
|
|
205
213
|
|
|
@@ -212,9 +220,9 @@ export class HTTPTransportServer {
|
|
|
212
220
|
endpoints: {
|
|
213
221
|
mcp: '/mcp',
|
|
214
222
|
health: '/health',
|
|
215
|
-
info: '/info'
|
|
223
|
+
info: '/info',
|
|
216
224
|
},
|
|
217
|
-
sessions: this.transports.size
|
|
225
|
+
sessions: this.transports.size,
|
|
218
226
|
});
|
|
219
227
|
});
|
|
220
228
|
}
|
|
@@ -238,14 +246,15 @@ export class HTTPTransportServer {
|
|
|
238
246
|
logger.warn('Maximum concurrent sessions reached', {
|
|
239
247
|
data: {
|
|
240
248
|
currentSessions: this.transports.size,
|
|
241
|
-
maxSessions: this.config.maxConcurrentSessions
|
|
242
|
-
}
|
|
249
|
+
maxSessions: this.config.maxConcurrentSessions,
|
|
250
|
+
},
|
|
243
251
|
});
|
|
244
252
|
res.status(503).json({
|
|
245
253
|
jsonrpc: '2.0',
|
|
246
254
|
error: {
|
|
247
255
|
code: -32000,
|
|
248
|
-
message:
|
|
256
|
+
message:
|
|
257
|
+
'Maximum concurrent sessions reached. Please try again later.',
|
|
249
258
|
},
|
|
250
259
|
id: null,
|
|
251
260
|
});
|
|
@@ -257,9 +266,12 @@ export class HTTPTransportServer {
|
|
|
257
266
|
logger.info('Created new MCP transport', {});
|
|
258
267
|
} else {
|
|
259
268
|
// Invalid request
|
|
260
|
-
logger.warn(
|
|
261
|
-
|
|
262
|
-
|
|
269
|
+
logger.warn(
|
|
270
|
+
'Invalid MCP request - no session ID or not initialize request',
|
|
271
|
+
{
|
|
272
|
+
data: { sessionId, hasInitialize: isInitializeRequest(req.body) },
|
|
273
|
+
},
|
|
274
|
+
);
|
|
263
275
|
res.status(400).json({
|
|
264
276
|
jsonrpc: '2.0',
|
|
265
277
|
error: {
|
|
@@ -273,7 +285,6 @@ export class HTTPTransportServer {
|
|
|
273
285
|
|
|
274
286
|
// Handle the request through the transport
|
|
275
287
|
await transport.handleRequest(req, res, req.body);
|
|
276
|
-
|
|
277
288
|
} catch (error) {
|
|
278
289
|
logger.error('Error handling MCP request', { error });
|
|
279
290
|
if (!res.headersSent) {
|
|
@@ -296,7 +307,9 @@ export class HTTPTransportServer {
|
|
|
296
307
|
const sessionId = req.headers['mcp-session-id'];
|
|
297
308
|
|
|
298
309
|
if (!sessionId || !this.transports.has(sessionId)) {
|
|
299
|
-
logger.warn('SSE request with invalid session ID', {
|
|
310
|
+
logger.warn('SSE request with invalid session ID', {
|
|
311
|
+
data: { sessionId },
|
|
312
|
+
});
|
|
300
313
|
res.status(400).send('Invalid or missing session ID');
|
|
301
314
|
return;
|
|
302
315
|
}
|
|
@@ -307,7 +320,10 @@ export class HTTPTransportServer {
|
|
|
307
320
|
await transport.handleRequest(req, res);
|
|
308
321
|
logger.debug('SSE connection established', { data: { sessionId } });
|
|
309
322
|
} catch (error) {
|
|
310
|
-
logger.error('Error handling SSE request', {
|
|
323
|
+
logger.error('Error handling SSE request', {
|
|
324
|
+
error,
|
|
325
|
+
data: { sessionId },
|
|
326
|
+
});
|
|
311
327
|
if (!res.headersSent) {
|
|
312
328
|
res.status(500).send('Internal server error');
|
|
313
329
|
}
|
|
@@ -321,7 +337,9 @@ export class HTTPTransportServer {
|
|
|
321
337
|
const sessionId = req.headers['mcp-session-id'];
|
|
322
338
|
|
|
323
339
|
if (!sessionId || !this.transports.has(sessionId)) {
|
|
324
|
-
logger.warn('Session termination with invalid session ID', {
|
|
340
|
+
logger.warn('Session termination with invalid session ID', {
|
|
341
|
+
data: { sessionId },
|
|
342
|
+
});
|
|
325
343
|
res.status(400).send('Invalid or missing session ID');
|
|
326
344
|
return;
|
|
327
345
|
}
|
|
@@ -364,7 +382,7 @@ export class HTTPTransportServer {
|
|
|
364
382
|
this.cleanupSession(transport.sessionId);
|
|
365
383
|
clearTransportSession(transport);
|
|
366
384
|
logger.debug('Transport session closed', {
|
|
367
|
-
data: { sessionId: transport.sessionId }
|
|
385
|
+
data: { sessionId: transport.sessionId },
|
|
368
386
|
});
|
|
369
387
|
}
|
|
370
388
|
};
|
|
@@ -431,7 +449,7 @@ export class HTTPTransportServer {
|
|
|
431
449
|
|
|
432
450
|
this.cleanupInterval = setInterval(() => {
|
|
433
451
|
logger.debug('Running session cleanup', {
|
|
434
|
-
data: { activeSessions: this.transports.size }
|
|
452
|
+
data: { activeSessions: this.transports.size },
|
|
435
453
|
});
|
|
436
454
|
|
|
437
455
|
// The timeout mechanism handles cleanup automatically,
|
|
@@ -439,7 +457,10 @@ export class HTTPTransportServer {
|
|
|
439
457
|
}, this.config.sessionCleanupInterval);
|
|
440
458
|
|
|
441
459
|
// Unref the interval so it doesn't keep the process alive
|
|
442
|
-
if (
|
|
460
|
+
if (
|
|
461
|
+
this.cleanupInterval &&
|
|
462
|
+
typeof this.cleanupInterval.unref === 'function'
|
|
463
|
+
) {
|
|
443
464
|
this.cleanupInterval.unref();
|
|
444
465
|
}
|
|
445
466
|
}
|
|
@@ -453,28 +474,34 @@ export class HTTPTransportServer {
|
|
|
453
474
|
}
|
|
454
475
|
|
|
455
476
|
return new Promise((resolve, reject) => {
|
|
456
|
-
this.server = this.app.listen(
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
477
|
+
this.server = this.app.listen(
|
|
478
|
+
this.config.port,
|
|
479
|
+
this.config.host,
|
|
480
|
+
(err) => {
|
|
481
|
+
if (err) {
|
|
482
|
+
logger.error('Failed to start HTTP transport server', {
|
|
483
|
+
error: err,
|
|
484
|
+
});
|
|
485
|
+
reject(err);
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
462
488
|
|
|
463
|
-
|
|
464
|
-
|
|
489
|
+
this.isStarted = true;
|
|
490
|
+
this.startSessionCleanup();
|
|
465
491
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
492
|
+
const address = this.server.address();
|
|
493
|
+
logger.info('HTTP transport server started', {
|
|
494
|
+
data: {
|
|
495
|
+
host: address.address,
|
|
496
|
+
port: address.port,
|
|
497
|
+
endpoint: `http://${this.config.host}:${address.port}/mcp`,
|
|
498
|
+
sessionTimeout: this.config.sessionTimeout,
|
|
499
|
+
maxSessions: this.config.maxConcurrentSessions,
|
|
500
|
+
},
|
|
501
|
+
});
|
|
502
|
+
resolve(address);
|
|
503
|
+
},
|
|
504
|
+
);
|
|
478
505
|
});
|
|
479
506
|
}
|
|
480
507
|
|
|
@@ -538,8 +565,8 @@ export class HTTPTransportServer {
|
|
|
538
565
|
maxRequestSize: this.config.maxRequestSize,
|
|
539
566
|
corsEnabled: this.config.enableCors,
|
|
540
567
|
rateLimitEnabled: this.config.rateLimitEnabled,
|
|
541
|
-
dnsRebindingProtection: this.config.enableDnsRebindingProtection
|
|
542
|
-
}
|
|
568
|
+
dnsRebindingProtection: this.config.enableDnsRebindingProtection,
|
|
569
|
+
},
|
|
543
570
|
};
|
|
544
571
|
}
|
|
545
572
|
}
|
|
@@ -33,7 +33,12 @@ export class ContextProcessorError extends Error {
|
|
|
33
33
|
* Supported image extensions (everything else is treated as text)
|
|
34
34
|
*/
|
|
35
35
|
const SUPPORTED_IMAGE_EXTENSIONS = [
|
|
36
|
-
'.jpg',
|
|
36
|
+
'.jpg',
|
|
37
|
+
'.jpeg',
|
|
38
|
+
'.png',
|
|
39
|
+
'.gif',
|
|
40
|
+
'.webp',
|
|
41
|
+
'.bmp',
|
|
37
42
|
];
|
|
38
43
|
|
|
39
44
|
/**
|
|
@@ -48,19 +53,24 @@ async function validateFilePath(filePath, options = {}) {
|
|
|
48
53
|
if (!filePath || typeof filePath !== 'string') {
|
|
49
54
|
throw new ContextProcessorError(
|
|
50
55
|
'File path must be a non-empty string',
|
|
51
|
-
'INVALID_PATH'
|
|
56
|
+
'INVALID_PATH',
|
|
52
57
|
);
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
// Convert to absolute path
|
|
56
61
|
// For relative paths, resolve from the client's working directory if provided,
|
|
57
62
|
// otherwise use process.cwd()
|
|
58
|
-
const absolutePath = isAbsolute(filePath)
|
|
63
|
+
const absolutePath = isAbsolute(filePath)
|
|
64
|
+
? filePath
|
|
65
|
+
: resolve(options.clientCwd || process.cwd(), filePath);
|
|
59
66
|
|
|
60
67
|
// Security check is now optional and disabled by default
|
|
61
68
|
if (options.enforceSecurityCheck) {
|
|
62
|
-
const allowedDirs = options.allowedDirectories || [
|
|
63
|
-
|
|
69
|
+
const allowedDirs = options.allowedDirectories || [
|
|
70
|
+
process.cwd(),
|
|
71
|
+
PROJECT_ROOT,
|
|
72
|
+
];
|
|
73
|
+
const isAllowed = allowedDirs.some((dir) => {
|
|
64
74
|
const resolvedDir = resolve(dir);
|
|
65
75
|
return absolutePath.startsWith(resolvedDir);
|
|
66
76
|
});
|
|
@@ -69,7 +79,7 @@ async function validateFilePath(filePath, options = {}) {
|
|
|
69
79
|
throw new ContextProcessorError(
|
|
70
80
|
'File access denied: path outside allowed directories',
|
|
71
81
|
'SECURITY_VIOLATION',
|
|
72
|
-
{ path: absolutePath, allowedDirs }
|
|
82
|
+
{ path: absolutePath, allowedDirs },
|
|
73
83
|
);
|
|
74
84
|
}
|
|
75
85
|
}
|
|
@@ -81,7 +91,7 @@ async function validateFilePath(filePath, options = {}) {
|
|
|
81
91
|
throw new ContextProcessorError(
|
|
82
92
|
`File not accessible: ${error.message}`,
|
|
83
93
|
'FILE_ACCESS_ERROR',
|
|
84
|
-
{ path: absolutePath }
|
|
94
|
+
{ path: absolutePath },
|
|
85
95
|
);
|
|
86
96
|
}
|
|
87
97
|
|
|
@@ -106,7 +116,7 @@ export async function processFileContent(filePath, options = {}) {
|
|
|
106
116
|
if (!dataUrlMatch) {
|
|
107
117
|
throw new ContextProcessorError(
|
|
108
118
|
'Invalid data URL format',
|
|
109
|
-
'INVALID_DATA_URL'
|
|
119
|
+
'INVALID_DATA_URL',
|
|
110
120
|
);
|
|
111
121
|
}
|
|
112
122
|
|
|
@@ -123,7 +133,7 @@ export async function processFileContent(filePath, options = {}) {
|
|
|
123
133
|
error: null,
|
|
124
134
|
lastModified: new Date(),
|
|
125
135
|
encoding: 'base64',
|
|
126
|
-
mimeType
|
|
136
|
+
mimeType,
|
|
127
137
|
};
|
|
128
138
|
}
|
|
129
139
|
|
|
@@ -135,11 +145,9 @@ export async function processFileContent(filePath, options = {}) {
|
|
|
135
145
|
|
|
136
146
|
// Check if it's actually a file (not a directory)
|
|
137
147
|
if (!fileStats.isFile()) {
|
|
138
|
-
throw new ContextProcessorError(
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
{ path: validatedPath }
|
|
142
|
-
);
|
|
148
|
+
throw new ContextProcessorError('Path is not a file', 'NOT_A_FILE', {
|
|
149
|
+
path: validatedPath,
|
|
150
|
+
});
|
|
143
151
|
}
|
|
144
152
|
|
|
145
153
|
const result = {
|
|
@@ -171,7 +179,6 @@ export async function processFileContent(filePath, options = {}) {
|
|
|
171
179
|
result.content = buffer.toString('base64');
|
|
172
180
|
result.mimeType = getMimeType(extension);
|
|
173
181
|
result.encoding = 'base64';
|
|
174
|
-
|
|
175
182
|
} else {
|
|
176
183
|
// Read everything else as text
|
|
177
184
|
result.type = 'text';
|
|
@@ -189,13 +196,15 @@ export async function processFileContent(filePath, options = {}) {
|
|
|
189
196
|
}
|
|
190
197
|
|
|
191
198
|
return result;
|
|
192
|
-
|
|
193
199
|
} catch (error) {
|
|
194
200
|
return {
|
|
195
201
|
path: filePath,
|
|
196
202
|
originalPath: filePath,
|
|
197
203
|
type: 'error',
|
|
198
|
-
error:
|
|
204
|
+
error:
|
|
205
|
+
error instanceof ContextProcessorError
|
|
206
|
+
? error.message
|
|
207
|
+
: `Unexpected error: ${error.message}`,
|
|
199
208
|
errorCode: error.code || 'UNKNOWN_ERROR',
|
|
200
209
|
content: null,
|
|
201
210
|
lastModified: null,
|
|
@@ -213,13 +222,13 @@ export async function processMultipleFiles(filePaths, options = {}) {
|
|
|
213
222
|
if (!Array.isArray(filePaths)) {
|
|
214
223
|
throw new ContextProcessorError(
|
|
215
224
|
'filePaths must be an array',
|
|
216
|
-
'INVALID_INPUT'
|
|
225
|
+
'INVALID_INPUT',
|
|
217
226
|
);
|
|
218
227
|
}
|
|
219
228
|
|
|
220
229
|
// Process files in parallel but isolate errors
|
|
221
230
|
const results = await Promise.allSettled(
|
|
222
|
-
filePaths.map(path => processFileContent(path, options))
|
|
231
|
+
filePaths.map((path) => processFileContent(path, options)),
|
|
223
232
|
);
|
|
224
233
|
|
|
225
234
|
// Convert Promise.allSettled results to consistent format
|
|
@@ -260,7 +269,7 @@ export async function processWebSearchContext(query, options = {}) {
|
|
|
260
269
|
// - DuckDuckGo API
|
|
261
270
|
// - Custom search engines
|
|
262
271
|
placeholder: true,
|
|
263
|
-
message: 'Web search integration placeholder - not yet implemented'
|
|
272
|
+
message: 'Web search integration placeholder - not yet implemented',
|
|
264
273
|
};
|
|
265
274
|
}
|
|
266
275
|
|
|
@@ -292,20 +301,25 @@ export async function processUnifiedContext(contextRequest, options = {}) {
|
|
|
292
301
|
if (contextRequest.images && Array.isArray(contextRequest.images)) {
|
|
293
302
|
result.images = await processMultipleFiles(contextRequest.images, {
|
|
294
303
|
...options,
|
|
295
|
-
imageProcessingMode: true // Placeholder for future image-specific processing
|
|
304
|
+
imageProcessingMode: true, // Placeholder for future image-specific processing
|
|
296
305
|
});
|
|
297
306
|
}
|
|
298
307
|
|
|
299
308
|
// Process web search if provided
|
|
300
|
-
if (
|
|
301
|
-
|
|
309
|
+
if (
|
|
310
|
+
contextRequest.webSearch &&
|
|
311
|
+
typeof contextRequest.webSearch === 'string'
|
|
312
|
+
) {
|
|
313
|
+
result.webSearch = await processWebSearchContext(
|
|
314
|
+
contextRequest.webSearch,
|
|
315
|
+
options,
|
|
316
|
+
);
|
|
302
317
|
}
|
|
303
|
-
|
|
304
318
|
} catch (error) {
|
|
305
319
|
result.errors.push({
|
|
306
320
|
type: 'unified_processing_error',
|
|
307
321
|
message: error.message,
|
|
308
|
-
code: error.code || 'UNKNOWN_ERROR'
|
|
322
|
+
code: error.code || 'UNKNOWN_ERROR',
|
|
309
323
|
});
|
|
310
324
|
}
|
|
311
325
|
|
|
@@ -323,9 +337,11 @@ export function createFileContext(processedFiles, options = {}) {
|
|
|
323
337
|
return null;
|
|
324
338
|
}
|
|
325
339
|
|
|
326
|
-
const textFiles = processedFiles.filter(f => f.type === 'text' && !f.error);
|
|
327
|
-
const imageFiles = processedFiles.filter(
|
|
328
|
-
|
|
340
|
+
const textFiles = processedFiles.filter((f) => f.type === 'text' && !f.error);
|
|
341
|
+
const imageFiles = processedFiles.filter(
|
|
342
|
+
(f) => f.type === 'image' && !f.error,
|
|
343
|
+
);
|
|
344
|
+
const errors = processedFiles.filter((f) => f.error);
|
|
329
345
|
|
|
330
346
|
const includeErrors = options.includeErrors !== false; // Default to true
|
|
331
347
|
|
|
@@ -377,11 +393,13 @@ export function createFileContext(processedFiles, options = {}) {
|
|
|
377
393
|
data: image.content,
|
|
378
394
|
},
|
|
379
395
|
// Add metadata for debugging
|
|
380
|
-
metadata: options.includeMetadata
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
396
|
+
metadata: options.includeMetadata
|
|
397
|
+
? {
|
|
398
|
+
path: image.originalPath || image.path,
|
|
399
|
+
size: image.size,
|
|
400
|
+
lastModified: image.lastModified,
|
|
401
|
+
}
|
|
402
|
+
: undefined,
|
|
385
403
|
});
|
|
386
404
|
}
|
|
387
405
|
|
|
@@ -428,7 +446,7 @@ export async function validateFilePaths(filePaths, options = {}) {
|
|
|
428
446
|
if (!Array.isArray(filePaths)) {
|
|
429
447
|
throw new ContextProcessorError(
|
|
430
448
|
'filePaths must be an array',
|
|
431
|
-
'INVALID_INPUT'
|
|
449
|
+
'INVALID_INPUT',
|
|
432
450
|
);
|
|
433
451
|
}
|
|
434
452
|
|
|
@@ -444,13 +462,13 @@ export async function validateFilePaths(filePaths, options = {}) {
|
|
|
444
462
|
results.valid.push({
|
|
445
463
|
originalPath: path,
|
|
446
464
|
validatedPath,
|
|
447
|
-
isValid: true
|
|
465
|
+
isValid: true,
|
|
448
466
|
});
|
|
449
467
|
} catch (error) {
|
|
450
468
|
const errorInfo = {
|
|
451
469
|
path,
|
|
452
470
|
error: error.message,
|
|
453
|
-
code: error.code
|
|
471
|
+
code: error.code,
|
|
454
472
|
};
|
|
455
473
|
|
|
456
474
|
if (error.code === 'SECURITY_VIOLATION') {
|
|
@@ -463,4 +481,3 @@ export async function validateFilePaths(filePaths, options = {}) {
|
|
|
463
481
|
|
|
464
482
|
return results;
|
|
465
483
|
}
|
|
466
|
-
|