@anyshift/mcp-proxy 0.3.1 → 0.3.2

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 @@
1
+ export {};
@@ -0,0 +1,204 @@
1
+ import { describe, it, expect } from '@jest/globals';
2
+ import { parseDynatraceDqlResponse, isDynatraceDqlTool, } from '../../fileWriter/dynatrace.js';
3
+ // Type guards for test assertions
4
+ function isDqlSuccess(result) {
5
+ return (typeof result === 'object' &&
6
+ result !== null &&
7
+ 'isDynatraceDql' in result &&
8
+ result.isDynatraceDql === true &&
9
+ 'isError' in result &&
10
+ result.isError === false);
11
+ }
12
+ function isDqlError(result) {
13
+ return (typeof result === 'object' &&
14
+ result !== null &&
15
+ 'isDynatraceDql' in result &&
16
+ result.isDynatraceDql === true &&
17
+ 'isError' in result &&
18
+ result.isError === true);
19
+ }
20
+ describe('Dynatrace DQL Parser', () => {
21
+ describe('isDynatraceDqlTool', () => {
22
+ it('should return true for execute_dql tool', () => {
23
+ expect(isDynatraceDqlTool('execute_dql')).toBe(true);
24
+ });
25
+ it('should return false for other tools', () => {
26
+ expect(isDynatraceDqlTool('execute_jq_query')).toBe(false);
27
+ expect(isDynatraceDqlTool('get_metrics')).toBe(false);
28
+ expect(isDynatraceDqlTool('list_entities')).toBe(false);
29
+ });
30
+ });
31
+ describe('parseDynatraceDqlResponse - Success Responses', () => {
32
+ const successResponse = `📊 **DQL Query Results**
33
+
34
+ - **Scanned Records:** 3,988,787
35
+ - **Scanned Bytes:** 10.00 GB (Session total: 10.00 GB / 100 GB budget, 10.0% used)
36
+ 💡 **Moderate Data Usage:** This query scanned 10.00 GB of data.
37
+ - **⚠️ Sampling Used:** Yes (results may be approximate)
38
+
39
+ 📋 **Query Results**: (50 records):
40
+
41
+ \`\`\`json
42
+ [
43
+ {
44
+ "faas.name": "compassdigital-order-v1-root",
45
+ "logCount": "861021"
46
+ },
47
+ {
48
+ "faas.name": "cdl-shoppingcart-v1-root",
49
+ "logCount": "421993"
50
+ }
51
+ ]
52
+ \`\`\``;
53
+ it('should parse a success response correctly', () => {
54
+ const result = parseDynatraceDqlResponse(successResponse);
55
+ expect(result.isDynatraceDql).toBe(true);
56
+ expect(isDqlSuccess(result)).toBe(true);
57
+ if (isDqlSuccess(result)) {
58
+ expect(result.metadata.scannedRecords).toBe(3988787);
59
+ expect(result.metadata.scannedBytes).toBe('10.00 GB');
60
+ expect(result.metadata.sessionTotal).toBe('10.00 GB');
61
+ expect(result.metadata.sessionBudget).toBe('100 GB');
62
+ expect(result.metadata.budgetUsedPercent).toBe('10.0%');
63
+ expect(result.metadata.samplingUsed).toBe(true);
64
+ expect(result.metadata.recordCount).toBe(50);
65
+ // dataUsageNote is optional, may or may not be present depending on formatting
66
+ expect(result.data).toBeInstanceOf(Array);
67
+ expect(result.data.length).toBe(2);
68
+ expect(result.data[0]['faas.name']).toBe('compassdigital-order-v1-root');
69
+ }
70
+ });
71
+ it('should parse response without sampling info', () => {
72
+ const responseNoSampling = `📊 **DQL Query Results**
73
+
74
+ - **Scanned Records:** 1,000
75
+ - **Scanned Bytes:** 100 MB (Session total: 100 MB / 100 GB budget, 0.1% used)
76
+
77
+ 📋 **Query Results**: (10 records):
78
+
79
+ \`\`\`json
80
+ [{"id": 1}]
81
+ \`\`\``;
82
+ const result = parseDynatraceDqlResponse(responseNoSampling);
83
+ expect(result.isDynatraceDql).toBe(true);
84
+ if (isDqlSuccess(result)) {
85
+ expect(result.metadata.samplingUsed).toBeUndefined();
86
+ expect(result.metadata.recordCount).toBe(10);
87
+ }
88
+ });
89
+ it('should handle single record count', () => {
90
+ const responseSingleRecord = `📊 **DQL Query Results**
91
+
92
+ - **Scanned Records:** 100
93
+ - **Scanned Bytes:** 1 MB
94
+
95
+ 📋 **Query Results**: (1 record):
96
+
97
+ \`\`\`json
98
+ {"id": 1}
99
+ \`\`\``;
100
+ const result = parseDynatraceDqlResponse(responseSingleRecord);
101
+ expect(result.isDynatraceDql).toBe(true);
102
+ if (isDqlSuccess(result)) {
103
+ expect(result.metadata.recordCount).toBe(1);
104
+ }
105
+ });
106
+ });
107
+ describe('parseDynatraceDqlResponse - Error Responses', () => {
108
+ const errorResponse = `Client Request Error: PARAMETER_MUST_NOT_BE_AN_AGGREGATION. exceptionType: "DQL-SYNTAX-ERROR". syntaxErrorPosition: {"start":{"column":113,"index":112,"line":1},"end":{"column":119,"index":118,"line":1}}. errorType: "PARAMETER_MUST_NOT_BE_AN_AGGREGATION". errorMessage: "Aggregations aren't allowed here, but \`count()\` is an aggregation function.". arguments: ["count()"]. queryString: "fetch logs, scanLimitGBytes: 10, samplingRatio: 100, from: now()-7d | summarize count(), by: {faas.name} | sort count() desc | limit 50". errorMessageFormatSpecifierTypes: ["INPUT_QUERY_PART"]. errorMessageFormat: "Aggregations aren't allowed here, but \`%1$s\` is an aggregation function.". queryId: "9be6a579-545c-4157-957f-5d8d39129a78" with HTTP status: 400. (body: {"error":{"message":"PARAMETER_MUST_NOT_BE_AN_AGGREGATION","details":{"exceptionType":"DQL-SYNTAX-ERROR"}}})`;
109
+ it('should parse an error response correctly', () => {
110
+ const result = parseDynatraceDqlResponse(errorResponse);
111
+ expect(result.isDynatraceDql).toBe(true);
112
+ expect(isDqlError(result)).toBe(true);
113
+ if (isDqlError(result)) {
114
+ expect(result.errorMessage).toContain("Aggregations aren't allowed here");
115
+ expect(result.errorMessage).toContain('count()');
116
+ expect(result.errorMessage).toContain('Query:');
117
+ expect(result.errorMessage).toContain('HTTP 400');
118
+ }
119
+ });
120
+ it('should extract error type when no errorMessage field', () => {
121
+ const simpleError = `Client Request Error: INVALID_QUERY. exceptionType: "DQL-ERROR". with HTTP status: 400.`;
122
+ const result = parseDynatraceDqlResponse(simpleError);
123
+ expect(result.isDynatraceDql).toBe(true);
124
+ expect(isDqlError(result)).toBe(true);
125
+ if (isDqlError(result)) {
126
+ expect(result.errorMessage).toContain('INVALID_QUERY');
127
+ expect(result.errorMessage).toContain('HTTP 400');
128
+ }
129
+ });
130
+ });
131
+ describe('parseDynatraceDqlResponse - Non-Dynatrace Responses', () => {
132
+ it('should return isDynatraceDql: false for regular JSON', () => {
133
+ const regularJson = '{"status": "success", "data": [1, 2, 3]}';
134
+ const result = parseDynatraceDqlResponse(regularJson);
135
+ expect(result.isDynatraceDql).toBe(false);
136
+ });
137
+ it('should return isDynatraceDql: false for plain text', () => {
138
+ const plainText = 'This is just some plain text response';
139
+ const result = parseDynatraceDqlResponse(plainText);
140
+ expect(result.isDynatraceDql).toBe(false);
141
+ });
142
+ it('should return isDynatraceDql: false for other MCP tool outputs', () => {
143
+ const otherToolOutput = `Listed metrics:
144
+ - metric.cpu.usage
145
+ - metric.memory.used`;
146
+ const result = parseDynatraceDqlResponse(otherToolOutput);
147
+ expect(result.isDynatraceDql).toBe(false);
148
+ });
149
+ });
150
+ describe('Edge Cases', () => {
151
+ it('should handle malformed JSON in success response', () => {
152
+ const malformedJson = `📊 **DQL Query Results**
153
+
154
+ - **Scanned Records:** 100
155
+
156
+ 📋 **Query Results**: (1 record):
157
+
158
+ \`\`\`json
159
+ {this is not valid json}
160
+ \`\`\``;
161
+ const result = parseDynatraceDqlResponse(malformedJson);
162
+ expect(result.isDynatraceDql).toBe(true);
163
+ if (isDqlSuccess(result)) {
164
+ // Should still parse metadata
165
+ expect(result.metadata.scannedRecords).toBe(100);
166
+ // Data should be the raw text from code block since JSON parsing failed
167
+ expect(result.data).toBe('{this is not valid json}');
168
+ }
169
+ });
170
+ it('should handle empty JSON array', () => {
171
+ const emptyResults = `📊 **DQL Query Results**
172
+
173
+ - **Scanned Records:** 0
174
+
175
+ 📋 **Query Results**: (0 records):
176
+
177
+ \`\`\`json
178
+ []
179
+ \`\`\``;
180
+ const result = parseDynatraceDqlResponse(emptyResults);
181
+ expect(result.isDynatraceDql).toBe(true);
182
+ if (isDqlSuccess(result)) {
183
+ expect(result.metadata.scannedRecords).toBe(0);
184
+ expect(result.metadata.recordCount).toBe(0);
185
+ expect(result.data).toEqual([]);
186
+ }
187
+ });
188
+ it('should handle response with no code block', () => {
189
+ const noCodeBlock = `📊 **DQL Query Results**
190
+
191
+ - **Scanned Records:** 100
192
+
193
+ 📋 **Query Results**: (0 records):
194
+
195
+ No data found.`;
196
+ const result = parseDynatraceDqlResponse(noCodeBlock);
197
+ expect(result.isDynatraceDql).toBe(true);
198
+ if (isDqlSuccess(result)) {
199
+ expect(result.metadata.scannedRecords).toBe(100);
200
+ expect(result.data).toBeNull();
201
+ }
202
+ });
203
+ });
204
+ });
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Dynatrace DQL Response Parser
3
+ *
4
+ * Parses Dynatrace DQL query responses to extract:
5
+ * - Metadata (scanned records, bytes, sampling info)
6
+ * - Data (the actual query results)
7
+ * - Errors (if the query failed)
8
+ */
9
+ export interface DynatraceDqlMetadata {
10
+ scannedRecords?: number;
11
+ scannedBytes?: string;
12
+ sessionTotal?: string;
13
+ sessionBudget?: string;
14
+ budgetUsedPercent?: string;
15
+ dataUsageNote?: string;
16
+ samplingUsed?: boolean;
17
+ recordCount?: number;
18
+ }
19
+ export interface DynatraceDqlParsedResponse {
20
+ isDynatraceDql: true;
21
+ isError: false;
22
+ metadata: DynatraceDqlMetadata;
23
+ data: unknown;
24
+ }
25
+ export interface DynatraceDqlErrorResponse {
26
+ isDynatraceDql: true;
27
+ isError: true;
28
+ errorMessage: string;
29
+ }
30
+ export interface NotDynatraceDqlResponse {
31
+ isDynatraceDql: false;
32
+ }
33
+ export type DynatraceParseResult = DynatraceDqlParsedResponse | DynatraceDqlErrorResponse | NotDynatraceDqlResponse;
34
+ /**
35
+ * Parse a Dynatrace DQL response
36
+ * @param text - The raw text response from Dynatrace
37
+ * @returns Parsed response with metadata/data or error, or indication that it's not a Dynatrace response
38
+ */
39
+ export declare function parseDynatraceDqlResponse(text: string): DynatraceParseResult;
40
+ /**
41
+ * Check if a tool name is a Dynatrace DQL tool
42
+ */
43
+ export declare function isDynatraceDqlTool(toolName: string): boolean;
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Dynatrace DQL Response Parser
3
+ *
4
+ * Parses Dynatrace DQL query responses to extract:
5
+ * - Metadata (scanned records, bytes, sampling info)
6
+ * - Data (the actual query results)
7
+ * - Errors (if the query failed)
8
+ */
9
+ /**
10
+ * Detect if a text response is a Dynatrace DQL error
11
+ */
12
+ function isDynatraceError(text) {
13
+ return text.includes('Client Request Error:') && text.includes('HTTP status:');
14
+ }
15
+ /**
16
+ * Detect if a text response is a Dynatrace DQL success response
17
+ */
18
+ function isDynatraceSuccess(text) {
19
+ return text.includes('📊 **DQL Query Results**');
20
+ }
21
+ /**
22
+ * Parse a Dynatrace error response - keeps more context for debugging
23
+ */
24
+ function parseErrorResponse(text) {
25
+ // The error format is:
26
+ // Client Request Error: ERROR_TYPE. exceptionType: "...". ... errorMessage: "...". ...
27
+ const parts = [];
28
+ // Extract error type
29
+ const errorTypeMatch = text.match(/Client Request Error:\s*([^.]+)/);
30
+ if (errorTypeMatch) {
31
+ parts.push(errorTypeMatch[1].trim());
32
+ }
33
+ // Extract the human-readable error message
34
+ const errorMessageMatch = text.match(/errorMessage:\s*"([^"]+)"/);
35
+ if (errorMessageMatch) {
36
+ parts.push(errorMessageMatch[1]);
37
+ }
38
+ // Extract the query string for context
39
+ const queryStringMatch = text.match(/queryString:\s*"([^"]+)"/);
40
+ if (queryStringMatch) {
41
+ parts.push(`Query: ${queryStringMatch[1]}`);
42
+ }
43
+ // Extract syntax error position if available
44
+ const positionMatch = text.match(/syntaxErrorPosition:\s*(\{[^}]+\})/);
45
+ if (positionMatch) {
46
+ try {
47
+ const pos = JSON.parse(positionMatch[1]);
48
+ if (pos.start) {
49
+ parts.push(`Position: line ${pos.start.line}, column ${pos.start.column}`);
50
+ }
51
+ }
52
+ catch {
53
+ // Ignore JSON parse errors
54
+ }
55
+ }
56
+ // Extract HTTP status
57
+ const httpStatusMatch = text.match(/HTTP status:\s*(\d+)/);
58
+ if (httpStatusMatch) {
59
+ parts.push(`HTTP ${httpStatusMatch[1]}`);
60
+ }
61
+ if (parts.length > 0) {
62
+ return parts.join('. ');
63
+ }
64
+ // Last fallback: return a truncated version of the raw error
65
+ const firstLine = text.split('\n')[0];
66
+ if (firstLine.length > 500) {
67
+ return firstLine.substring(0, 500) + '...';
68
+ }
69
+ return firstLine;
70
+ }
71
+ /**
72
+ * Parse metadata from a Dynatrace DQL success response
73
+ */
74
+ function parseMetadata(text) {
75
+ const metadata = {};
76
+ // Parse scanned records: "- **Scanned Records:** 3,988,787"
77
+ const scannedRecordsMatch = text.match(/\*\*Scanned Records:\*\*\s*([\d,]+)/);
78
+ if (scannedRecordsMatch) {
79
+ metadata.scannedRecords = parseInt(scannedRecordsMatch[1].replace(/,/g, ''), 10);
80
+ }
81
+ // Parse scanned bytes: "- **Scanned Bytes:** 10.00 GB (Session total: 10.00 GB / 100 GB budget, 10.0% used)"
82
+ const scannedBytesMatch = text.match(/\*\*Scanned Bytes:\*\*\s*([^\n(]+)/);
83
+ if (scannedBytesMatch) {
84
+ metadata.scannedBytes = scannedBytesMatch[1].trim();
85
+ }
86
+ // Parse session total and budget from the parenthetical
87
+ const sessionMatch = text.match(/Session total:\s*([^/]+)\/\s*([^,]+)\s*budget,\s*([^)%]+)%\s*used/);
88
+ if (sessionMatch) {
89
+ metadata.sessionTotal = sessionMatch[1].trim();
90
+ metadata.sessionBudget = sessionMatch[2].trim();
91
+ metadata.budgetUsedPercent = sessionMatch[3].trim() + '%';
92
+ }
93
+ // Parse data usage note: " 💡 **Moderate Data Usage:** This query scanned 10.00 GB of data."
94
+ const dataUsageMatch = text.match(/\s*💡\s*\*\*([^*]+)\*\*:\s*([^\n]+)/);
95
+ if (dataUsageMatch) {
96
+ metadata.dataUsageNote = `${dataUsageMatch[1]}: ${dataUsageMatch[2].trim()}`;
97
+ }
98
+ // Parse sampling info: "- **⚠️ Sampling Used:** Yes"
99
+ const samplingMatch = text.match(/\*\*⚠️?\s*Sampling Used:\*\*\s*(Yes|No)/i);
100
+ if (samplingMatch) {
101
+ metadata.samplingUsed = samplingMatch[1].toLowerCase() === 'yes';
102
+ }
103
+ // Parse record count: "📋 **Query Results**: (50 records):"
104
+ const recordCountMatch = text.match(/\*\*Query Results\*\*[^(]*\((\d+)\s*records?\)/);
105
+ if (recordCountMatch) {
106
+ metadata.recordCount = parseInt(recordCountMatch[1], 10);
107
+ }
108
+ return metadata;
109
+ }
110
+ /**
111
+ * Extract JSON data from a Dynatrace DQL success response
112
+ */
113
+ function extractData(text) {
114
+ // The JSON is in a code block: ```json\n[...]\n```
115
+ const jsonBlockMatch = text.match(/```json\s*\n([\s\S]*?)\n```/);
116
+ if (jsonBlockMatch) {
117
+ try {
118
+ return JSON.parse(jsonBlockMatch[1]);
119
+ }
120
+ catch {
121
+ // If JSON parsing fails, return the raw text from the block
122
+ return jsonBlockMatch[1];
123
+ }
124
+ }
125
+ // Fallback: try to find a JSON array or object anywhere after the header
126
+ const jsonMatch = text.match(/(\[[\s\S]*\]|\{[\s\S]*\})/);
127
+ if (jsonMatch) {
128
+ try {
129
+ return JSON.parse(jsonMatch[1]);
130
+ }
131
+ catch {
132
+ return null;
133
+ }
134
+ }
135
+ return null;
136
+ }
137
+ /**
138
+ * Parse a Dynatrace DQL response
139
+ * @param text - The raw text response from Dynatrace
140
+ * @returns Parsed response with metadata/data or error, or indication that it's not a Dynatrace response
141
+ */
142
+ export function parseDynatraceDqlResponse(text) {
143
+ // Check if it's an error response
144
+ if (isDynatraceError(text)) {
145
+ return {
146
+ isDynatraceDql: true,
147
+ isError: true,
148
+ errorMessage: parseErrorResponse(text),
149
+ };
150
+ }
151
+ // Check if it's a success response
152
+ if (isDynatraceSuccess(text)) {
153
+ const metadata = parseMetadata(text);
154
+ const data = extractData(text);
155
+ return {
156
+ isDynatraceDql: true,
157
+ isError: false,
158
+ metadata,
159
+ data,
160
+ };
161
+ }
162
+ // Not a Dynatrace DQL response
163
+ return {
164
+ isDynatraceDql: false,
165
+ };
166
+ }
167
+ /**
168
+ * Check if a tool name is a Dynatrace DQL tool
169
+ */
170
+ export function isDynatraceDqlTool(toolName) {
171
+ // The tool that executes DQL queries
172
+ return toolName === 'execute_dql';
173
+ }
@@ -2,6 +2,7 @@ import fs from 'fs/promises';
2
2
  import path from 'path';
3
3
  import { generateToolId } from '../utils/filename.js';
4
4
  import { generateQueryAssistSchema } from './schema.js';
5
+ import { parseDynatraceDqlResponse, isDynatraceDqlTool, } from './dynatrace.js';
5
6
  // Default minimum character count to trigger file writing
6
7
  const DEFAULT_MIN_CHARS = 1000;
7
8
  /**
@@ -160,6 +161,60 @@ const extractContentForFile = (responseData) => {
160
161
  }
161
162
  return { contentToWrite, parsedForSchema };
162
163
  };
164
+ /**
165
+ * Handle Dynatrace DQL response - extracts metadata and data, writes structured file
166
+ * @param config - File writer configuration
167
+ * @param tool_id - Generated tool ID
168
+ * @param parsedDql - Parsed DQL response with metadata and data
169
+ * @returns UnifiedToolResponse or null if should fall through to default handling
170
+ */
171
+ async function handleDynatraceDqlResponse(config, tool_id, parsedDql) {
172
+ // Build the structured content to write
173
+ const structuredContent = {
174
+ metadata: parsedDql.metadata,
175
+ data: parsedDql.data,
176
+ };
177
+ const contentToWrite = JSON.stringify(structuredContent, null, 2);
178
+ const contentLength = contentToWrite.length;
179
+ // If file writing is disabled, return null to fall through to default handling
180
+ if (!config.enabled || !config.outputPath) {
181
+ return null;
182
+ }
183
+ // Check character count threshold
184
+ const minChars = config.minCharsForWrite ?? DEFAULT_MIN_CHARS;
185
+ if (contentLength < minChars) {
186
+ return null;
187
+ }
188
+ // Write structured file
189
+ try {
190
+ const filename = `${tool_id}.json`;
191
+ const filepath = path.join(config.outputPath, filename);
192
+ await fs.mkdir(config.outputPath, { recursive: true });
193
+ await fs.writeFile(filepath, contentToWrite);
194
+ // Generate schema from the full structured content (metadata + data)
195
+ const fileSchema = generateQueryAssistSchema(structuredContent, {
196
+ maxDepth: config.schemaMaxDepth ?? 2,
197
+ maxPaths: config.schemaMaxPaths ?? 20,
198
+ maxKeys: config.schemaMaxKeys ?? 50,
199
+ dataSize: contentLength,
200
+ });
201
+ return {
202
+ tool_id,
203
+ wroteToFile: true,
204
+ filePath: filepath,
205
+ fileSchema,
206
+ };
207
+ }
208
+ catch (error) {
209
+ console.error(`[handleDynatraceDqlResponse] Error writing file:`, error);
210
+ return {
211
+ tool_id,
212
+ wroteToFile: false,
213
+ outputContent: structuredContent,
214
+ error: `File write failed: ${error instanceof Error ? error.message : String(error)}`,
215
+ };
216
+ }
217
+ }
163
218
  /**
164
219
  * Centralized response handler with file writing capability
165
220
  * @param config - File writer configuration
@@ -180,6 +235,38 @@ export async function handleToolResponse(config, toolName, args, responseData) {
180
235
  outputContent: parsedForSchema ?? contentToWrite,
181
236
  };
182
237
  }
238
+ // Handle Dynatrace DQL responses specially
239
+ if (isDynatraceDqlTool(toolName)) {
240
+ const rawText = extractRawText(responseData);
241
+ if (rawText) {
242
+ const parsedDql = parseDynatraceDqlResponse(rawText);
243
+ if (parsedDql.isDynatraceDql) {
244
+ // If it's an error, return error directly
245
+ if (parsedDql.isError) {
246
+ return {
247
+ tool_id,
248
+ wroteToFile: false,
249
+ error: parsedDql.errorMessage,
250
+ };
251
+ }
252
+ // Try to handle as structured Dynatrace response
253
+ const dqlResult = await handleDynatraceDqlResponse(config, tool_id, parsedDql);
254
+ if (dqlResult) {
255
+ return dqlResult;
256
+ }
257
+ // If dqlResult is null, fall through to return the structured content directly
258
+ return {
259
+ tool_id,
260
+ wroteToFile: false,
261
+ outputContent: {
262
+ metadata: parsedDql.metadata,
263
+ data: parsedDql.data,
264
+ },
265
+ };
266
+ }
267
+ }
268
+ // If not a recognized Dynatrace DQL format, fall through to default handling
269
+ }
183
270
  // If there's an error, return error in unified format
184
271
  if (isErrorResponse(responseData)) {
185
272
  const errorMessage = extractErrorMessage(responseData);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anyshift/mcp-proxy",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
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",