@anyshift/mcp-proxy 0.6.7-dev → 0.6.8
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/dist/fileWriter/writer.js +132 -13
- package/package.json +3 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import Papa from 'papaparse';
|
|
3
4
|
import { generateToolId } from '../utils/filename.js';
|
|
4
5
|
import { generateQueryAssistSchema, generateJsonlQueryAssistSchema } from './schema.js';
|
|
5
6
|
import { parseDynatraceDqlResponse, isDynatraceDqlTool, } from './dynatrace.js';
|
|
@@ -10,18 +11,85 @@ const FILE_WRITTEN_MESSAGE = `To read this file, use the "mcp__file_query__execu
|
|
|
10
11
|
|
|
11
12
|
IMPORTANT for supporting facts: This tool_id CANNOT be used as the proxy_tool_id in your output's supporting_facts evidence. You must read the file using one of the tools above and use THAT tool's tool_id as the proxy_tool_id to support facts in your output.`;
|
|
12
13
|
/**
|
|
13
|
-
* Detect
|
|
14
|
+
* Detect if content is CSV and convert to JSONL using papaparse
|
|
15
|
+
* Handles: auto-delimiter detection, header detection, quoted fields, etc.
|
|
16
|
+
*/
|
|
17
|
+
function detectAndConvertCsv(content) {
|
|
18
|
+
const trimmed = content.trim();
|
|
19
|
+
// Skip if empty
|
|
20
|
+
if (!trimmed) {
|
|
21
|
+
return { isCsv: false, hasHeaders: false };
|
|
22
|
+
}
|
|
23
|
+
// Skip if it looks like JSON
|
|
24
|
+
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
|
25
|
+
return { isCsv: false, hasHeaders: false };
|
|
26
|
+
}
|
|
27
|
+
// First, try parsing WITH headers
|
|
28
|
+
const resultWithHeaders = Papa.parse(trimmed, {
|
|
29
|
+
header: true,
|
|
30
|
+
skipEmptyLines: true,
|
|
31
|
+
dynamicTyping: false,
|
|
32
|
+
});
|
|
33
|
+
// Check if parsing was successful and looks like valid CSV
|
|
34
|
+
const hasFields = resultWithHeaders.meta.fields && resultWithHeaders.meta.fields.length > 0;
|
|
35
|
+
const hasData = resultWithHeaders.data.length > 0;
|
|
36
|
+
const hasDelimiter = resultWithHeaders.meta.delimiter !== undefined;
|
|
37
|
+
if (!hasFields || !hasData || !hasDelimiter) {
|
|
38
|
+
return { isCsv: false, hasHeaders: false };
|
|
39
|
+
}
|
|
40
|
+
// Check if headers look valid (not numeric, reasonable length)
|
|
41
|
+
const fields = resultWithHeaders.meta.fields;
|
|
42
|
+
const looksLikeHeaders = fields.every(h => {
|
|
43
|
+
const cleaned = h.trim();
|
|
44
|
+
return (cleaned.length > 0 &&
|
|
45
|
+
cleaned.length < 100 &&
|
|
46
|
+
!/^[\d.]+$/.test(cleaned) // Not purely numeric
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
if (looksLikeHeaders) {
|
|
50
|
+
// Use header mode - convert to JSONL with objects
|
|
51
|
+
const jsonLines = resultWithHeaders.data.map(row => JSON.stringify(row));
|
|
52
|
+
return {
|
|
53
|
+
isCsv: true,
|
|
54
|
+
jsonlContent: jsonLines.join('\n'),
|
|
55
|
+
headers: fields,
|
|
56
|
+
rowCount: jsonLines.length,
|
|
57
|
+
hasHeaders: true,
|
|
58
|
+
delimiter: resultWithHeaders.meta.delimiter,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
// Headers don't look valid - parse without headers (as arrays)
|
|
63
|
+
const resultNoHeaders = Papa.parse(trimmed, {
|
|
64
|
+
header: false,
|
|
65
|
+
skipEmptyLines: true,
|
|
66
|
+
dynamicTyping: false,
|
|
67
|
+
});
|
|
68
|
+
const jsonLines = resultNoHeaders.data.map(row => JSON.stringify(row));
|
|
69
|
+
return {
|
|
70
|
+
isCsv: true,
|
|
71
|
+
jsonlContent: jsonLines.join('\n'),
|
|
72
|
+
headers: undefined,
|
|
73
|
+
rowCount: jsonLines.length,
|
|
74
|
+
hasHeaders: false,
|
|
75
|
+
delimiter: resultNoHeaders.meta.delimiter,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Detect whether content is JSON, JSONL, CSV, or plain text
|
|
81
|
+
* Returns format and CSV conversion result (if applicable) to avoid double parsing
|
|
14
82
|
*/
|
|
15
83
|
function detectContentFormat(content) {
|
|
16
84
|
const trimmed = content.trim();
|
|
17
85
|
// Empty content
|
|
18
86
|
if (!trimmed)
|
|
19
|
-
return 'text';
|
|
87
|
+
return { format: 'text' };
|
|
20
88
|
// Try parsing as single JSON object/array
|
|
21
89
|
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
|
22
90
|
try {
|
|
23
91
|
JSON.parse(trimmed);
|
|
24
|
-
return 'json';
|
|
92
|
+
return { format: 'json' };
|
|
25
93
|
}
|
|
26
94
|
catch {
|
|
27
95
|
// Could be JSONL starting with {
|
|
@@ -42,10 +110,15 @@ function detectContentFormat(content) {
|
|
|
42
110
|
}
|
|
43
111
|
});
|
|
44
112
|
if (allSampledLinesAreJson) {
|
|
45
|
-
return 'jsonl';
|
|
113
|
+
return { format: 'jsonl' };
|
|
46
114
|
}
|
|
47
115
|
}
|
|
48
|
-
|
|
116
|
+
// Check for CSV format - store result to avoid re-parsing later
|
|
117
|
+
const csvResult = detectAndConvertCsv(trimmed);
|
|
118
|
+
if (csvResult.isCsv) {
|
|
119
|
+
return { format: 'csv', csvConversion: csvResult };
|
|
120
|
+
}
|
|
121
|
+
return { format: 'text' };
|
|
49
122
|
}
|
|
50
123
|
/**
|
|
51
124
|
* Get file extension based on content format
|
|
@@ -353,24 +426,66 @@ export async function handleToolResponse(config, toolName, args, responseData) {
|
|
|
353
426
|
// Success case: write to file
|
|
354
427
|
try {
|
|
355
428
|
// Detect content format for appropriate extension and schema
|
|
356
|
-
|
|
357
|
-
const
|
|
429
|
+
// This also returns CSV conversion result if applicable, to avoid double parsing
|
|
430
|
+
const detection = detectContentFormat(contentToWrite);
|
|
431
|
+
// Handle format conversion to JSONL for non-JSON formats
|
|
432
|
+
let finalContent = contentToWrite;
|
|
433
|
+
let finalFormat = detection.format;
|
|
434
|
+
let conversionWarning;
|
|
435
|
+
if (detection.format === 'csv' && detection.csvConversion) {
|
|
436
|
+
// CSV: Use the already-converted JSONL from detection
|
|
437
|
+
const csvResult = detection.csvConversion;
|
|
438
|
+
if (csvResult.jsonlContent) {
|
|
439
|
+
finalContent = csvResult.jsonlContent;
|
|
440
|
+
finalFormat = 'jsonl';
|
|
441
|
+
const delimiterName = csvResult.delimiter === '\t' ? 'tab' :
|
|
442
|
+
csvResult.delimiter === ';' ? 'semicolon' :
|
|
443
|
+
csvResult.delimiter === '|' ? 'pipe' : 'comma';
|
|
444
|
+
if (csvResult.hasHeaders) {
|
|
445
|
+
conversionWarning = `NOTE: Original data was CSV/DSV format (${delimiterName}-delimited). ` +
|
|
446
|
+
`Converted to JSONL assuming first row was the header. ` +
|
|
447
|
+
`Headers detected: [${csvResult.headers?.join(', ')}]. ` +
|
|
448
|
+
`Rows converted: ${csvResult.rowCount}. ` +
|
|
449
|
+
`If the data looks incorrect, the first row may not have been a header.`;
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
conversionWarning = `NOTE: Original data was CSV/DSV format (${delimiterName}-delimited) without detectable headers. ` +
|
|
453
|
+
`Converted to JSONL as arrays (one array per row). ` +
|
|
454
|
+
`Each row is: ["field1", "field2", ...]. ` +
|
|
455
|
+
`Rows converted: ${csvResult.rowCount}. ` +
|
|
456
|
+
`Access fields by index: .[0], .[1], etc.`;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
else if (detection.format === 'text') {
|
|
461
|
+
// Plain text/markdown/other: Wrap each line as JSON object for jq compatibility
|
|
462
|
+
const lines = contentToWrite.split('\n');
|
|
463
|
+
const jsonLines = lines
|
|
464
|
+
.map((line, index) => JSON.stringify({ line_number: index + 1, content: line }));
|
|
465
|
+
finalContent = jsonLines.join('\n');
|
|
466
|
+
finalFormat = 'jsonl';
|
|
467
|
+
conversionWarning = `NOTE: Original data was plain text (not JSON/CSV). ` +
|
|
468
|
+
`Artificially converted to JSONL to make it accessible to the jq tool. ` +
|
|
469
|
+
`Each line is wrapped as: {"line_number": N, "content": "original line text"}. ` +
|
|
470
|
+
`Total lines: ${lines.length}.`;
|
|
471
|
+
}
|
|
472
|
+
const extension = getFileExtension(finalFormat);
|
|
358
473
|
const filename = `${tool_id}${extension}`;
|
|
359
474
|
const filepath = path.join(config.outputPath, filename);
|
|
360
475
|
// Ensure output directory exists
|
|
361
476
|
await fs.mkdir(config.outputPath, { recursive: true });
|
|
362
|
-
// Write the
|
|
363
|
-
await fs.writeFile(filepath,
|
|
477
|
+
// Write the content (converted if CSV)
|
|
478
|
+
await fs.writeFile(filepath, finalContent);
|
|
364
479
|
// Generate query-assist schema based on format
|
|
365
480
|
let fileSchema;
|
|
366
|
-
if (
|
|
481
|
+
if (finalFormat === 'jsonl') {
|
|
367
482
|
// JSONL format: sample multiple records for schema
|
|
368
|
-
const lines =
|
|
483
|
+
const lines = finalContent.trim().split('\n').filter(l => l.trim());
|
|
369
484
|
fileSchema = generateJsonlQueryAssistSchema(lines, {
|
|
370
485
|
maxDepth: config.schemaMaxDepth ?? 2,
|
|
371
486
|
maxPaths: config.schemaMaxPaths ?? 20,
|
|
372
487
|
maxKeys: config.schemaMaxKeys ?? 50,
|
|
373
|
-
dataSize:
|
|
488
|
+
dataSize: finalContent.length,
|
|
374
489
|
totalLines: lines.length,
|
|
375
490
|
sampleSize: 5
|
|
376
491
|
});
|
|
@@ -388,12 +503,16 @@ export async function handleToolResponse(config, toolName, args, responseData) {
|
|
|
388
503
|
dataSize: contentLength
|
|
389
504
|
});
|
|
390
505
|
}
|
|
506
|
+
// Build message with optional conversion warning
|
|
507
|
+
const message = conversionWarning
|
|
508
|
+
? `${conversionWarning}\n\n${FILE_WRITTEN_MESSAGE}`
|
|
509
|
+
: FILE_WRITTEN_MESSAGE;
|
|
391
510
|
return {
|
|
392
511
|
tool_id,
|
|
393
512
|
wroteToFile: true,
|
|
394
513
|
filePath: filepath,
|
|
395
514
|
fileSchema,
|
|
396
|
-
message
|
|
515
|
+
message,
|
|
397
516
|
};
|
|
398
517
|
}
|
|
399
518
|
catch (error) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anyshift/mcp-proxy",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.8",
|
|
4
4
|
"description": "Generic MCP proxy that adds truncation, file writing, and JQ capabilities to any MCP server",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"@modelcontextprotocol/sdk": "^1.24.0",
|
|
17
17
|
"glob": "^13.0.0",
|
|
18
|
+
"papaparse": "^5.5.3",
|
|
18
19
|
"zod": "^3.24.2"
|
|
19
20
|
},
|
|
20
21
|
"devDependencies": {
|
|
@@ -22,6 +23,7 @@
|
|
|
22
23
|
"@types/glob": "^9.0.0",
|
|
23
24
|
"@types/jest": "^30.0.0",
|
|
24
25
|
"@types/node": "^22.0.0",
|
|
26
|
+
"@types/papaparse": "^5.5.2",
|
|
25
27
|
"jest": "^30.2.0",
|
|
26
28
|
"ts-jest": "^29.4.5",
|
|
27
29
|
"tsx": "^4.7.0",
|