@anyshift/mcp-proxy 0.3.2 → 0.3.4
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/schema.d.ts +11 -0
- package/dist/fileWriter/schema.js +114 -0
- package/dist/fileWriter/writer.js +68 -4
- package/dist/jq/handler.js +4 -3
- package/dist/jq/tool.js +2 -2
- package/package.json +1 -1
|
@@ -32,6 +32,10 @@ export interface QueryAssistOptions {
|
|
|
32
32
|
maxKeys?: number;
|
|
33
33
|
dataSize?: number;
|
|
34
34
|
}
|
|
35
|
+
export interface JsonlSchemaOptions extends QueryAssistOptions {
|
|
36
|
+
totalLines: number;
|
|
37
|
+
sampleSize?: number;
|
|
38
|
+
}
|
|
35
39
|
/**
|
|
36
40
|
* Generate query-assist schema for JSON data
|
|
37
41
|
* Main entry point for schema generation
|
|
@@ -40,3 +44,10 @@ export interface QueryAssistOptions {
|
|
|
40
44
|
* @returns Compact text schema optimized for JQ queries
|
|
41
45
|
*/
|
|
42
46
|
export declare function generateQueryAssistSchema(data: unknown, options?: QueryAssistOptions): string;
|
|
47
|
+
/**
|
|
48
|
+
* Generate schema for JSONL data by sampling multiple records
|
|
49
|
+
* @param lines - Array of raw JSON lines
|
|
50
|
+
* @param options - Configuration options including totalLines and sampleSize
|
|
51
|
+
* @returns Formatted text schema optimized for JQ queries on JSONL files
|
|
52
|
+
*/
|
|
53
|
+
export declare function generateJsonlQueryAssistSchema(lines: string[], options: JsonlSchemaOptions): string;
|
|
@@ -268,3 +268,117 @@ export function generateQueryAssistSchema(data, options = {}) {
|
|
|
268
268
|
// Format as text
|
|
269
269
|
return formatQueryAssist(selectedPaths, limits, dataSize);
|
|
270
270
|
}
|
|
271
|
+
/**
|
|
272
|
+
* Select sample indices evenly distributed through the data
|
|
273
|
+
*/
|
|
274
|
+
function selectSampleIndices(totalLines, sampleSize) {
|
|
275
|
+
if (totalLines <= sampleSize) {
|
|
276
|
+
return Array.from({ length: totalLines }, (_, i) => i);
|
|
277
|
+
}
|
|
278
|
+
const indices = [0]; // Always include first
|
|
279
|
+
const step = Math.floor(totalLines / (sampleSize - 1));
|
|
280
|
+
for (let i = 1; i < sampleSize - 1; i++) {
|
|
281
|
+
indices.push(i * step);
|
|
282
|
+
}
|
|
283
|
+
indices.push(totalLines - 1); // Always include last
|
|
284
|
+
return indices;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Analyze schema consistency across sampled records
|
|
288
|
+
*/
|
|
289
|
+
function analyzeSchemaConsistency(records) {
|
|
290
|
+
if (records.length < 2) {
|
|
291
|
+
return { isConsistent: true, optionalFields: [] };
|
|
292
|
+
}
|
|
293
|
+
// Collect all keys from all records
|
|
294
|
+
const allKeys = new Set();
|
|
295
|
+
const keyPresenceCount = new Map();
|
|
296
|
+
for (const record of records) {
|
|
297
|
+
if (record && typeof record === 'object' && !Array.isArray(record)) {
|
|
298
|
+
const keys = Object.keys(record);
|
|
299
|
+
for (const key of keys) {
|
|
300
|
+
allKeys.add(key);
|
|
301
|
+
keyPresenceCount.set(key, (keyPresenceCount.get(key) || 0) + 1);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
// Find optional fields (not present in all records)
|
|
306
|
+
const optionalFields = [];
|
|
307
|
+
for (const key of allKeys) {
|
|
308
|
+
if (keyPresenceCount.get(key) !== records.length) {
|
|
309
|
+
optionalFields.push(key);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
isConsistent: optionalFields.length === 0,
|
|
314
|
+
optionalFields
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Generate schema for JSONL data by sampling multiple records
|
|
319
|
+
* @param lines - Array of raw JSON lines
|
|
320
|
+
* @param options - Configuration options including totalLines and sampleSize
|
|
321
|
+
* @returns Formatted text schema optimized for JQ queries on JSONL files
|
|
322
|
+
*/
|
|
323
|
+
export function generateJsonlQueryAssistSchema(lines, options) {
|
|
324
|
+
const { totalLines, sampleSize = 5 } = options;
|
|
325
|
+
const parts = [];
|
|
326
|
+
parts.push('📊 JSONL STRUCTURE GUIDE (for JQ queries)');
|
|
327
|
+
parts.push('');
|
|
328
|
+
parts.push(`Format: JSON Lines (${totalLines.toLocaleString()} records)`);
|
|
329
|
+
if (options.dataSize) {
|
|
330
|
+
parts.push(`Size: ${options.dataSize.toLocaleString()} characters`);
|
|
331
|
+
}
|
|
332
|
+
parts.push('');
|
|
333
|
+
// JQ usage guide for JSONL
|
|
334
|
+
parts.push('JQ USAGE (JSONL files):');
|
|
335
|
+
parts.push(' • Stream records: jq -c "." file.jsonl');
|
|
336
|
+
parts.push(' • Filter: jq -c "select(.field == value)" file.jsonl');
|
|
337
|
+
parts.push(' • Extract field: jq -r ".fieldName" file.jsonl');
|
|
338
|
+
parts.push(' • Count records: jq -s "length" file.jsonl');
|
|
339
|
+
parts.push(' • First N records: jq -s ".[:N]" file.jsonl');
|
|
340
|
+
parts.push(' • Unique values: jq -s "[.[].field] | unique" file.jsonl');
|
|
341
|
+
parts.push('');
|
|
342
|
+
// Sample records evenly distributed through the file
|
|
343
|
+
const indicesToSample = selectSampleIndices(totalLines, sampleSize);
|
|
344
|
+
const sampledRecords = [];
|
|
345
|
+
for (const idx of indicesToSample) {
|
|
346
|
+
if (lines[idx]) {
|
|
347
|
+
try {
|
|
348
|
+
sampledRecords.push(JSON.parse(lines[idx].trim()));
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
// Skip unparseable lines
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (sampledRecords.length === 0) {
|
|
356
|
+
parts.push('⚠️ Could not parse any records');
|
|
357
|
+
return parts.join('\n');
|
|
358
|
+
}
|
|
359
|
+
// Analyze schema consistency across samples
|
|
360
|
+
const schemaAnalysis = analyzeSchemaConsistency(sampledRecords);
|
|
361
|
+
if (schemaAnalysis.isConsistent) {
|
|
362
|
+
parts.push(`RECORD STRUCTURE (consistent across ${sampledRecords.length} sampled records):`);
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
parts.push(`RECORD STRUCTURE (⚠️ variations detected in ${sampledRecords.length} samples):`);
|
|
366
|
+
if (schemaAnalysis.optionalFields.length > 0) {
|
|
367
|
+
parts.push(` Optional fields: ${schemaAnalysis.optionalFields.join(', ')}`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
parts.push('');
|
|
371
|
+
// Generate schema from first record (representative)
|
|
372
|
+
const recordSchema = generateQueryAssistSchema(sampledRecords[0], {
|
|
373
|
+
maxDepth: options.maxDepth,
|
|
374
|
+
maxPaths: options.maxPaths,
|
|
375
|
+
maxKeys: options.maxKeys,
|
|
376
|
+
});
|
|
377
|
+
// Indent the record schema
|
|
378
|
+
const indentedSchema = recordSchema
|
|
379
|
+
.split('\n')
|
|
380
|
+
.map(l => ' ' + l)
|
|
381
|
+
.join('\n');
|
|
382
|
+
parts.push(indentedSchema);
|
|
383
|
+
return parts.join('\n');
|
|
384
|
+
}
|
|
@@ -1,10 +1,58 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { generateToolId } from '../utils/filename.js';
|
|
4
|
-
import { generateQueryAssistSchema } from './schema.js';
|
|
4
|
+
import { generateQueryAssistSchema, generateJsonlQueryAssistSchema } from './schema.js';
|
|
5
5
|
import { parseDynatraceDqlResponse, isDynatraceDqlTool, } from './dynatrace.js';
|
|
6
6
|
// Default minimum character count to trigger file writing
|
|
7
7
|
const DEFAULT_MIN_CHARS = 1000;
|
|
8
|
+
/**
|
|
9
|
+
* Detect whether content is JSON, JSONL, or plain text
|
|
10
|
+
*/
|
|
11
|
+
function detectContentFormat(content) {
|
|
12
|
+
const trimmed = content.trim();
|
|
13
|
+
// Empty content
|
|
14
|
+
if (!trimmed)
|
|
15
|
+
return 'text';
|
|
16
|
+
// Try parsing as single JSON object/array
|
|
17
|
+
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
|
18
|
+
try {
|
|
19
|
+
JSON.parse(trimmed);
|
|
20
|
+
return 'json';
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// Could be JSONL starting with {
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// Check for JSONL (multiple JSON objects, one per line)
|
|
27
|
+
const lines = trimmed.split('\n').filter(l => l.trim());
|
|
28
|
+
if (lines.length > 1) {
|
|
29
|
+
// Sample first few lines to avoid parsing huge files
|
|
30
|
+
const sampleSize = Math.min(lines.length, 10);
|
|
31
|
+
const allSampledLinesAreJson = lines.slice(0, sampleSize).every(line => {
|
|
32
|
+
try {
|
|
33
|
+
JSON.parse(line.trim());
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
if (allSampledLinesAreJson) {
|
|
41
|
+
return 'jsonl';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return 'text';
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Get file extension based on content format
|
|
48
|
+
*/
|
|
49
|
+
function getFileExtension(format) {
|
|
50
|
+
switch (format) {
|
|
51
|
+
case 'jsonl': return '.jsonl';
|
|
52
|
+
case 'text': return '.txt';
|
|
53
|
+
default: return '.json';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
8
56
|
/**
|
|
9
57
|
* Helper function to detect if a response contains an error
|
|
10
58
|
*/
|
|
@@ -299,15 +347,31 @@ export async function handleToolResponse(config, toolName, args, responseData) {
|
|
|
299
347
|
}
|
|
300
348
|
// Success case: write to file
|
|
301
349
|
try {
|
|
302
|
-
|
|
350
|
+
// Detect content format for appropriate extension and schema
|
|
351
|
+
const format = detectContentFormat(contentToWrite);
|
|
352
|
+
const extension = getFileExtension(format);
|
|
353
|
+
const filename = `${tool_id}${extension}`;
|
|
303
354
|
const filepath = path.join(config.outputPath, filename);
|
|
304
355
|
// Ensure output directory exists
|
|
305
356
|
await fs.mkdir(config.outputPath, { recursive: true });
|
|
306
357
|
// Write the exact content we counted
|
|
307
358
|
await fs.writeFile(filepath, contentToWrite);
|
|
308
|
-
// Generate query-assist schema
|
|
359
|
+
// Generate query-assist schema based on format
|
|
309
360
|
let fileSchema;
|
|
310
|
-
if (
|
|
361
|
+
if (format === 'jsonl') {
|
|
362
|
+
// JSONL format: sample multiple records for schema
|
|
363
|
+
const lines = contentToWrite.trim().split('\n').filter(l => l.trim());
|
|
364
|
+
fileSchema = generateJsonlQueryAssistSchema(lines, {
|
|
365
|
+
maxDepth: config.schemaMaxDepth ?? 2,
|
|
366
|
+
maxPaths: config.schemaMaxPaths ?? 20,
|
|
367
|
+
maxKeys: config.schemaMaxKeys ?? 50,
|
|
368
|
+
dataSize: contentLength,
|
|
369
|
+
totalLines: lines.length,
|
|
370
|
+
sampleSize: 5
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
else if (parsedForSchema) {
|
|
374
|
+
// Standard JSON format: use existing schema generator
|
|
311
375
|
// Use the clean data (without pagination) for schema analysis
|
|
312
376
|
const { pagination, has_more, next_page, previous_page, page, page_size, total_pages, ...cleanData } = parsedForSchema;
|
|
313
377
|
// Generate compact query-assist schema using config values
|
package/dist/jq/handler.js
CHANGED
|
@@ -37,9 +37,10 @@ export async function executeJqQuery(config, jqQuery, filePath) {
|
|
|
37
37
|
if (!existsSync(filePath)) {
|
|
38
38
|
throw new Error(`File not found: ${filePath}`);
|
|
39
39
|
}
|
|
40
|
-
// Validate file extension
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
// Validate file extension (support both .json and .jsonl)
|
|
41
|
+
const lowerPath = filePath.toLowerCase();
|
|
42
|
+
if (!lowerPath.endsWith('.json') && !lowerPath.endsWith('.jsonl')) {
|
|
43
|
+
throw new Error(`Only JSON (.json) and JSONL (.jsonl) files are supported for jq processing: ${filePath}`);
|
|
43
44
|
}
|
|
44
45
|
// Validate path is within allowed directories
|
|
45
46
|
validatePathWithinAllowedDirs(filePath, config.allowedPaths);
|
package/dist/jq/tool.js
CHANGED
|
@@ -8,7 +8,7 @@ export const ExecuteJqQuerySchema = z.object({
|
|
|
8
8
|
.describe('The jq query to execute on the JSON file. Query will be sanitized to prevent environment variable access.'),
|
|
9
9
|
file_path: z
|
|
10
10
|
.string()
|
|
11
|
-
.describe('Absolute path starting with "/" pointing to the JSON file to process. Must be a valid, existing file with .json extension. The file will be validated for existence and readability before processing.'),
|
|
11
|
+
.describe('Absolute path starting with "/" pointing to the JSON or JSONL file to process. Must be a valid, existing file with .json or .jsonl extension. The file will be validated for existence and readability before processing.'),
|
|
12
12
|
description: z
|
|
13
13
|
.string()
|
|
14
14
|
.optional()
|
|
@@ -109,7 +109,7 @@ export const JQ_TOOL_DEFINITION = {
|
|
|
109
109
|
},
|
|
110
110
|
file_path: {
|
|
111
111
|
type: 'string',
|
|
112
|
-
description: 'Absolute path starting with "/" pointing to the JSON file to process. Must be a valid, existing file with .json extension. The file will be validated for existence and readability before processing.',
|
|
112
|
+
description: 'Absolute path starting with "/" pointing to the JSON or JSONL file to process. Must be a valid, existing file with .json or .jsonl extension. The file will be validated for existence and readability before processing.',
|
|
113
113
|
},
|
|
114
114
|
description: {
|
|
115
115
|
type: 'string',
|