@anyshift/mcp-proxy 0.2.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 +314 -0
- package/dist/fileWriter/index.d.ts +18 -0
- package/dist/fileWriter/index.js +21 -0
- package/dist/fileWriter/schema.d.ts +15 -0
- package/dist/fileWriter/schema.js +120 -0
- package/dist/fileWriter/types.d.ts +4 -0
- package/dist/fileWriter/types.js +1 -0
- package/dist/fileWriter/writer.d.ts +10 -0
- package/dist/fileWriter/writer.js +277 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +346 -0
- package/dist/jq/handler.d.ts +14 -0
- package/dist/jq/handler.js +90 -0
- package/dist/jq/index.d.ts +51 -0
- package/dist/jq/index.js +26 -0
- package/dist/jq/tool.d.ts +43 -0
- package/dist/jq/tool.js +106 -0
- package/dist/jq/types.d.ts +4 -0
- package/dist/jq/types.js +1 -0
- package/dist/truncation/index.d.ts +8 -0
- package/dist/truncation/index.js +7 -0
- package/dist/truncation/truncate.d.ts +28 -0
- package/dist/truncation/truncate.js +80 -0
- package/dist/truncation/types.d.ts +26 -0
- package/dist/truncation/types.js +1 -0
- package/dist/types/index.d.ts +52 -0
- package/dist/types/index.js +1 -0
- package/dist/utils/filename.d.ts +8 -0
- package/dist/utils/filename.js +42 -0
- package/dist/utils/pathValidation.d.ts +8 -0
- package/dist/utils/pathValidation.js +42 -0
- package/package.json +32 -0
package/README.md
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
# @anyshift/mcp-proxy
|
|
2
|
+
|
|
3
|
+
**Universal MCP Proxy** - Add truncation, file writing, and JQ capabilities to ANY Model Context Protocol (MCP) server.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
🔄 **100% MCP-Agnostic**: Works with any MCP server through environment variable contract
|
|
8
|
+
✂️ **Response Truncation**: Auto-truncate large responses to token limits
|
|
9
|
+
💾 **Automatic File Writing**: Save large responses to disk
|
|
10
|
+
🔍 **JQ Tool**: Query saved JSON files with JQ syntax
|
|
11
|
+
🎯 **Zero Configuration**: No config files, just environment variables
|
|
12
|
+
|
|
13
|
+
## Why Use This?
|
|
14
|
+
|
|
15
|
+
MCP servers often return very large responses (dashboards, metrics, logs) that:
|
|
16
|
+
- Exceed AI model context windows (causing truncation or errors)
|
|
17
|
+
- Make it hard for AI to synthesize information
|
|
18
|
+
- Cannot be easily queried or analyzed
|
|
19
|
+
|
|
20
|
+
This proxy solves these problems by:
|
|
21
|
+
1. **Truncating** responses to configurable token limits
|
|
22
|
+
2. **Saving** full responses to disk automatically
|
|
23
|
+
3. **Adding** a JQ tool to query saved JSON files
|
|
24
|
+
|
|
25
|
+
## Environment Variable Contract
|
|
26
|
+
|
|
27
|
+
The proxy uses a **namespace convention** to separate its configuration from the child MCP server's configuration:
|
|
28
|
+
|
|
29
|
+
### Proxy Configuration (MCP_PROXY_* prefix)
|
|
30
|
+
|
|
31
|
+
These variables control the proxy's behavior:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# REQUIRED: Child MCP specification
|
|
35
|
+
MCP_PROXY_CHILD_COMMAND="mcp-grafana" # Command to spawn child MCP
|
|
36
|
+
|
|
37
|
+
# OPTIONAL: Child command arguments
|
|
38
|
+
MCP_PROXY_CHILD_ARGS="arg1,arg2" # Comma-separated arguments
|
|
39
|
+
|
|
40
|
+
# OPTIONAL: Truncation settings (defaults shown)
|
|
41
|
+
MCP_PROXY_MAX_TOKENS=10000 # Max tokens before truncation
|
|
42
|
+
MCP_PROXY_CHARS_PER_TOKEN=4 # Chars per token calculation
|
|
43
|
+
|
|
44
|
+
# OPTIONAL: File writing settings
|
|
45
|
+
MCP_PROXY_WRITE_TO_FILE=true # Enable file writing (default: false)
|
|
46
|
+
MCP_PROXY_OUTPUT_PATH=/tmp/mcp-results # REQUIRED if WRITE_TO_FILE=true
|
|
47
|
+
MCP_PROXY_MIN_CHARS_FOR_WRITE=1000 # Min size to save (default: 1000)
|
|
48
|
+
|
|
49
|
+
# OPTIONAL: JQ tool settings
|
|
50
|
+
MCP_PROXY_ENABLE_JQ=true # Enable JQ tool (default: true)
|
|
51
|
+
MCP_PROXY_JQ_TIMEOUT_MS=30000 # JQ timeout (default: 30000)
|
|
52
|
+
|
|
53
|
+
# OPTIONAL: Debug logging
|
|
54
|
+
MCP_PROXY_ENABLE_LOGGING=true # Enable debug logs (default: false)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Pass-Through Variables (NO prefix)
|
|
58
|
+
|
|
59
|
+
All other environment variables are passed directly to the child MCP:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# Grafana example
|
|
63
|
+
GRAFANA_URL=https://your-instance.grafana.net
|
|
64
|
+
GRAFANA_SERVICE_ACCOUNT_TOKEN=glsa_...
|
|
65
|
+
|
|
66
|
+
# Datadog example
|
|
67
|
+
DATADOG_API_KEY=...
|
|
68
|
+
DATADOG_APP_KEY=...
|
|
69
|
+
|
|
70
|
+
# Anyshift example
|
|
71
|
+
API_TOKEN=...
|
|
72
|
+
API_BASE_URL=...
|
|
73
|
+
|
|
74
|
+
# Any other env vars your MCP needs
|
|
75
|
+
CUSTOM_VAR=value
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Key Design Principle:** The proxy knows nothing about specific MCP servers. It simply:
|
|
79
|
+
1. Reads `MCP_PROXY_*` vars for its own config
|
|
80
|
+
2. Passes everything else to the child MCP
|
|
81
|
+
|
|
82
|
+
## Quick Start Examples
|
|
83
|
+
|
|
84
|
+
### Example 1: Wrap Grafana MCP
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# Proxy configuration
|
|
88
|
+
export MCP_PROXY_CHILD_COMMAND="mcp-grafana"
|
|
89
|
+
export MCP_PROXY_MAX_TOKENS=10000
|
|
90
|
+
export MCP_PROXY_WRITE_TO_FILE=true
|
|
91
|
+
export MCP_PROXY_OUTPUT_PATH=/tmp/grafana-results
|
|
92
|
+
|
|
93
|
+
# Grafana credentials (passed through to child)
|
|
94
|
+
export GRAFANA_URL=https://your-instance.grafana.net
|
|
95
|
+
export GRAFANA_SERVICE_ACCOUNT_TOKEN=glsa_your_token
|
|
96
|
+
|
|
97
|
+
# Run proxy
|
|
98
|
+
npx @anyshift/mcp-proxy
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Example 2: Wrap Anyshift MCP
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
# Proxy configuration
|
|
105
|
+
export MCP_PROXY_CHILD_COMMAND="npx"
|
|
106
|
+
export MCP_PROXY_CHILD_ARGS="-y,@anyshift/anyshift-mcp-server"
|
|
107
|
+
export MCP_PROXY_WRITE_TO_FILE=true
|
|
108
|
+
export MCP_PROXY_OUTPUT_PATH=/tmp/anyshift-results
|
|
109
|
+
|
|
110
|
+
# Anyshift credentials (passed through)
|
|
111
|
+
export API_TOKEN=your_token
|
|
112
|
+
export API_BASE_URL=https://api.anyshift.io
|
|
113
|
+
|
|
114
|
+
# Run proxy
|
|
115
|
+
npx @anyshift/mcp-proxy
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Example 3: Wrap Custom MCP
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
# Proxy configuration
|
|
122
|
+
export MCP_PROXY_CHILD_COMMAND="node"
|
|
123
|
+
export MCP_PROXY_CHILD_ARGS="/path/to/your/mcp-server.js"
|
|
124
|
+
export MCP_PROXY_MAX_TOKENS=5000
|
|
125
|
+
|
|
126
|
+
# Your MCP's credentials (passed through)
|
|
127
|
+
export YOUR_API_KEY=...
|
|
128
|
+
export YOUR_API_URL=...
|
|
129
|
+
|
|
130
|
+
# Run proxy
|
|
131
|
+
npx @anyshift/mcp-proxy
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## How It Works
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
┌─────────────┐
|
|
138
|
+
│ AI Agent │
|
|
139
|
+
└──────┬──────┘
|
|
140
|
+
│ MCP Protocol (stdio)
|
|
141
|
+
▼
|
|
142
|
+
┌─────────────────────────────────┐
|
|
143
|
+
│ @anyshift/mcp-proxy │
|
|
144
|
+
│ │
|
|
145
|
+
│ 1. Spawns child MCP │
|
|
146
|
+
│ with pass-through env vars │
|
|
147
|
+
│ │
|
|
148
|
+
│ 2. Discovers child tools │
|
|
149
|
+
│ + adds JQ tool │
|
|
150
|
+
│ │
|
|
151
|
+
│ 3. Forwards tool calls │
|
|
152
|
+
│ │
|
|
153
|
+
│ 4. Applies truncation │
|
|
154
|
+
│ if response > MAX_TOKENS │
|
|
155
|
+
│ │
|
|
156
|
+
│ 5. Writes to file │
|
|
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
|
+
└──────────────┘
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Integration Examples
|
|
171
|
+
|
|
172
|
+
### AI-Workbench Integration
|
|
173
|
+
|
|
174
|
+
```go
|
|
175
|
+
// In ai-workbench builder.go
|
|
176
|
+
func WrapWithProxy(baseMCPConfig MCPConfig, ...) MCPConfig {
|
|
177
|
+
proxyEnv := map[string]string{
|
|
178
|
+
"MCP_PROXY_CHILD_COMMAND": baseMCPConfig.Command,
|
|
179
|
+
"MCP_PROXY_MAX_TOKENS": "10000",
|
|
180
|
+
"MCP_PROXY_WRITE_TO_FILE": "true",
|
|
181
|
+
"MCP_PROXY_OUTPUT_PATH": outputPath,
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Merge child's env vars (pass-through)
|
|
185
|
+
for key, value := range baseMCPConfig.Env {
|
|
186
|
+
proxyEnv[key] = value
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return MCPConfig{
|
|
190
|
+
Command: "npx",
|
|
191
|
+
Args: []string{"-y", "@anyshift/mcp-proxy"},
|
|
192
|
+
Env: proxyEnv,
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Claude Desktop Integration
|
|
198
|
+
|
|
199
|
+
```json
|
|
200
|
+
{
|
|
201
|
+
"mcpServers": {
|
|
202
|
+
"grafana": {
|
|
203
|
+
"command": "npx",
|
|
204
|
+
"args": ["-y", "@anyshift/mcp-proxy"],
|
|
205
|
+
"env": {
|
|
206
|
+
"MCP_PROXY_CHILD_COMMAND": "mcp-grafana",
|
|
207
|
+
"MCP_PROXY_MAX_TOKENS": "10000",
|
|
208
|
+
"MCP_PROXY_WRITE_TO_FILE": "true",
|
|
209
|
+
"MCP_PROXY_OUTPUT_PATH": "/tmp/grafana-results",
|
|
210
|
+
"GRAFANA_URL": "https://your-instance.grafana.net",
|
|
211
|
+
"GRAFANA_SERVICE_ACCOUNT_TOKEN": "glsa_..."
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Environment Variable Reference
|
|
219
|
+
|
|
220
|
+
### MCP_PROXY_CHILD_COMMAND (REQUIRED)
|
|
221
|
+
|
|
222
|
+
The command to spawn as the child MCP server.
|
|
223
|
+
|
|
224
|
+
**Examples:**
|
|
225
|
+
- `"mcp-grafana"` - Direct binary
|
|
226
|
+
- `"npx"` - Use with `MCP_PROXY_CHILD_ARGS="-y,@anyshift/anyshift-mcp-server"`
|
|
227
|
+
- `"node"` - Use with `MCP_PROXY_CHILD_ARGS="/path/to/server.js"`
|
|
228
|
+
|
|
229
|
+
### MCP_PROXY_CHILD_ARGS (OPTIONAL)
|
|
230
|
+
|
|
231
|
+
Comma-separated arguments to pass to the child command.
|
|
232
|
+
|
|
233
|
+
**Example:** `"arg1,arg2,--verbose"` becomes `["arg1", "arg2", "--verbose"]`
|
|
234
|
+
|
|
235
|
+
### MCP_PROXY_MAX_TOKENS (OPTIONAL, default: 10000)
|
|
236
|
+
|
|
237
|
+
Maximum tokens before truncating responses.
|
|
238
|
+
|
|
239
|
+
**Calculation:** `maxTokens × charsPerToken = max characters`
|
|
240
|
+
**Default:** `10000 × 4 = 40,000 characters`
|
|
241
|
+
|
|
242
|
+
### MCP_PROXY_WRITE_TO_FILE (OPTIONAL, default: false)
|
|
243
|
+
|
|
244
|
+
Enable automatic file writing for responses above `MIN_CHARS_FOR_WRITE`.
|
|
245
|
+
|
|
246
|
+
**When to enable:**
|
|
247
|
+
- Responses frequently exceed token limits
|
|
248
|
+
- Need full data for later analysis
|
|
249
|
+
- Want to use JQ tool to query responses
|
|
250
|
+
|
|
251
|
+
### MCP_PROXY_OUTPUT_PATH (REQUIRED if WRITE_TO_FILE=true)
|
|
252
|
+
|
|
253
|
+
Directory where response files are saved.
|
|
254
|
+
|
|
255
|
+
**File naming:** `{timestamp}_{tool_name}_{short_id}.json`
|
|
256
|
+
|
|
257
|
+
### MCP_PROXY_ENABLE_JQ (OPTIONAL, default: true)
|
|
258
|
+
|
|
259
|
+
Add the `execute_jq_query` tool for querying saved JSON files.
|
|
260
|
+
|
|
261
|
+
**Disable when:**
|
|
262
|
+
- Child MCP already provides JQ tool
|
|
263
|
+
- Don't need file querying capability
|
|
264
|
+
|
|
265
|
+
### MCP_PROXY_ENABLE_LOGGING (OPTIONAL, default: false)
|
|
266
|
+
|
|
267
|
+
Enable debug logging to stderr.
|
|
268
|
+
|
|
269
|
+
**Logs include:**
|
|
270
|
+
- Configuration summary
|
|
271
|
+
- Tool discovery count
|
|
272
|
+
- Truncation events
|
|
273
|
+
- File writing operations
|
|
274
|
+
|
|
275
|
+
## Development
|
|
276
|
+
|
|
277
|
+
```bash
|
|
278
|
+
# Clone and install
|
|
279
|
+
cd mcp-proxy
|
|
280
|
+
pnpm install
|
|
281
|
+
|
|
282
|
+
# Build
|
|
283
|
+
pnpm build
|
|
284
|
+
|
|
285
|
+
# Test with Grafana
|
|
286
|
+
export MCP_PROXY_CHILD_COMMAND="mcp-grafana"
|
|
287
|
+
export GRAFANA_URL=...
|
|
288
|
+
export GRAFANA_SERVICE_ACCOUNT_TOKEN=...
|
|
289
|
+
node dist/index.js
|
|
290
|
+
|
|
291
|
+
# Test with custom MCP
|
|
292
|
+
export MCP_PROXY_CHILD_COMMAND="node"
|
|
293
|
+
export MCP_PROXY_CHILD_ARGS="/path/to/my-mcp.js"
|
|
294
|
+
export MY_API_KEY=...
|
|
295
|
+
node dist/index.js
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## Benefits
|
|
299
|
+
|
|
300
|
+
✅ **Zero configuration files** - Pure environment variables
|
|
301
|
+
✅ **Truly MCP-agnostic** - Works with ANY MCP server
|
|
302
|
+
✅ **Simple integration** - Just wrap the command with env vars
|
|
303
|
+
✅ **Clean separation** - Proxy config vs child config
|
|
304
|
+
✅ **Pass-through design** - Child gets exactly what it needs
|
|
305
|
+
|
|
306
|
+
## License
|
|
307
|
+
|
|
308
|
+
MIT
|
|
309
|
+
|
|
310
|
+
## Related Projects
|
|
311
|
+
|
|
312
|
+
- [@anyshift/mcp-tools-common](../mcp-tools-common) - Shared MCP utilities (truncation, file writing, JQ)
|
|
313
|
+
- [mcp-grafana](https://github.com/grafana/mcp-grafana) - Official Grafana MCP server
|
|
314
|
+
- [Model Context Protocol](https://modelcontextprotocol.io/) - MCP specification
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { FileWriterConfig, FileWriterResult } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Create a file writer instance with the given configuration
|
|
4
|
+
* @param config - File writer configuration
|
|
5
|
+
* @returns Object with handleResponse method
|
|
6
|
+
*/
|
|
7
|
+
export declare function createFileWriter(config: FileWriterConfig): {
|
|
8
|
+
/**
|
|
9
|
+
* Handle tool response - writes to file if conditions are met
|
|
10
|
+
* @param toolName - Name of the tool that generated the response
|
|
11
|
+
* @param args - Arguments passed to the tool
|
|
12
|
+
* @param responseData - The response data to potentially write to file
|
|
13
|
+
* @returns Either the original response or a file reference response
|
|
14
|
+
*/
|
|
15
|
+
handleResponse: (toolName: string, args: Record<string, unknown>, responseData: unknown) => Promise<FileWriterResult | unknown>;
|
|
16
|
+
};
|
|
17
|
+
export type { FileWriterConfig, FileWriterResult } from './types.js';
|
|
18
|
+
export { analyzeJsonSchema, extractNullableFields } from './schema.js';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { handleToolResponse } from './writer.js';
|
|
2
|
+
/**
|
|
3
|
+
* Create a file writer instance with the given configuration
|
|
4
|
+
* @param config - File writer configuration
|
|
5
|
+
* @returns Object with handleResponse method
|
|
6
|
+
*/
|
|
7
|
+
export function createFileWriter(config) {
|
|
8
|
+
return {
|
|
9
|
+
/**
|
|
10
|
+
* Handle tool response - writes to file if conditions are met
|
|
11
|
+
* @param toolName - Name of the tool that generated the response
|
|
12
|
+
* @param args - Arguments passed to the tool
|
|
13
|
+
* @param responseData - The response data to potentially write to file
|
|
14
|
+
* @returns Either the original response or a file reference response
|
|
15
|
+
*/
|
|
16
|
+
handleResponse: async (toolName, args, responseData) => {
|
|
17
|
+
return handleToolResponse(config, toolName, args, responseData);
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export { analyzeJsonSchema, extractNullableFields } from './schema.js';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { JsonSchema, NullableFields } from '../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Analyze JSON structure and generate enhanced schema
|
|
4
|
+
* @param obj - The object to analyze
|
|
5
|
+
* @param path - Current path in the object (for debugging)
|
|
6
|
+
* @returns Schema representation of the object
|
|
7
|
+
*/
|
|
8
|
+
export declare function analyzeJsonSchema(obj: unknown, path?: string): JsonSchema;
|
|
9
|
+
/**
|
|
10
|
+
* Extract nullable and always-null fields from schema
|
|
11
|
+
* @param schema - The schema to analyze
|
|
12
|
+
* @param basePath - Base path for field names
|
|
13
|
+
* @returns Object containing arrays of always-null and nullable field paths
|
|
14
|
+
*/
|
|
15
|
+
export declare function extractNullableFields(schema: unknown, basePath?: string): NullableFields;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analyze JSON structure and generate enhanced schema
|
|
3
|
+
* @param obj - The object to analyze
|
|
4
|
+
* @param path - Current path in the object (for debugging)
|
|
5
|
+
* @returns Schema representation of the object
|
|
6
|
+
*/
|
|
7
|
+
export function analyzeJsonSchema(obj, path = 'root') {
|
|
8
|
+
if (obj === null)
|
|
9
|
+
return { type: 'null' };
|
|
10
|
+
if (obj === undefined)
|
|
11
|
+
return { type: 'undefined' };
|
|
12
|
+
const type = Array.isArray(obj) ? 'array' : typeof obj;
|
|
13
|
+
if (type === 'object') {
|
|
14
|
+
const properties = {};
|
|
15
|
+
const objRecord = obj;
|
|
16
|
+
const keys = Object.keys(objRecord);
|
|
17
|
+
// Detect numeric string keys (common in Cypher results)
|
|
18
|
+
const numericKeys = keys.filter((k) => /^\d+$/.test(k));
|
|
19
|
+
const hasNumericKeys = keys.length > 0 && numericKeys.length >= keys.length * 0.8;
|
|
20
|
+
for (const key in objRecord) {
|
|
21
|
+
if (Object.prototype.hasOwnProperty.call(objRecord, key)) {
|
|
22
|
+
properties[key] = analyzeJsonSchema(objRecord[key], `${path}.${key}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const schema = { type: 'object', properties };
|
|
26
|
+
// Add metadata hints for numeric keys
|
|
27
|
+
if (hasNumericKeys) {
|
|
28
|
+
schema._keysAreNumeric = true;
|
|
29
|
+
schema._accessPattern = 'Use .["0"] not .[0]';
|
|
30
|
+
}
|
|
31
|
+
return schema;
|
|
32
|
+
}
|
|
33
|
+
else if (type === 'array') {
|
|
34
|
+
const arr = obj;
|
|
35
|
+
if (arr.length === 0) {
|
|
36
|
+
return { type: 'array', items: { type: 'unknown' }, length: 0 };
|
|
37
|
+
}
|
|
38
|
+
// Analyze array items for mixed types and nulls
|
|
39
|
+
const itemTypes = new Set();
|
|
40
|
+
let hasNulls = false;
|
|
41
|
+
// Sample first 10 items to detect type variance
|
|
42
|
+
const sampled = arr.slice(0, Math.min(10, arr.length));
|
|
43
|
+
for (const item of sampled) {
|
|
44
|
+
if (item === null) {
|
|
45
|
+
hasNulls = true;
|
|
46
|
+
itemTypes.add('null');
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
itemTypes.add(Array.isArray(item) ? 'array' : typeof item);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const schema = {
|
|
53
|
+
type: 'array',
|
|
54
|
+
items: itemTypes.size === 1 && !hasNulls
|
|
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;
|
|
62
|
+
}
|
|
63
|
+
return schema;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
return { type };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Extract nullable and always-null fields from schema
|
|
71
|
+
* @param schema - The schema to analyze
|
|
72
|
+
* @param basePath - Base path for field names
|
|
73
|
+
* @returns Object containing arrays of always-null and nullable field paths
|
|
74
|
+
*/
|
|
75
|
+
export function extractNullableFields(schema, basePath = '') {
|
|
76
|
+
const alwaysNull = [];
|
|
77
|
+
const nullable = [];
|
|
78
|
+
function traverse(s, path) {
|
|
79
|
+
if (!s || typeof s !== 'object')
|
|
80
|
+
return;
|
|
81
|
+
const schemaObj = s;
|
|
82
|
+
// Check if this field is always null
|
|
83
|
+
if (schemaObj.type === 'null') {
|
|
84
|
+
alwaysNull.push(path);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
// Check if this field can be null (mixed types)
|
|
88
|
+
if (schemaObj.items && typeof schemaObj.items === 'object') {
|
|
89
|
+
const items = schemaObj.items;
|
|
90
|
+
if (items.types &&
|
|
91
|
+
Array.isArray(items.types) &&
|
|
92
|
+
items.types.includes('null')) {
|
|
93
|
+
nullable.push(path);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Recurse into object properties
|
|
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);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// Recurse into array items
|
|
105
|
+
if (schemaObj.type === 'array' &&
|
|
106
|
+
schemaObj.items &&
|
|
107
|
+
typeof schemaObj.items === 'object') {
|
|
108
|
+
const items = schemaObj.items;
|
|
109
|
+
if (items.type === 'object' && items.properties) {
|
|
110
|
+
const props = items.properties;
|
|
111
|
+
for (const [key, value] of Object.entries(props)) {
|
|
112
|
+
const newPath = path ? `${path}[].${key}` : `[].${key}`;
|
|
113
|
+
traverse(value, newPath);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
traverse(schema, basePath);
|
|
119
|
+
return { alwaysNull, nullable };
|
|
120
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { FileWriterConfig, FileWriterResult } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Centralized response handler with file writing capability
|
|
4
|
+
* @param config - File writer configuration
|
|
5
|
+
* @param toolName - Name of the tool that generated the response
|
|
6
|
+
* @param args - Arguments passed to the tool
|
|
7
|
+
* @param responseData - The response data to potentially write to file
|
|
8
|
+
* @returns Either the original response or a file reference response
|
|
9
|
+
*/
|
|
10
|
+
export declare function handleToolResponse(config: FileWriterConfig, toolName: string, args: Record<string, unknown>, responseData: unknown): Promise<FileWriterResult | unknown>;
|