@anyshift/mcp-proxy 0.2.1 → 0.2.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.
- package/README.md +74 -31
- package/dist/__tests__/unit/queryAssistSchema.test.d.ts +1 -0
- package/dist/__tests__/unit/queryAssistSchema.test.js +267 -0
- package/dist/fileWriter/index.d.ts +1 -1
- package/dist/fileWriter/index.js +1 -1
- package/dist/fileWriter/schema.d.ts +38 -11
- package/dist/fileWriter/schema.js +248 -98
- package/dist/fileWriter/writer.js +14 -52
- package/dist/index.js +22 -1
- package/dist/jq/tool.js +15 -0
- package/dist/types/index.d.ts +6 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -133,39 +133,82 @@ npx @anyshift/mcp-proxy
|
|
|
133
133
|
|
|
134
134
|
## How It Works
|
|
135
135
|
|
|
136
|
+
```mermaid
|
|
137
|
+
graph TB
|
|
138
|
+
AI[🤖 AI Agent<br/>Claude]
|
|
139
|
+
|
|
140
|
+
AI -->|MCP Protocol| Proxy
|
|
141
|
+
|
|
142
|
+
subgraph Proxy["@anyshift/mcp-proxy"]
|
|
143
|
+
Start[Receive tool call]
|
|
144
|
+
CheckJQ{JQ tool?}
|
|
145
|
+
ExecuteJQ[Execute JQ locally]
|
|
146
|
+
Forward[Forward to child MCP]
|
|
147
|
+
GetResponse[Get response from child]
|
|
148
|
+
CheckSize{Size ≥ 1000 chars?}
|
|
149
|
+
WriteFile[📄 Write FULL data to file]
|
|
150
|
+
ReturnFile[Return file reference]
|
|
151
|
+
CheckTrunc{Size > 40K chars?}
|
|
152
|
+
Truncate[Truncate with notice]
|
|
153
|
+
ReturnDirect[Return response]
|
|
154
|
+
|
|
155
|
+
Start --> CheckJQ
|
|
156
|
+
CheckJQ -->|Yes| ExecuteJQ
|
|
157
|
+
CheckJQ -->|No| Forward
|
|
158
|
+
Forward --> GetResponse
|
|
159
|
+
ExecuteJQ --> CheckTrunc
|
|
160
|
+
GetResponse --> CheckSize
|
|
161
|
+
CheckSize -->|Yes| WriteFile
|
|
162
|
+
WriteFile --> ReturnFile
|
|
163
|
+
CheckSize -->|No| CheckTrunc
|
|
164
|
+
CheckTrunc -->|Yes| Truncate
|
|
165
|
+
CheckTrunc -->|No| ReturnDirect
|
|
166
|
+
|
|
167
|
+
ReturnFile -.->|📄 Small reference| AI
|
|
168
|
+
Truncate -.->|Truncated text| AI
|
|
169
|
+
ReturnDirect -.->|Full response| AI
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
Proxy -->|stdio + env vars| Child[Child MCP<br/>anyshift/datadog/grafana]
|
|
173
|
+
Child -.->|Response| Proxy
|
|
174
|
+
|
|
175
|
+
style AI fill:#e1f5ff
|
|
176
|
+
style Proxy fill:#fff4e1
|
|
177
|
+
style Child fill:#e8f5e9
|
|
178
|
+
style WriteFile fill:#c8e6c9
|
|
179
|
+
style ReturnFile fill:#c8e6c9
|
|
136
180
|
```
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
│ if response > MIN_CHARS │
|
|
158
|
-
│ │
|
|
159
|
-
│ 6. Returns modified response │
|
|
160
|
-
│ to AI agent │
|
|
161
|
-
└────────────┬────────────────────┘
|
|
162
|
-
│ Child process (stdio)
|
|
163
|
-
▼
|
|
164
|
-
┌──────────────┐
|
|
165
|
-
│ Child MCP │ (mcp-grafana, custom-mcp, etc.)
|
|
166
|
-
│ Server │ Gets env vars WITHOUT MCP_PROXY_ prefix
|
|
167
|
-
└──────────────┘
|
|
181
|
+
|
|
182
|
+
### Response Handling Examples
|
|
183
|
+
|
|
184
|
+
**Small responses (< 1,000 chars):**
|
|
185
|
+
```
|
|
186
|
+
Child: 500 chars → Proxy: Return directly → AI: 500 chars ✓
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
**Medium responses (1,000 - 40,000 chars):**
|
|
190
|
+
```
|
|
191
|
+
Child: 5,000 chars → Proxy: Write to file → AI: "📄 File: path/to/file.json" ✓
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
**Large responses (> 40,000 chars):**
|
|
195
|
+
```
|
|
196
|
+
Child: 100,000 chars → Proxy: Write FULL 100K to file → AI: "📄 File: ..." ✓
|
|
197
|
+
Note: File contains complete data, not truncated!
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**JQ tool queries:**
|
|
168
201
|
```
|
|
202
|
+
AI: JQ query → Proxy: Execute locally → AI: Result (truncated if > 40K) ✓
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Key Design Principle
|
|
206
|
+
|
|
207
|
+
**File writing happens BEFORE truncation.** This ensures:
|
|
208
|
+
- Files always contain complete, untruncated data
|
|
209
|
+
- Large responses are accessible via file references
|
|
210
|
+
- AI receives small, manageable responses
|
|
211
|
+
- No data loss due to context limits
|
|
169
212
|
|
|
170
213
|
## Integration Examples
|
|
171
214
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { describe, it, expect } from '@jest/globals';
|
|
2
|
+
import { generateQueryAssistSchema } from '../../fileWriter/schema.js';
|
|
3
|
+
describe('Query-Assist Schema Generator', () => {
|
|
4
|
+
describe('Basic Structure Detection', () => {
|
|
5
|
+
it('should detect simple object structure', () => {
|
|
6
|
+
const data = {
|
|
7
|
+
id: '123',
|
|
8
|
+
name: 'Test',
|
|
9
|
+
count: 42
|
|
10
|
+
};
|
|
11
|
+
const schema = generateQueryAssistSchema(data, { maxDepth: 2, maxPaths: 20 });
|
|
12
|
+
expect(schema).toContain('📊 STRUCTURE GUIDE');
|
|
13
|
+
expect(schema).toContain('.id');
|
|
14
|
+
expect(schema).toContain('.name');
|
|
15
|
+
expect(schema).toContain('.count');
|
|
16
|
+
expect(schema).toContain('string');
|
|
17
|
+
expect(schema).toContain('number');
|
|
18
|
+
});
|
|
19
|
+
it('should detect array structure', () => {
|
|
20
|
+
const data = {
|
|
21
|
+
items: [
|
|
22
|
+
{ id: '1', price: 100 },
|
|
23
|
+
{ id: '2', price: 200 }
|
|
24
|
+
]
|
|
25
|
+
};
|
|
26
|
+
// With maxDepth=3, we can see array item fields
|
|
27
|
+
const schema = generateQueryAssistSchema(data, { maxDepth: 3, maxPaths: 20 });
|
|
28
|
+
expect(schema).toContain('.items');
|
|
29
|
+
expect(schema).toContain('array[2]');
|
|
30
|
+
expect(schema).toContain('.items[].id');
|
|
31
|
+
expect(schema).toContain('.items[].price');
|
|
32
|
+
});
|
|
33
|
+
it('should detect nested object structure', () => {
|
|
34
|
+
const data = {
|
|
35
|
+
user: {
|
|
36
|
+
profile: {
|
|
37
|
+
name: 'Alice',
|
|
38
|
+
age: 30
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
const schema = generateQueryAssistSchema(data, { maxDepth: 2, maxPaths: 20 });
|
|
43
|
+
expect(schema).toContain('.user');
|
|
44
|
+
expect(schema).toContain('.user.profile');
|
|
45
|
+
expect(schema).toContain('object');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
describe('Depth Limiting', () => {
|
|
49
|
+
it('should stop at max depth and show warning', () => {
|
|
50
|
+
const data = {
|
|
51
|
+
level1: {
|
|
52
|
+
level2: {
|
|
53
|
+
level3: {
|
|
54
|
+
level4: 'too deep'
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
const schema = generateQueryAssistSchema(data, { maxDepth: 2, maxPaths: 20 });
|
|
60
|
+
// Should show paths up to depth 2
|
|
61
|
+
expect(schema).toContain('.level1.level2');
|
|
62
|
+
// Should show depth limit warning in consolidated format
|
|
63
|
+
expect(schema).toContain('⚠️ Limits: DEPTH (max: 2)');
|
|
64
|
+
// level3 can appear in exploration prompts, but not as a path entry
|
|
65
|
+
// Check that level3 is not shown as a separate path line
|
|
66
|
+
const lines = schema.split('\n');
|
|
67
|
+
const pathLines = lines.filter(l => l.includes('→') && l.includes('.level'));
|
|
68
|
+
expect(pathLines.some(l => l.includes('.level1.level2.level3') && l.includes(' → '))).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
it('should show exploration prompts when depth limit hit', () => {
|
|
71
|
+
const data = {
|
|
72
|
+
deep: {
|
|
73
|
+
nested: {
|
|
74
|
+
value: 'hidden'
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
const schema = generateQueryAssistSchema(data, { maxDepth: 1, maxPaths: 20 });
|
|
79
|
+
expect(schema).toContain('💡 EXPLORATION GUIDE');
|
|
80
|
+
expect(schema).toContain('⚠️ Limits: DEPTH (max: 1)');
|
|
81
|
+
expect(schema).toContain('View keys:');
|
|
82
|
+
expect(schema).toContain('Check type:');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
describe('Key Limiting', () => {
|
|
86
|
+
it('should limit keys per object and show warning', () => {
|
|
87
|
+
const data = {};
|
|
88
|
+
for (let i = 0; i < 100; i++) {
|
|
89
|
+
data[`field_${i}`] = i;
|
|
90
|
+
}
|
|
91
|
+
const schema = generateQueryAssistSchema(data, { maxDepth: 2, maxPaths: 100, maxKeys: 20 });
|
|
92
|
+
expect(schema).toContain('💡 EXPLORATION GUIDE');
|
|
93
|
+
expect(schema).toContain('⚠️ Limits: KEYS (20 shown, 80 more)');
|
|
94
|
+
});
|
|
95
|
+
it('should show key exploration prompts when limit hit', () => {
|
|
96
|
+
const data = {};
|
|
97
|
+
for (let i = 0; i < 60; i++) {
|
|
98
|
+
data[`key${i}`] = `value${i}`;
|
|
99
|
+
}
|
|
100
|
+
const schema = generateQueryAssistSchema(data, { maxDepth: 2, maxPaths: 100, maxKeys: 30 });
|
|
101
|
+
expect(schema).toContain('💡 EXPLORATION GUIDE');
|
|
102
|
+
expect(schema).toContain('⚠️ Limits: KEYS (30 shown, 30 more)');
|
|
103
|
+
expect(schema).toContain('View keys:');
|
|
104
|
+
expect(schema).toContain('Count items:');
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
describe('Path Limiting', () => {
|
|
108
|
+
it('should limit total paths shown and prioritize important ones', () => {
|
|
109
|
+
// Create data with many fields to exceed path limit
|
|
110
|
+
const data = {
|
|
111
|
+
id: '123',
|
|
112
|
+
name: 'Test'
|
|
113
|
+
};
|
|
114
|
+
// Add many top-level fields
|
|
115
|
+
for (let i = 0; i < 20; i++) {
|
|
116
|
+
data[`field${i}`] = { nested: `value${i}` };
|
|
117
|
+
}
|
|
118
|
+
const schema = generateQueryAssistSchema(data, { maxDepth: 2, maxPaths: 10, maxKeys: 50 });
|
|
119
|
+
// Should show path limit warning in consolidated format
|
|
120
|
+
expect(schema).toContain('💡 EXPLORATION GUIDE');
|
|
121
|
+
expect(schema).toContain('PATHS (10 of');
|
|
122
|
+
// Should show some paths (prioritizes objects over primitives)
|
|
123
|
+
expect(schema).toContain('.field');
|
|
124
|
+
// Count number of path lines shown (should be exactly 10)
|
|
125
|
+
const pathLines = schema.split('\n').filter(l => l.includes(' → '));
|
|
126
|
+
expect(pathLines.length).toBeLessThanOrEqual(11); // 10 paths + root = 11
|
|
127
|
+
});
|
|
128
|
+
it('should show path exploration prompts when limit hit', () => {
|
|
129
|
+
const largeData = {};
|
|
130
|
+
for (let i = 0; i < 30; i++) {
|
|
131
|
+
largeData[`field${i}`] = { nested: 'value' };
|
|
132
|
+
}
|
|
133
|
+
const schema = generateQueryAssistSchema(largeData, { maxDepth: 2, maxPaths: 10, maxKeys: 50 });
|
|
134
|
+
expect(schema).toContain('💡 EXPLORATION GUIDE');
|
|
135
|
+
expect(schema).toContain('PATHS (10 of');
|
|
136
|
+
expect(schema).toContain('View keys:');
|
|
137
|
+
expect(schema).toContain('List all paths:');
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
describe('Mixed Schema Detection', () => {
|
|
141
|
+
it('should detect heterogeneous arrays', () => {
|
|
142
|
+
const data = {
|
|
143
|
+
items: [
|
|
144
|
+
{ type: 'book', pages: 200 },
|
|
145
|
+
{ type: 'video', duration: 120 },
|
|
146
|
+
{ type: 'audio', length: 180 }
|
|
147
|
+
]
|
|
148
|
+
};
|
|
149
|
+
const schema = generateQueryAssistSchema(data, { maxDepth: 2, maxPaths: 20 });
|
|
150
|
+
expect(schema).toContain('💡 EXPLORATION GUIDE');
|
|
151
|
+
expect(schema).toContain('MIXED SCHEMAS');
|
|
152
|
+
});
|
|
153
|
+
it('should show mixed schema exploration prompts', () => {
|
|
154
|
+
const data = {
|
|
155
|
+
data: [
|
|
156
|
+
{ a: 1 },
|
|
157
|
+
{ b: 2 },
|
|
158
|
+
{ c: 3 }
|
|
159
|
+
]
|
|
160
|
+
};
|
|
161
|
+
const schema = generateQueryAssistSchema(data, { maxDepth: 2, maxPaths: 20 });
|
|
162
|
+
expect(schema).toContain('💡 EXPLORATION GUIDE');
|
|
163
|
+
expect(schema).toContain('MIXED SCHEMAS');
|
|
164
|
+
expect(schema).toContain('Check variance:');
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
describe('Numeric Keys Detection', () => {
|
|
168
|
+
it('should detect numeric string keys and show representative structure', () => {
|
|
169
|
+
const data = {
|
|
170
|
+
'0': { name: 'Alice', age: 30 },
|
|
171
|
+
'1': { name: 'Bob', age: 25 },
|
|
172
|
+
'2': { name: 'Charlie', age: 35 }
|
|
173
|
+
};
|
|
174
|
+
const schema = generateQueryAssistSchema(data, { maxDepth: 3, maxPaths: 20 });
|
|
175
|
+
// Should detect numeric keys
|
|
176
|
+
expect(schema).toContain('object (numeric keys)');
|
|
177
|
+
expect(schema).toContain('(3 keys)');
|
|
178
|
+
// Should show representative structure with .[<idx>] notation
|
|
179
|
+
expect(schema).toContain('.[<idx>]');
|
|
180
|
+
// Should show nested structure of representative item
|
|
181
|
+
expect(schema).toContain('.[<idx>].name');
|
|
182
|
+
expect(schema).toContain('.[<idx>].age');
|
|
183
|
+
// Should show explanation note in exploration guide
|
|
184
|
+
expect(schema).toContain('💡 EXPLORATION GUIDE');
|
|
185
|
+
expect(schema).toContain('NUMERIC KEYS:');
|
|
186
|
+
expect(schema).toContain('.["0"], .["1"]');
|
|
187
|
+
expect(schema).toContain('.[0], .[1]');
|
|
188
|
+
// Should NOT enumerate individual keys
|
|
189
|
+
expect(schema).not.toContain('.0 ');
|
|
190
|
+
expect(schema).not.toContain('.1 ');
|
|
191
|
+
expect(schema).not.toContain('.2 ');
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
describe('Nullable Fields', () => {
|
|
195
|
+
it('should detect null values', () => {
|
|
196
|
+
const data = {
|
|
197
|
+
present: 'value',
|
|
198
|
+
missing: null,
|
|
199
|
+
empty: ''
|
|
200
|
+
};
|
|
201
|
+
const schema = generateQueryAssistSchema(data, { maxDepth: 2, maxPaths: 20 });
|
|
202
|
+
expect(schema).toContain('.missing');
|
|
203
|
+
expect(schema).toContain('null');
|
|
204
|
+
expect(schema).toContain('(nullable)');
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
describe('Schema Compactness', () => {
|
|
208
|
+
it('should not include common JQ patterns (moved to tool description)', () => {
|
|
209
|
+
const data = { simple: 'data' };
|
|
210
|
+
const schema = generateQueryAssistSchema(data, { maxDepth: 2, maxPaths: 20 });
|
|
211
|
+
// Common JQ patterns are now in the JQ tool description, not in every file reference
|
|
212
|
+
expect(schema).not.toContain('COMMON JQ PATTERNS:');
|
|
213
|
+
expect(schema).not.toContain('List all keys:');
|
|
214
|
+
// Schema should still contain the structure guide header
|
|
215
|
+
expect(schema).toContain('📊 STRUCTURE GUIDE');
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
describe('Size Constraints', () => {
|
|
219
|
+
it('should generate compact output for large structures', () => {
|
|
220
|
+
// Create a large nested structure
|
|
221
|
+
const data = {};
|
|
222
|
+
for (let i = 0; i < 100; i++) {
|
|
223
|
+
data[`key${i}`] = {
|
|
224
|
+
nested: {
|
|
225
|
+
deep: {
|
|
226
|
+
value: i
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
const schema = generateQueryAssistSchema(data, {
|
|
232
|
+
maxDepth: 2,
|
|
233
|
+
maxPaths: 20,
|
|
234
|
+
maxKeys: 50
|
|
235
|
+
});
|
|
236
|
+
// Schema should be compact (under 5KB as designed)
|
|
237
|
+
expect(schema.length).toBeLessThan(5000);
|
|
238
|
+
// Should contain warnings about limits
|
|
239
|
+
expect(schema).toContain('⚠️');
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
describe('Empty Data Handling', () => {
|
|
243
|
+
it('should handle empty object', () => {
|
|
244
|
+
const data = {};
|
|
245
|
+
const schema = generateQueryAssistSchema(data, { maxDepth: 2, maxPaths: 20 });
|
|
246
|
+
expect(schema).toContain('📊 STRUCTURE GUIDE');
|
|
247
|
+
expect(schema).toContain('(root)');
|
|
248
|
+
expect(schema).toContain('object');
|
|
249
|
+
expect(schema).toContain('(0 keys)');
|
|
250
|
+
});
|
|
251
|
+
it('should handle empty array', () => {
|
|
252
|
+
const data = { items: [] };
|
|
253
|
+
const schema = generateQueryAssistSchema(data, { maxDepth: 2, maxPaths: 20 });
|
|
254
|
+
expect(schema).toContain('.items');
|
|
255
|
+
expect(schema).toContain('array[0]');
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
describe('Data Size Reporting', () => {
|
|
259
|
+
it('should report data size in characters', () => {
|
|
260
|
+
const data = { test: 'value' };
|
|
261
|
+
const schema = generateQueryAssistSchema(data, { maxDepth: 2, maxPaths: 20 });
|
|
262
|
+
expect(schema).toContain('Size:');
|
|
263
|
+
expect(schema).toContain('characters');
|
|
264
|
+
expect(schema).toMatch(/Size: \d+(,\d{3})* characters/);
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
});
|
|
@@ -15,4 +15,4 @@ export declare function createFileWriter(config: FileWriterConfig): {
|
|
|
15
15
|
handleResponse: (toolName: string, args: Record<string, unknown>, responseData: unknown) => Promise<FileWriterResult | unknown>;
|
|
16
16
|
};
|
|
17
17
|
export type { FileWriterConfig, FileWriterResult } from './types.js';
|
|
18
|
-
export {
|
|
18
|
+
export { generateQueryAssistSchema } from './schema.js';
|
package/dist/fileWriter/index.js
CHANGED
|
@@ -1,15 +1,42 @@
|
|
|
1
|
-
import { JsonSchema, NullableFields } from '../types/index.js';
|
|
2
1
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
* Query-Assist Schema Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates compact, LLM-friendly schemas optimized for crafting JQ queries.
|
|
5
|
+
* Uses JQ-style path notation (.items[].price) instead of JSON Schema.
|
|
6
|
+
* Includes exploration prompts when limits are reached.
|
|
7
7
|
*/
|
|
8
|
-
export
|
|
8
|
+
export interface PathInfo {
|
|
9
|
+
path: string;
|
|
10
|
+
type: string;
|
|
11
|
+
depth: number;
|
|
12
|
+
nullable?: boolean;
|
|
13
|
+
arrayLength?: number;
|
|
14
|
+
keyCount?: number;
|
|
15
|
+
mixed?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface LimitMetadata {
|
|
18
|
+
depthLimitHit: boolean;
|
|
19
|
+
keyLimitHit: boolean;
|
|
20
|
+
pathLimitHit: boolean;
|
|
21
|
+
mixedSchemasDetected: boolean;
|
|
22
|
+
maxDepth: number;
|
|
23
|
+
maxKeys: number;
|
|
24
|
+
maxPaths: number;
|
|
25
|
+
totalPathsFound: number;
|
|
26
|
+
deepestPathTruncated?: string;
|
|
27
|
+
truncatedKeyCount?: number;
|
|
28
|
+
}
|
|
29
|
+
export interface QueryAssistOptions {
|
|
30
|
+
maxDepth?: number;
|
|
31
|
+
maxPaths?: number;
|
|
32
|
+
maxKeys?: number;
|
|
33
|
+
dataSize?: number;
|
|
34
|
+
}
|
|
9
35
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* @param
|
|
13
|
-
* @
|
|
36
|
+
* Generate query-assist schema for JSON data
|
|
37
|
+
* Main entry point for schema generation
|
|
38
|
+
* @param data - JSON data to analyze
|
|
39
|
+
* @param options - Configuration options
|
|
40
|
+
* @returns Compact text schema optimized for JQ queries
|
|
14
41
|
*/
|
|
15
|
-
export declare function
|
|
42
|
+
export declare function generateQueryAssistSchema(data: unknown, options?: QueryAssistOptions): string;
|
|
@@ -1,120 +1,270 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Query-Assist Schema Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates compact, LLM-friendly schemas optimized for crafting JQ queries.
|
|
5
|
+
* Uses JQ-style path notation (.items[].price) instead of JSON Schema.
|
|
6
|
+
* Includes exploration prompts when limits are reached.
|
|
6
7
|
*/
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Collect all paths from JSON data with limits applied
|
|
10
|
+
* @param data - The JSON data to analyze
|
|
11
|
+
* @param maxDepth - Maximum depth to traverse (default: 2)
|
|
12
|
+
* @param maxKeys - Maximum keys to analyze per object (default: 50)
|
|
13
|
+
* @returns Array of path information and limit metadata
|
|
14
|
+
*/
|
|
15
|
+
function collectPaths(data, maxDepth = 2, maxKeys = 50) {
|
|
16
|
+
const paths = [];
|
|
17
|
+
const limits = {
|
|
18
|
+
depthLimitHit: false,
|
|
19
|
+
keyLimitHit: false,
|
|
20
|
+
pathLimitHit: false,
|
|
21
|
+
mixedSchemasDetected: false,
|
|
22
|
+
maxDepth,
|
|
23
|
+
maxKeys,
|
|
24
|
+
maxPaths: 0, // Will be set later
|
|
25
|
+
totalPathsFound: 0
|
|
26
|
+
};
|
|
27
|
+
function traverse(val, path, depth) {
|
|
28
|
+
// Hard stop at max depth
|
|
29
|
+
if (depth > maxDepth) {
|
|
30
|
+
limits.depthLimitHit = true;
|
|
31
|
+
if (!limits.deepestPathTruncated) {
|
|
32
|
+
limits.deepestPathTruncated = path;
|
|
23
33
|
}
|
|
34
|
+
return;
|
|
24
35
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if (hasNumericKeys) {
|
|
28
|
-
schema._keysAreNumeric = true;
|
|
29
|
-
schema._accessPattern = 'Use .["0"] not .[0]';
|
|
36
|
+
if (val === null) {
|
|
37
|
+
paths.push({ path, type: 'null', depth, nullable: true });
|
|
30
38
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
39
|
+
else if (Array.isArray(val)) {
|
|
40
|
+
paths.push({ path, type: 'array', depth, arrayLength: val.length });
|
|
41
|
+
if (val.length === 0) {
|
|
42
|
+
return; // Empty array, nothing to explore
|
|
43
|
+
}
|
|
44
|
+
// Sample first 5 items to detect schema variance
|
|
45
|
+
const sample = val.slice(0, Math.min(5, val.length));
|
|
46
|
+
const types = new Set(sample.map(item => item === null
|
|
47
|
+
? 'null'
|
|
48
|
+
: Array.isArray(item)
|
|
49
|
+
? 'array'
|
|
50
|
+
: typeof item));
|
|
51
|
+
// Detect mixed schemas (heterogeneous arrays)
|
|
52
|
+
// Mixed if we have more than 1 distinct type
|
|
53
|
+
const mixed = types.size > 1;
|
|
54
|
+
if (mixed) {
|
|
55
|
+
limits.mixedSchemasDetected = true;
|
|
56
|
+
}
|
|
57
|
+
// Traverse first non-null item
|
|
58
|
+
const first = sample.find(v => v !== null);
|
|
59
|
+
if (first !== undefined) {
|
|
60
|
+
const arrayPath = `${path}[]`;
|
|
61
|
+
if (mixed) {
|
|
62
|
+
// For mixed arrays, only show the mixed marker (don't traverse to avoid duplicate paths)
|
|
63
|
+
paths.push({ path: arrayPath, type: 'mixed', depth: depth + 1, mixed: true });
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
// For uniform arrays, traverse to show the structure
|
|
67
|
+
traverse(first, arrayPath, depth + 1);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// For objects in arrays, also check if they have different keys
|
|
71
|
+
if (types.has('object')) {
|
|
72
|
+
const objects = sample.filter(v => v && typeof v === 'object' && !Array.isArray(v));
|
|
73
|
+
if (objects.length >= 2) {
|
|
74
|
+
const keySets = objects.map(o => new Set(Object.keys(o)));
|
|
75
|
+
// Check if any two objects have different keys
|
|
76
|
+
for (let i = 0; i < keySets.length - 1; i++) {
|
|
77
|
+
const keys1 = Array.from(keySets[i]);
|
|
78
|
+
const keys2 = Array.from(keySets[i + 1]);
|
|
79
|
+
if (keys1.length !== keys2.length || !keys1.every(k => keySets[i + 1].has(k))) {
|
|
80
|
+
limits.mixedSchemasDetected = true;
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
37
86
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
87
|
+
else if (typeof val === 'object') {
|
|
88
|
+
const keys = Object.keys(val).sort();
|
|
89
|
+
// Check for numeric keys (common pattern - treat as collection like arrays)
|
|
90
|
+
const numericKeys = keys.filter(k => /^\d+$/.test(k));
|
|
91
|
+
const hasNumericKeys = keys.length > 0 && numericKeys.length >= keys.length * 0.8;
|
|
92
|
+
// Always show key count (including 0)
|
|
93
|
+
paths.push({
|
|
94
|
+
path,
|
|
95
|
+
type: hasNumericKeys ? 'object (numeric keys)' : 'object',
|
|
96
|
+
depth,
|
|
97
|
+
keyCount: keys.length
|
|
98
|
+
});
|
|
99
|
+
if (hasNumericKeys) {
|
|
100
|
+
// Treat as collection - show ONE representative item structure
|
|
101
|
+
// This avoids enumerating .0, .1, .2, ... which is repetitive and wastes space
|
|
102
|
+
const representativePath = path ? `${path}.[<idx>]` : `.[<idx>]`;
|
|
103
|
+
// Pick first key to show structure
|
|
104
|
+
if (keys.length > 0) {
|
|
105
|
+
const firstKey = keys[0];
|
|
106
|
+
traverse(val[firstKey], representativePath, depth + 1);
|
|
107
|
+
}
|
|
47
108
|
}
|
|
48
109
|
else {
|
|
49
|
-
|
|
110
|
+
// Normal object - traverse keys individually
|
|
111
|
+
const keysToAnalyze = keys.slice(0, maxKeys);
|
|
112
|
+
if (keys.length > maxKeys) {
|
|
113
|
+
limits.keyLimitHit = true;
|
|
114
|
+
limits.truncatedKeyCount = keys.length - maxKeys;
|
|
115
|
+
}
|
|
116
|
+
// Traverse child keys
|
|
117
|
+
for (const key of keysToAnalyze) {
|
|
118
|
+
const childPath = path ? `${path}.${key}` : `.${key}`;
|
|
119
|
+
traverse(val[key], childPath, depth + 1);
|
|
120
|
+
}
|
|
50
121
|
}
|
|
51
122
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
? analyzeJsonSchema(arr[0], `${path}[0]`)
|
|
56
|
-
: { types: Array.from(itemTypes) },
|
|
57
|
-
length: arr.length,
|
|
58
|
-
};
|
|
59
|
-
// Add hints for null handling
|
|
60
|
-
if (hasNulls) {
|
|
61
|
-
schema._hasNulls = true;
|
|
123
|
+
else {
|
|
124
|
+
// Primitive types
|
|
125
|
+
paths.push({ path, type: typeof val, depth });
|
|
62
126
|
}
|
|
63
|
-
return schema;
|
|
64
|
-
}
|
|
65
|
-
else {
|
|
66
|
-
return { type };
|
|
67
127
|
}
|
|
128
|
+
traverse(data, '', 0);
|
|
129
|
+
limits.totalPathsFound = paths.length;
|
|
130
|
+
return { paths, limits };
|
|
68
131
|
}
|
|
69
132
|
/**
|
|
70
|
-
*
|
|
71
|
-
* @param
|
|
72
|
-
* @param
|
|
73
|
-
* @returns
|
|
133
|
+
* Select top N most relevant paths using scoring
|
|
134
|
+
* @param paths - All collected paths
|
|
135
|
+
* @param maxPaths - Maximum paths to return
|
|
136
|
+
* @returns Prioritized subset of paths
|
|
74
137
|
*/
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
138
|
+
function selectTopPaths(paths, maxPaths) {
|
|
139
|
+
// Score each path based on relevance
|
|
140
|
+
const scored = paths.map(p => ({
|
|
141
|
+
...p,
|
|
142
|
+
score: (p.nullable ? 0 : 10) + // Non-null = higher priority
|
|
143
|
+
(3 - p.depth) * 5 + // Shallower = higher priority
|
|
144
|
+
(p.type === 'array' ? 5 : 0) + // Arrays = interesting
|
|
145
|
+
(p.type === 'object' || p.type === 'object (numeric keys)' ? 3 : 0) + // Objects = interesting
|
|
146
|
+
(p.mixed ? 2 : 0) // Mixed types = interesting
|
|
147
|
+
}));
|
|
148
|
+
// Sort by score (desc), then by path length (asc) for stability
|
|
149
|
+
return scored
|
|
150
|
+
.sort((a, b) => {
|
|
151
|
+
if (b.score !== a.score)
|
|
152
|
+
return b.score - a.score;
|
|
153
|
+
return a.path.length - b.path.length;
|
|
154
|
+
})
|
|
155
|
+
.slice(0, maxPaths);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Format paths as query-assist text with exploration prompts
|
|
159
|
+
* @param paths - Selected paths to display
|
|
160
|
+
* @param limits - Limit metadata for generating prompts
|
|
161
|
+
* @param dataSize - Size of original data in characters
|
|
162
|
+
* @returns Formatted text schema
|
|
163
|
+
*/
|
|
164
|
+
function formatQueryAssist(paths, limits, dataSize) {
|
|
165
|
+
let output = '📊 STRUCTURE GUIDE (for JQ queries)\n\n';
|
|
166
|
+
output += `Size: ${dataSize.toLocaleString()} characters\n\n`;
|
|
167
|
+
// Group paths by depth
|
|
168
|
+
const byDepth = {};
|
|
169
|
+
for (const p of paths) {
|
|
170
|
+
if (!byDepth[p.depth])
|
|
171
|
+
byDepth[p.depth] = [];
|
|
172
|
+
byDepth[p.depth].push(p);
|
|
173
|
+
}
|
|
174
|
+
// Format paths by depth levels
|
|
175
|
+
const depths = Object.keys(byDepth)
|
|
176
|
+
.map(Number)
|
|
177
|
+
.sort((a, b) => a - b);
|
|
178
|
+
for (const depth of depths) {
|
|
179
|
+
const depthPaths = byDepth[depth];
|
|
180
|
+
const label = depth === 0 ? 'ROOT' : depth === 1 ? 'TOP-LEVEL' : `NESTED (depth ${depth})`;
|
|
181
|
+
output += `${label}:\n`;
|
|
182
|
+
for (const p of depthPaths) {
|
|
183
|
+
const pathStr = p.path || '(root)';
|
|
184
|
+
output += ` ${pathStr.padEnd(35)}`;
|
|
185
|
+
output += ` → ${p.type}`;
|
|
186
|
+
if (p.arrayLength !== undefined) {
|
|
187
|
+
output += `[${p.arrayLength}]`;
|
|
94
188
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (schemaObj.type === 'object' && schemaObj.properties) {
|
|
98
|
-
const props = schemaObj.properties;
|
|
99
|
-
for (const [key, value] of Object.entries(props)) {
|
|
100
|
-
const newPath = path ? `${path}.${key}` : key;
|
|
101
|
-
traverse(value, newPath);
|
|
189
|
+
if (p.keyCount !== undefined) {
|
|
190
|
+
output += ` (${p.keyCount} keys)`;
|
|
102
191
|
}
|
|
192
|
+
if (p.nullable) {
|
|
193
|
+
output += ' (nullable)';
|
|
194
|
+
}
|
|
195
|
+
if (p.mixed) {
|
|
196
|
+
output += ' ⚠️ MIXED SCHEMAS';
|
|
197
|
+
}
|
|
198
|
+
if (depth === limits.maxDepth && (p.type === 'object' || p.type === 'object (numeric keys)' || p.type === 'array')) {
|
|
199
|
+
output += ' ⚠️ DEPTH LIMIT';
|
|
200
|
+
}
|
|
201
|
+
output += '\n';
|
|
103
202
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
203
|
+
output += '\n';
|
|
204
|
+
}
|
|
205
|
+
// Check if we have numeric-keyed objects in the output
|
|
206
|
+
const hasNumericKeys = paths.some(p => p.path.includes('.[<idx>]'));
|
|
207
|
+
// Build exploration guide if any limits were hit or special patterns detected
|
|
208
|
+
const hasLimits = limits.depthLimitHit || limits.keyLimitHit || limits.pathLimitHit || limits.mixedSchemasDetected;
|
|
209
|
+
if (hasNumericKeys || hasLimits) {
|
|
210
|
+
output += '💡 EXPLORATION GUIDE\n\n';
|
|
211
|
+
// Numeric keys note (data-specific, keep separate)
|
|
212
|
+
if (hasNumericKeys) {
|
|
213
|
+
output += 'NUMERIC KEYS: .[<idx>] represents structure shared by all numeric keys\n';
|
|
214
|
+
output += ' Access: .["0"], .["1"] or .[0], .[1] (array-style) | List: keys\n\n';
|
|
215
|
+
}
|
|
216
|
+
// Show which limits were hit
|
|
217
|
+
if (hasLimits) {
|
|
218
|
+
const limitWarnings = [];
|
|
219
|
+
if (limits.depthLimitHit) {
|
|
220
|
+
limitWarnings.push(`DEPTH (max: ${limits.maxDepth})`);
|
|
221
|
+
}
|
|
222
|
+
if (limits.keyLimitHit && limits.truncatedKeyCount) {
|
|
223
|
+
limitWarnings.push(`KEYS (${limits.maxKeys} shown, ${limits.truncatedKeyCount} more)`);
|
|
224
|
+
}
|
|
225
|
+
if (limits.pathLimitHit) {
|
|
226
|
+
limitWarnings.push(`PATHS (${limits.maxPaths} of ${limits.totalPathsFound})`);
|
|
227
|
+
}
|
|
228
|
+
if (limits.mixedSchemasDetected) {
|
|
229
|
+
limitWarnings.push('MIXED SCHEMAS');
|
|
115
230
|
}
|
|
231
|
+
output += `⚠️ Limits: ${limitWarnings.join(' | ')}\n\n`;
|
|
232
|
+
// Generic JQ exploration patterns
|
|
233
|
+
output += 'Common JQ patterns:\n';
|
|
234
|
+
output += ' • View keys: <path> | keys\n';
|
|
235
|
+
output += ' • Check type: <path> | type\n';
|
|
236
|
+
output += ' • Count items: <path> | length\n';
|
|
237
|
+
output += ' • Search keys: keys | map(select(contains("term")))\n';
|
|
238
|
+
output += ' • List all paths: paths(scalars) | map(join("."))\n';
|
|
239
|
+
output += ' • Filter arrays: .[] | select(type == "object")\n';
|
|
240
|
+
if (limits.mixedSchemasDetected) {
|
|
241
|
+
output += ' • Check variance: .[] | type or [:3] | map(keys)\n';
|
|
242
|
+
}
|
|
243
|
+
output += '\n';
|
|
116
244
|
}
|
|
117
245
|
}
|
|
118
|
-
|
|
119
|
-
|
|
246
|
+
return output;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Generate query-assist schema for JSON data
|
|
250
|
+
* Main entry point for schema generation
|
|
251
|
+
* @param data - JSON data to analyze
|
|
252
|
+
* @param options - Configuration options
|
|
253
|
+
* @returns Compact text schema optimized for JQ queries
|
|
254
|
+
*/
|
|
255
|
+
export function generateQueryAssistSchema(data, options = {}) {
|
|
256
|
+
const maxDepth = options.maxDepth ?? 2;
|
|
257
|
+
const maxPaths = options.maxPaths ?? 20;
|
|
258
|
+
const maxKeys = options.maxKeys ?? 50;
|
|
259
|
+
// Collect paths with limits
|
|
260
|
+
const { paths, limits } = collectPaths(data, maxDepth, maxKeys);
|
|
261
|
+
// Select top paths
|
|
262
|
+
const selectedPaths = selectTopPaths(paths, maxPaths);
|
|
263
|
+
// Update limit metadata
|
|
264
|
+
limits.maxPaths = maxPaths;
|
|
265
|
+
limits.pathLimitHit = paths.length > maxPaths;
|
|
266
|
+
// Calculate data size (use provided size if available to avoid re-stringifying)
|
|
267
|
+
const dataSize = options.dataSize ?? JSON.stringify(data).length;
|
|
268
|
+
// Format as text
|
|
269
|
+
return formatQueryAssist(selectedPaths, limits, dataSize);
|
|
120
270
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { generateCompactFilename } from '../utils/filename.js';
|
|
4
|
-
import {
|
|
4
|
+
import { generateQueryAssistSchema } from './schema.js';
|
|
5
5
|
// Default minimum character count to trigger file writing
|
|
6
6
|
const DEFAULT_MIN_CHARS = 1000;
|
|
7
7
|
/**
|
|
@@ -160,8 +160,8 @@ const extractContentForFile = (responseData) => {
|
|
|
160
160
|
* @returns Either the original response or a file reference response
|
|
161
161
|
*/
|
|
162
162
|
export async function handleToolResponse(config, toolName, args, responseData) {
|
|
163
|
-
//
|
|
164
|
-
if (toolName === 'execute_jq_query') {
|
|
163
|
+
// Some tools should always return directly to AI (never write to file)
|
|
164
|
+
if (toolName === 'execute_jq_query' || toolName === 'get_label_schema') {
|
|
165
165
|
return responseData;
|
|
166
166
|
}
|
|
167
167
|
// If there's an error, return proper MCP error response (never write errors to file)
|
|
@@ -199,66 +199,28 @@ export async function handleToolResponse(config, toolName, args, responseData) {
|
|
|
199
199
|
await fs.mkdir(config.outputPath, { recursive: true });
|
|
200
200
|
// Write the exact content we counted
|
|
201
201
|
await fs.writeFile(filepath, contentToWrite);
|
|
202
|
-
//
|
|
202
|
+
// Generate query-assist schema if we have valid JSON
|
|
203
203
|
let schemaInfo = '';
|
|
204
|
-
let quickReference = '';
|
|
205
204
|
if (parsedForSchema) {
|
|
206
205
|
// Use the clean data (without pagination) for schema analysis
|
|
207
206
|
const { pagination, has_more, next_page, previous_page, page, page_size, total_pages, ...cleanData } = parsedForSchema;
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}
|
|
217
|
-
else if (schemaObj.type === 'array') {
|
|
218
|
-
quickReference += ` • Structure: Array with ${schemaObj.length} items\n`;
|
|
219
|
-
}
|
|
220
|
-
else if (schemaObj.type === 'object' && schemaObj.properties) {
|
|
221
|
-
const props = schemaObj.properties;
|
|
222
|
-
const keys = Object.keys(props).slice(0, 5).join(', ');
|
|
223
|
-
quickReference += ` • Structure: Object with keys: ${keys}\n`;
|
|
224
|
-
}
|
|
225
|
-
// Always null fields
|
|
226
|
-
if (nullFields.alwaysNull.length > 0) {
|
|
227
|
-
const fieldList = nullFields.alwaysNull.slice(0, 5).join(', ');
|
|
228
|
-
const more = nullFields.alwaysNull.length > 5
|
|
229
|
-
? ` (+${nullFields.alwaysNull.length - 5} more)`
|
|
230
|
-
: '';
|
|
231
|
-
quickReference += ` • Always null: ${fieldList}${more}\n`;
|
|
232
|
-
}
|
|
233
|
-
// Nullable fields
|
|
234
|
-
if (nullFields.nullable.length > 0) {
|
|
235
|
-
const fieldList = nullFields.nullable.slice(0, 5).join(', ');
|
|
236
|
-
const more = nullFields.nullable.length > 5
|
|
237
|
-
? ` (+${nullFields.nullable.length - 5} more)`
|
|
238
|
-
: '';
|
|
239
|
-
quickReference += ` • Sometimes null: ${fieldList}${more}\n`;
|
|
240
|
-
}
|
|
241
|
-
// Suggest exploratory queries
|
|
242
|
-
if (schemaObj._keysAreNumeric) {
|
|
243
|
-
quickReference += ` • Explore: keys, .["0"] | keys, .["0"]\n`;
|
|
244
|
-
}
|
|
245
|
-
else if (schemaObj.type === 'array' && schemaObj.length > 0) {
|
|
246
|
-
quickReference += ` • Explore: length, .[0] | keys, .[0]\n`;
|
|
247
|
-
}
|
|
248
|
-
else {
|
|
249
|
-
quickReference += ` • Explore: keys, type\n`;
|
|
250
|
-
}
|
|
251
|
-
// Full schema
|
|
252
|
-
schemaInfo = `\n\nFull JSON Schema:\n${JSON.stringify(schema, null, 2)}`;
|
|
207
|
+
// Generate compact query-assist schema using config values
|
|
208
|
+
// Pass contentLength to avoid re-stringifying large payloads
|
|
209
|
+
schemaInfo = `\n\n${generateQueryAssistSchema(cleanData, {
|
|
210
|
+
maxDepth: config.schemaMaxDepth ?? 2,
|
|
211
|
+
maxPaths: config.schemaMaxPaths ?? 20,
|
|
212
|
+
maxKeys: config.schemaMaxKeys ?? 50,
|
|
213
|
+
dataSize: contentLength
|
|
214
|
+
})}`;
|
|
253
215
|
}
|
|
254
216
|
// Count lines in the content
|
|
255
217
|
const lineCount = contentToWrite.split('\n').length;
|
|
256
|
-
// Return success message with file path, size, lines,
|
|
218
|
+
// Return success message with file path, size, lines, and schema
|
|
257
219
|
return {
|
|
258
220
|
content: [
|
|
259
221
|
{
|
|
260
222
|
type: 'text',
|
|
261
|
-
text: `📄 File: ${filepath}\nSize: ${contentToWrite.length} characters | Lines: ${lineCount}${
|
|
223
|
+
text: `📄 File: ${filepath}\nSize: ${contentToWrite.length} characters | Lines: ${lineCount}${schemaInfo}`,
|
|
262
224
|
},
|
|
263
225
|
],
|
|
264
226
|
};
|
package/dist/index.js
CHANGED
|
@@ -95,6 +95,24 @@ const ENABLE_JQ = process.env.MCP_PROXY_ENABLE_JQ !== 'false'; // default true
|
|
|
95
95
|
* Timeout in milliseconds for JQ query execution
|
|
96
96
|
*/
|
|
97
97
|
const JQ_TIMEOUT_MS = parseInt(process.env.MCP_PROXY_JQ_TIMEOUT_MS || '30000');
|
|
98
|
+
/**
|
|
99
|
+
* MCP_PROXY_SCHEMA_MAX_DEPTH (OPTIONAL, default: 3)
|
|
100
|
+
* Maximum depth to traverse when generating query-assist schemas
|
|
101
|
+
* Deeper structures will show exploration prompts instead
|
|
102
|
+
*/
|
|
103
|
+
const SCHEMA_MAX_DEPTH = parseInt(process.env.MCP_PROXY_SCHEMA_MAX_DEPTH || '3');
|
|
104
|
+
/**
|
|
105
|
+
* MCP_PROXY_SCHEMA_MAX_PATHS (OPTIONAL, default: 20)
|
|
106
|
+
* Maximum number of paths to show in query-assist schemas
|
|
107
|
+
* Prioritizes non-null, shallow, and interesting paths
|
|
108
|
+
*/
|
|
109
|
+
const SCHEMA_MAX_PATHS = parseInt(process.env.MCP_PROXY_SCHEMA_MAX_PATHS || '20');
|
|
110
|
+
/**
|
|
111
|
+
* MCP_PROXY_SCHEMA_MAX_KEYS (OPTIONAL, default: 50)
|
|
112
|
+
* Maximum number of object keys to analyze per object
|
|
113
|
+
* Objects with more keys will show a key limit warning
|
|
114
|
+
*/
|
|
115
|
+
const SCHEMA_MAX_KEYS = parseInt(process.env.MCP_PROXY_SCHEMA_MAX_KEYS || '50');
|
|
98
116
|
/**
|
|
99
117
|
* MCP_PROXY_ENABLE_LOGGING (OPTIONAL, default: false)
|
|
100
118
|
* Enable debug logging for the proxy
|
|
@@ -228,7 +246,10 @@ async function main() {
|
|
|
228
246
|
enabled: WRITE_TO_FILE,
|
|
229
247
|
outputPath: OUTPUT_PATH,
|
|
230
248
|
minCharsForWrite: MIN_CHARS_FOR_WRITE,
|
|
231
|
-
toolAbbreviations: {} // No service-specific abbreviations (generic proxy)
|
|
249
|
+
toolAbbreviations: {}, // No service-specific abbreviations (generic proxy)
|
|
250
|
+
schemaMaxDepth: SCHEMA_MAX_DEPTH,
|
|
251
|
+
schemaMaxPaths: SCHEMA_MAX_PATHS,
|
|
252
|
+
schemaMaxKeys: SCHEMA_MAX_KEYS
|
|
232
253
|
};
|
|
233
254
|
const fileWriter = createFileWriter(fileWriterConfig);
|
|
234
255
|
// JQ tool configuration
|
package/dist/jq/tool.js
CHANGED
|
@@ -73,6 +73,21 @@ export const JQ_TOOL_DEFINITION = {
|
|
|
73
73
|
'\n2. **Incremental filtering**: Start with no filters, add conditions one by one' +
|
|
74
74
|
'\n3. **Alternative null handling**: Use `// empty`, `select(. != null)`, or `try ... catch`' +
|
|
75
75
|
'\n4. **Simplified queries**: Break complex queries into smaller, testable parts' +
|
|
76
|
+
'\n\n## COMMON JQ PATTERNS (Quick Reference):' +
|
|
77
|
+
'\n- **List all keys**: `keys` or `.[] | keys` (for nested)' +
|
|
78
|
+
'\n- **Check type**: `type` or `.field | type`' +
|
|
79
|
+
'\n- **Array length**: `.items | length` or `[.[]] | length`' +
|
|
80
|
+
'\n- **Filter array**: `.items[] | select(.price > 100)` or `select(.field == "value")`' +
|
|
81
|
+
'\n- **Extract field**: `.items[].id` or `.[] | .field`' +
|
|
82
|
+
'\n- **Get unique values**: `.items[].type | unique` or `[.[].field] | unique`' +
|
|
83
|
+
'\n- **Find nulls**: `.items[] | select(.field == null)` or `select(.field)` (non-null only)' +
|
|
84
|
+
'\n- **Count occurrences**: `group_by(.type) | map({type: .[0].type, count: length})`' +
|
|
85
|
+
'\n- **Sort**: `sort_by(.price)` or `sort_by(.price) | reverse` (descending)' +
|
|
86
|
+
'\n- **Map transform**: `[.[] | {id: .id, name: .name}]` (extract subset of fields)' +
|
|
87
|
+
'\n- **First N items**: `.[:5]` (array slice)' +
|
|
88
|
+
'\n- **Limit stream**: `limit(10; .[])` (stream processing)' +
|
|
89
|
+
'\n- **Default values**: `.field // "default"` or `.field // empty`' +
|
|
90
|
+
'\n- **Conditional**: `if .price > 100 then "expensive" else "cheap" end`' +
|
|
76
91
|
'\n\n## COMPREHENSIVE EXAMPLES:' +
|
|
77
92
|
'\n**Debugging sequence for Cypher results**:' +
|
|
78
93
|
'\n- `keys` → ["0", "1", "2", ...] (shows object structure)' +
|
package/dist/types/index.d.ts
CHANGED
|
@@ -10,6 +10,12 @@ export interface FileWriterConfig {
|
|
|
10
10
|
minCharsForWrite?: number;
|
|
11
11
|
/** Custom abbreviations for tool names in filenames */
|
|
12
12
|
toolAbbreviations?: Record<string, string>;
|
|
13
|
+
/** Maximum depth for schema generation (default: 2) */
|
|
14
|
+
schemaMaxDepth?: number;
|
|
15
|
+
/** Maximum paths to show in schema (default: 20) */
|
|
16
|
+
schemaMaxPaths?: number;
|
|
17
|
+
/** Maximum keys to analyze per object (default: 50) */
|
|
18
|
+
schemaMaxKeys?: number;
|
|
13
19
|
}
|
|
14
20
|
/**
|
|
15
21
|
* Configuration for the JQ tool
|