@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 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,4 @@
1
+ /**
2
+ * Re-export FileWriterConfig and related types from the main types module
3
+ */
4
+ export type { FileWriterConfig, FileWriterResult } from '../types/index.js';
@@ -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>;