@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.
@@ -0,0 +1,277 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { generateCompactFilename } from '../utils/filename.js';
4
+ import { analyzeJsonSchema, extractNullableFields } from './schema.js';
5
+ // Default minimum character count to trigger file writing
6
+ const DEFAULT_MIN_CHARS = 1000;
7
+ /**
8
+ * Helper function to detect if a response contains an error
9
+ */
10
+ const isErrorResponse = (response) => {
11
+ if (!response || typeof response !== 'object')
12
+ return false;
13
+ const obj = response;
14
+ // Check MCP-level error flags first
15
+ if (obj.error || obj.isError) {
16
+ return true;
17
+ }
18
+ // For MCP responses, extract and parse the raw text to check for API-level errors
19
+ const rawText = extractRawText(obj);
20
+ if (rawText) {
21
+ try {
22
+ // Handle cases where text might contain "Listed incidents: {...}" or "Queried metrics data: {...}"
23
+ let jsonText = rawText;
24
+ // Extract JSON from common response patterns
25
+ const jsonMatch = rawText.match(/:\s*(\{.*\}|\[.*\])$/s);
26
+ if (jsonMatch) {
27
+ jsonText = jsonMatch[1];
28
+ }
29
+ // Try to parse as JSON to check for status: "error"
30
+ const parsed = JSON.parse(jsonText);
31
+ return parsed.status === 'error';
32
+ }
33
+ catch {
34
+ // If JSON parsing fails, fall back to string matching
35
+ return (rawText.includes('"status":"error"') ||
36
+ rawText.includes('"status": "error"'));
37
+ }
38
+ }
39
+ // Fallback: check if the response object directly has status === 'error'
40
+ return obj.status === 'error';
41
+ };
42
+ /**
43
+ * Helper function to extract error message from API error responses
44
+ */
45
+ const extractErrorMessage = (response) => {
46
+ if (!response || typeof response !== 'object') {
47
+ return 'Unknown error occurred';
48
+ }
49
+ const obj = response;
50
+ // Check for direct error properties first
51
+ if (obj.error && typeof obj.error === 'string') {
52
+ return obj.error;
53
+ }
54
+ // For MCP responses, extract error from parsed content
55
+ const rawText = extractRawText(obj);
56
+ if (rawText) {
57
+ try {
58
+ // Handle cases where text might contain "Listed incidents: {...}" or "Queried metrics data: {...}"
59
+ let jsonText = rawText;
60
+ // Extract JSON from common response patterns
61
+ const jsonMatch = rawText.match(/:\s*(\{.*\}|\[.*\])$/s);
62
+ if (jsonMatch) {
63
+ jsonText = jsonMatch[1];
64
+ }
65
+ const parsed = JSON.parse(jsonText);
66
+ // Try different error message fields
67
+ if (parsed.error && typeof parsed.error === 'string') {
68
+ return parsed.error;
69
+ }
70
+ if (parsed.message && typeof parsed.message === 'string') {
71
+ return parsed.message;
72
+ }
73
+ if (parsed.status === 'error') {
74
+ return 'API request failed with error status';
75
+ }
76
+ }
77
+ catch {
78
+ // If JSON parsing fails, look for error patterns in the raw text
79
+ const errorMatch = rawText.match(/"error":\s*"([^"]+)"/);
80
+ if (errorMatch && errorMatch[1]) {
81
+ return errorMatch[1];
82
+ }
83
+ const messageMatch = rawText.match(/"message":\s*"([^"]+)"/);
84
+ if (messageMatch && messageMatch[1]) {
85
+ return messageMatch[1];
86
+ }
87
+ }
88
+ }
89
+ return 'API error occurred';
90
+ };
91
+ /**
92
+ * Extract raw text from MCP response format
93
+ */
94
+ const extractRawText = (response) => {
95
+ if (response._rawText) {
96
+ return response._rawText;
97
+ }
98
+ if (response.content && Array.isArray(response.content)) {
99
+ const textContent = response.content
100
+ .filter((item) => item.type === 'text')
101
+ .map((item) => item.text)
102
+ .join('\n');
103
+ return textContent || null;
104
+ }
105
+ return null;
106
+ };
107
+ /**
108
+ * Centralized response handler with file writing capability
109
+ * @param config - File writer configuration
110
+ * @param toolName - Name of the tool that generated the response
111
+ * @param args - Arguments passed to the tool
112
+ * @param responseData - The response data to potentially write to file
113
+ * @returns Either the original response or a file reference response
114
+ */
115
+ export async function handleToolResponse(config, toolName, args, responseData) {
116
+ // JQ query tool should always return directly to AI (never write to file)
117
+ if (toolName === 'execute_jq_query') {
118
+ return responseData;
119
+ }
120
+ // If there's an error, return proper MCP error response (never write errors to file)
121
+ if (isErrorResponse(responseData)) {
122
+ const errorMessage = extractErrorMessage(responseData);
123
+ return {
124
+ content: [
125
+ {
126
+ type: 'text',
127
+ text: `Error: ${errorMessage}`,
128
+ },
129
+ ],
130
+ isError: true,
131
+ };
132
+ }
133
+ // If file writing is disabled, just return the response
134
+ if (!config.enabled || !config.outputPath) {
135
+ return responseData;
136
+ }
137
+ // Check character count threshold - if response is too short, return directly
138
+ const rawText = extractRawText(responseData);
139
+ let contentLength = 0;
140
+ if (rawText) {
141
+ contentLength = rawText.length;
142
+ }
143
+ else if (responseData &&
144
+ typeof responseData === 'object' &&
145
+ 'content' in responseData &&
146
+ Array.isArray(responseData.content)) {
147
+ const textContent = responseData.content
148
+ .filter((item) => item.type === 'text')
149
+ .map((item) => item.text)
150
+ .join('\n');
151
+ contentLength = textContent.length;
152
+ }
153
+ else {
154
+ contentLength = JSON.stringify(responseData).length;
155
+ }
156
+ // If content is shorter than threshold, return response directly instead of writing to file
157
+ const minChars = config.minCharsForWrite ?? DEFAULT_MIN_CHARS;
158
+ if (contentLength < minChars) {
159
+ return responseData;
160
+ }
161
+ // Success case: write to file
162
+ try {
163
+ // Create compact, LLM-friendly filename
164
+ const filename = generateCompactFilename(toolName, args, config.toolAbbreviations);
165
+ const filepath = path.join(config.outputPath, filename);
166
+ // Ensure output directory exists
167
+ await fs.mkdir(config.outputPath, { recursive: true });
168
+ // Extract the actual data from MCP response format
169
+ let contentToWrite;
170
+ let parsedForSchema;
171
+ if (rawText) {
172
+ try {
173
+ // Try to parse the raw text as JSON for prettier formatting
174
+ let jsonText = rawText;
175
+ // Extract JSON from common response patterns
176
+ const jsonMatch = rawText.match(/:\s*(\{.*\}|\[.*\])$/s);
177
+ if (jsonMatch) {
178
+ jsonText = jsonMatch[1];
179
+ }
180
+ const parsed = JSON.parse(jsonText);
181
+ parsedForSchema = parsed;
182
+ // Remove pagination-related fields before writing
183
+ const { pagination, has_more, next_page, previous_page, page, page_size, total_pages, ...cleanData } = parsed;
184
+ contentToWrite = JSON.stringify(cleanData, null, 2);
185
+ }
186
+ catch {
187
+ // If parsing fails, write the raw text directly
188
+ contentToWrite = rawText;
189
+ }
190
+ }
191
+ else {
192
+ // Fallback: extract from display content or stringify the entire response
193
+ if (responseData &&
194
+ typeof responseData === 'object' &&
195
+ 'content' in responseData &&
196
+ Array.isArray(responseData.content)) {
197
+ const textContent = responseData.content
198
+ .filter((item) => item.type === 'text')
199
+ .map((item) => item.text)
200
+ .join('\n');
201
+ contentToWrite = textContent;
202
+ }
203
+ else {
204
+ contentToWrite = JSON.stringify(responseData, null, 2);
205
+ }
206
+ }
207
+ await fs.writeFile(filepath, contentToWrite);
208
+ // Try to generate schema if we have valid JSON
209
+ let schemaInfo = '';
210
+ let quickReference = '';
211
+ if (parsedForSchema) {
212
+ // Use the clean data (without pagination) for schema analysis
213
+ const { pagination, has_more, next_page, previous_page, page, page_size, total_pages, ...cleanData } = parsedForSchema;
214
+ const schema = analyzeJsonSchema(cleanData);
215
+ const nullFields = extractNullableFields(schema);
216
+ const schemaObj = schema;
217
+ // Build quick reference section
218
+ quickReference += `\n\n🔍 UNDERSTAND THIS SCHEMA BEFORE WRITING JQ QUERIES:\n`;
219
+ // Structure hints
220
+ if (schemaObj._keysAreNumeric) {
221
+ quickReference += ` • Structure: Object with numeric keys ("0", "1", ...) - use .["0"]\n`;
222
+ }
223
+ else if (schemaObj.type === 'array') {
224
+ quickReference += ` • Structure: Array with ${schemaObj.length} items\n`;
225
+ }
226
+ else if (schemaObj.type === 'object' && schemaObj.properties) {
227
+ const props = schemaObj.properties;
228
+ const keys = Object.keys(props).slice(0, 5).join(', ');
229
+ quickReference += ` • Structure: Object with keys: ${keys}\n`;
230
+ }
231
+ // Always null fields
232
+ if (nullFields.alwaysNull.length > 0) {
233
+ const fieldList = nullFields.alwaysNull.slice(0, 5).join(', ');
234
+ const more = nullFields.alwaysNull.length > 5
235
+ ? ` (+${nullFields.alwaysNull.length - 5} more)`
236
+ : '';
237
+ quickReference += ` • Always null: ${fieldList}${more}\n`;
238
+ }
239
+ // Nullable fields
240
+ if (nullFields.nullable.length > 0) {
241
+ const fieldList = nullFields.nullable.slice(0, 5).join(', ');
242
+ const more = nullFields.nullable.length > 5
243
+ ? ` (+${nullFields.nullable.length - 5} more)`
244
+ : '';
245
+ quickReference += ` • Sometimes null: ${fieldList}${more}\n`;
246
+ }
247
+ // Suggest exploratory queries
248
+ if (schemaObj._keysAreNumeric) {
249
+ quickReference += ` • Explore: keys, .["0"] | keys, .["0"]\n`;
250
+ }
251
+ else if (schemaObj.type === 'array' && schemaObj.length > 0) {
252
+ quickReference += ` • Explore: length, .[0] | keys, .[0]\n`;
253
+ }
254
+ else {
255
+ quickReference += ` • Explore: keys, type\n`;
256
+ }
257
+ // Full schema
258
+ schemaInfo = `\n\nFull JSON Schema:\n${JSON.stringify(schema, null, 2)}`;
259
+ }
260
+ // Count lines in the content
261
+ const lineCount = contentToWrite.split('\n').length;
262
+ // Return success message with file path, size, lines, quick reference, and schema
263
+ return {
264
+ content: [
265
+ {
266
+ type: 'text',
267
+ text: `📄 File: ${filepath}\nSize: ${contentToWrite.length} characters | Lines: ${lineCount}${quickReference}${schemaInfo}`,
268
+ },
269
+ ],
270
+ };
271
+ }
272
+ catch (error) {
273
+ // If file writing fails, return the original response
274
+ console.error(`[handleToolResponse] Error writing file:`, error);
275
+ return responseData;
276
+ }
277
+ }
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Generic MCP Proxy
4
+ *
5
+ * A universal wrapper for ANY Model Context Protocol (MCP) server that adds:
6
+ * 1. Response truncation (to stay within token limits)
7
+ * 2. Automatic file writing (for large responses)
8
+ * 3. JQ tool (for querying saved JSON files)
9
+ *
10
+ * This proxy is completely MCP-agnostic and works with any MCP server through
11
+ * a clean environment variable contract.
12
+ *
13
+ * MODES:
14
+ * - Wrapper Mode: When MCP_PROXY_CHILD_COMMAND is set, wraps another MCP server
15
+ * - Standalone Mode: When MCP_PROXY_CHILD_COMMAND is not set, provides only JQ tool
16
+ */
17
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,346 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Generic MCP Proxy
4
+ *
5
+ * A universal wrapper for ANY Model Context Protocol (MCP) server that adds:
6
+ * 1. Response truncation (to stay within token limits)
7
+ * 2. Automatic file writing (for large responses)
8
+ * 3. JQ tool (for querying saved JSON files)
9
+ *
10
+ * This proxy is completely MCP-agnostic and works with any MCP server through
11
+ * a clean environment variable contract.
12
+ *
13
+ * MODES:
14
+ * - Wrapper Mode: When MCP_PROXY_CHILD_COMMAND is set, wraps another MCP server
15
+ * - Standalone Mode: When MCP_PROXY_CHILD_COMMAND is not set, provides only JQ tool
16
+ */
17
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
18
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
19
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
20
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
21
+ import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
22
+ import { createJqTool } from './jq/index.js';
23
+ import { truncateResponseIfNeeded } from './truncation/index.js';
24
+ import { createFileWriter } from './fileWriter/index.js';
25
+ /**
26
+ * ENVIRONMENT VARIABLE CONTRACT
27
+ * =============================
28
+ *
29
+ * The proxy uses a namespace convention to separate its configuration from
30
+ * the child MCP server's configuration:
31
+ *
32
+ * MCP_PROXY_* variables = Proxy's own configuration
33
+ * Everything else = Passed through to child MCP server
34
+ *
35
+ * This design allows the proxy to wrap ANY MCP server without knowing
36
+ * anything about its specific requirements.
37
+ */
38
+ // ============================================================================
39
+ // PROXY CONFIGURATION (MCP_PROXY_* prefix)
40
+ // ============================================================================
41
+ /**
42
+ * MCP_PROXY_CHILD_COMMAND (OPTIONAL)
43
+ * The command to spawn as the child MCP server
44
+ * If not provided, proxy runs in standalone mode (JQ tool only)
45
+ * Examples: "mcp-grafana", "npx @anyshift/anyshift-mcp-server", "node /path/to/server.js"
46
+ */
47
+ const CHILD_COMMAND = process.env.MCP_PROXY_CHILD_COMMAND;
48
+ /**
49
+ * MCP_PROXY_CHILD_ARGS (OPTIONAL)
50
+ * Comma-separated list of arguments to pass to the child command
51
+ * Example: "arg1,arg2,arg3" becomes ["arg1", "arg2", "arg3"]
52
+ */
53
+ const CHILD_ARGS = process.env.MCP_PROXY_CHILD_ARGS
54
+ ? process.env.MCP_PROXY_CHILD_ARGS.split(',').map(s => s.trim()).filter(Boolean)
55
+ : [];
56
+ /**
57
+ * MCP_PROXY_MAX_TOKENS (OPTIONAL, default: 10000)
58
+ * Maximum tokens before truncating responses
59
+ * Calculated as: maxTokens * charsPerToken = max characters
60
+ * Default: 10000 tokens * 4 chars/token = 40,000 characters
61
+ */
62
+ const MAX_TOKENS = parseInt(process.env.MCP_PROXY_MAX_TOKENS || '10000');
63
+ /**
64
+ * MCP_PROXY_CHARS_PER_TOKEN (OPTIONAL, default: 4)
65
+ * Average characters per token for truncation calculation
66
+ * Standard approximation: 1 token ≈ 4 characters
67
+ */
68
+ const CHARS_PER_TOKEN = parseInt(process.env.MCP_PROXY_CHARS_PER_TOKEN || '4');
69
+ /**
70
+ * MCP_PROXY_WRITE_TO_FILE (OPTIONAL, default: false)
71
+ * Enable automatic file writing for large responses
72
+ * When enabled, responses above MIN_CHARS_FOR_WRITE are saved to disk
73
+ */
74
+ const WRITE_TO_FILE = process.env.MCP_PROXY_WRITE_TO_FILE === 'true';
75
+ /**
76
+ * MCP_PROXY_OUTPUT_PATH (REQUIRED if WRITE_TO_FILE=true)
77
+ * Directory where response files should be saved
78
+ * Must be an absolute path
79
+ */
80
+ const OUTPUT_PATH = process.env.MCP_PROXY_OUTPUT_PATH || '';
81
+ /**
82
+ * MCP_PROXY_MIN_CHARS_FOR_WRITE (OPTIONAL, default: 1000)
83
+ * Minimum response size (in characters) before saving to file
84
+ * Responses smaller than this are not written to disk
85
+ */
86
+ const MIN_CHARS_FOR_WRITE = parseInt(process.env.MCP_PROXY_MIN_CHARS_FOR_WRITE || '1000');
87
+ /**
88
+ * MCP_PROXY_ENABLE_JQ (OPTIONAL, default: true)
89
+ * Enable the JQ tool for querying JSON files
90
+ * The JQ tool is added to the list of available tools from the child MCP
91
+ */
92
+ const ENABLE_JQ = process.env.MCP_PROXY_ENABLE_JQ !== 'false'; // default true
93
+ /**
94
+ * MCP_PROXY_JQ_TIMEOUT_MS (OPTIONAL, default: 30000)
95
+ * Timeout in milliseconds for JQ query execution
96
+ */
97
+ const JQ_TIMEOUT_MS = parseInt(process.env.MCP_PROXY_JQ_TIMEOUT_MS || '30000');
98
+ /**
99
+ * MCP_PROXY_ENABLE_LOGGING (OPTIONAL, default: false)
100
+ * Enable debug logging for the proxy
101
+ * Logs truncation events and file writing operations
102
+ */
103
+ const ENABLE_LOGGING = process.env.MCP_PROXY_ENABLE_LOGGING === 'true';
104
+ // ============================================================================
105
+ // VALIDATION
106
+ // ============================================================================
107
+ // Validate configuration
108
+ // CHILD_COMMAND is now optional - if not provided, proxy runs in standalone mode (JQ only)
109
+ if (!CHILD_COMMAND) {
110
+ console.error('[mcp-proxy] No child command specified - running in standalone mode (JQ only)');
111
+ if (!ENABLE_JQ) {
112
+ console.error('ERROR: Standalone mode requires JQ to be enabled (MCP_PROXY_ENABLE_JQ must not be false)');
113
+ process.exit(1);
114
+ }
115
+ }
116
+ if (WRITE_TO_FILE && !OUTPUT_PATH) {
117
+ console.error('ERROR: MCP_PROXY_OUTPUT_PATH is required when MCP_PROXY_WRITE_TO_FILE=true');
118
+ console.error('Example: export MCP_PROXY_OUTPUT_PATH="/tmp/mcp-results"');
119
+ process.exit(1);
120
+ }
121
+ // ============================================================================
122
+ // PASS-THROUGH ENVIRONMENT VARIABLES
123
+ // ============================================================================
124
+ /**
125
+ * Extract all environment variables that should be passed to the child MCP.
126
+ *
127
+ * We pass through everything EXCEPT:
128
+ * - Variables prefixed with MCP_PROXY_ (those are for the proxy itself)
129
+ * - System variables that shouldn't be inherited (PATH, HOME, etc. are handled by spawn)
130
+ *
131
+ * This allows the child MCP to receive exactly the environment variables it needs,
132
+ * such as:
133
+ * - GRAFANA_URL, GRAFANA_SERVICE_ACCOUNT_TOKEN
134
+ * - DATADOG_API_KEY, DATADOG_APP_KEY
135
+ * - API_TOKEN, API_BASE_URL
136
+ * - etc.
137
+ */
138
+ const childEnv = {};
139
+ for (const [key, value] of Object.entries(process.env)) {
140
+ // Skip proxy configuration variables
141
+ if (key.startsWith('MCP_PROXY_')) {
142
+ continue;
143
+ }
144
+ // Only include defined values
145
+ if (value !== undefined) {
146
+ childEnv[key] = value;
147
+ }
148
+ }
149
+ // ============================================================================
150
+ // CONFIGURATION SUMMARY
151
+ // ============================================================================
152
+ if (ENABLE_LOGGING) {
153
+ console.error('[mcp-proxy] Configuration:');
154
+ console.error(` Mode: ${CHILD_COMMAND ? 'wrapper' : 'standalone (JQ only)'}`);
155
+ if (CHILD_COMMAND) {
156
+ console.error(` Child command: ${CHILD_COMMAND}`);
157
+ console.error(` Child args: [${CHILD_ARGS.join(', ')}]`);
158
+ }
159
+ console.error(` Max tokens: ${MAX_TOKENS}`);
160
+ console.error(` Chars per token: ${CHARS_PER_TOKEN}`);
161
+ console.error(` Write to file: ${WRITE_TO_FILE}`);
162
+ if (WRITE_TO_FILE) {
163
+ console.error(` Output path: ${OUTPUT_PATH}`);
164
+ console.error(` Min chars for write: ${MIN_CHARS_FOR_WRITE}`);
165
+ }
166
+ console.error(` JQ tool enabled: ${ENABLE_JQ}`);
167
+ if (CHILD_COMMAND) {
168
+ console.error(` Pass-through env vars: ${Object.keys(childEnv).length}`);
169
+ }
170
+ }
171
+ // ============================================================================
172
+ // MAIN PROXY LOGIC
173
+ // ============================================================================
174
+ async function main() {
175
+ console.error('[mcp-proxy] Starting generic MCP proxy...');
176
+ // ------------------------------------------------------------------------
177
+ // 1. SPAWN CHILD MCP SERVER (if configured)
178
+ // ------------------------------------------------------------------------
179
+ let childClient = null;
180
+ let childToolsResponse = { tools: [] };
181
+ if (CHILD_COMMAND) {
182
+ console.error(`[mcp-proxy] Spawning child MCP: ${CHILD_COMMAND}`);
183
+ const childTransport = new StdioClientTransport({
184
+ command: CHILD_COMMAND,
185
+ args: CHILD_ARGS,
186
+ env: childEnv // All values are defined strings (filtered in extraction loop)
187
+ });
188
+ childClient = new Client({
189
+ name: 'mcp-proxy-client',
190
+ version: '1.0.0'
191
+ }, {
192
+ capabilities: {}
193
+ });
194
+ await childClient.connect(childTransport);
195
+ console.error('[mcp-proxy] Connected to child MCP');
196
+ // ------------------------------------------------------------------------
197
+ // 2. DISCOVER TOOLS FROM CHILD MCP
198
+ // ------------------------------------------------------------------------
199
+ childToolsResponse = await childClient.listTools();
200
+ console.error(`[mcp-proxy] Discovered ${childToolsResponse.tools.length} tools from child MCP`);
201
+ }
202
+ else {
203
+ console.error('[mcp-proxy] Standalone mode - no child MCP');
204
+ }
205
+ // ------------------------------------------------------------------------
206
+ // 3. CREATE PROXY SERVER
207
+ // ------------------------------------------------------------------------
208
+ const server = new Server({
209
+ name: 'mcp-proxy',
210
+ version: '1.0.0'
211
+ }, {
212
+ capabilities: {
213
+ tools: {}
214
+ }
215
+ });
216
+ // ------------------------------------------------------------------------
217
+ // 4. INITIALIZE PROXY CAPABILITIES
218
+ // ------------------------------------------------------------------------
219
+ // Truncation configuration
220
+ const truncationConfig = {
221
+ maxTokens: MAX_TOKENS,
222
+ charsPerToken: CHARS_PER_TOKEN,
223
+ enableLogging: ENABLE_LOGGING,
224
+ messagePrefix: 'RESPONSE TRUNCATED' // Can be customized per service
225
+ };
226
+ // File writer configuration
227
+ const fileWriterConfig = {
228
+ enabled: WRITE_TO_FILE,
229
+ outputPath: OUTPUT_PATH,
230
+ minCharsForWrite: MIN_CHARS_FOR_WRITE,
231
+ toolAbbreviations: {} // No service-specific abbreviations (generic proxy)
232
+ };
233
+ const fileWriter = createFileWriter(fileWriterConfig);
234
+ // JQ tool configuration
235
+ let jqTool = null;
236
+ if (ENABLE_JQ) {
237
+ jqTool = createJqTool({
238
+ allowedPaths: [process.cwd(), OUTPUT_PATH].filter(Boolean),
239
+ timeoutMs: JQ_TIMEOUT_MS
240
+ });
241
+ }
242
+ // ------------------------------------------------------------------------
243
+ // 5. REGISTER ALL TOOLS (CHILD + PROXY)
244
+ // ------------------------------------------------------------------------
245
+ const allTools = [
246
+ ...childToolsResponse.tools,
247
+ ...(jqTool ? [jqTool.toolDefinition] : [])
248
+ ];
249
+ console.error(`[mcp-proxy] Exposing ${allTools.length} tools total (${childToolsResponse.tools.length} from child${jqTool ? ' + 1 JQ' : ''})`);
250
+ // ------------------------------------------------------------------------
251
+ // 6. HANDLE TOOL LIST REQUESTS
252
+ // ------------------------------------------------------------------------
253
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
254
+ return { tools: allTools };
255
+ });
256
+ // ------------------------------------------------------------------------
257
+ // 7. HANDLE TOOL CALL REQUESTS (WITH TRUNCATION & FILE WRITING)
258
+ // ------------------------------------------------------------------------
259
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
260
+ const toolName = request.params.name;
261
+ const toolArgs = request.params.arguments || {};
262
+ if (ENABLE_LOGGING) {
263
+ console.error(`[mcp-proxy] Tool call: ${toolName}`);
264
+ }
265
+ try {
266
+ // Handle JQ tool locally (if enabled)
267
+ if (toolName === 'execute_jq_query' && jqTool) {
268
+ if (ENABLE_LOGGING) {
269
+ console.error('[mcp-proxy] Executing JQ tool locally');
270
+ }
271
+ return await jqTool.handler({
272
+ params: { arguments: toolArgs }
273
+ });
274
+ }
275
+ // Forward all other tools to child MCP (if child exists)
276
+ if (!childClient) {
277
+ return {
278
+ content: [{
279
+ type: 'text',
280
+ text: `Error: Tool ${toolName} not available in standalone mode (no child MCP)`
281
+ }],
282
+ isError: true
283
+ };
284
+ }
285
+ if (ENABLE_LOGGING) {
286
+ console.error(`[mcp-proxy] Forwarding to child MCP: ${toolName}`);
287
+ }
288
+ const result = await childClient.callTool({
289
+ name: toolName,
290
+ arguments: toolArgs
291
+ });
292
+ // Apply truncation and file writing to text responses
293
+ if (result.content && Array.isArray(result.content) && result.content.length > 0) {
294
+ for (let i = 0; i < result.content.length; i++) {
295
+ const item = result.content[i];
296
+ if (item.type === 'text' && typeof item.text === 'string') {
297
+ const originalLength = item.text.length;
298
+ // Step 1: Apply truncation
299
+ item.text = truncateResponseIfNeeded(truncationConfig, item.text);
300
+ if (item.text.length < originalLength && ENABLE_LOGGING) {
301
+ console.error(`[mcp-proxy] Truncated response: ${originalLength} → ${item.text.length} chars`);
302
+ }
303
+ // Step 2: Apply file writing (if enabled)
304
+ if (fileWriterConfig.enabled) {
305
+ const fileResult = await fileWriter.handleResponse(toolName, toolArgs, {
306
+ content: [{ type: 'text', text: item.text }]
307
+ });
308
+ if (fileResult && fileResult.content && Array.isArray(fileResult.content) &&
309
+ fileResult.content.length > 0 && fileResult.content[0].type === 'text') {
310
+ item.text = fileResult.content[0].text;
311
+ if (ENABLE_LOGGING) {
312
+ console.error(`[mcp-proxy] File writing applied for ${toolName}`);
313
+ }
314
+ }
315
+ }
316
+ }
317
+ }
318
+ }
319
+ return result;
320
+ }
321
+ catch (error) {
322
+ console.error(`[mcp-proxy] Error executing tool ${toolName}:`, error);
323
+ return {
324
+ content: [{
325
+ type: 'text',
326
+ text: `Error executing ${toolName}: ${error.message || String(error)}`
327
+ }],
328
+ isError: true
329
+ };
330
+ }
331
+ });
332
+ // ------------------------------------------------------------------------
333
+ // 8. CONNECT PROXY TO STDIO
334
+ // ------------------------------------------------------------------------
335
+ const transport = new StdioServerTransport();
336
+ await server.connect(transport);
337
+ console.error('[mcp-proxy] Proxy server ready on stdio');
338
+ console.error('[mcp-proxy] Waiting for MCP protocol messages...');
339
+ }
340
+ // ============================================================================
341
+ // START THE PROXY
342
+ // ============================================================================
343
+ main().catch((error) => {
344
+ console.error('[mcp-proxy] Fatal error:', error);
345
+ process.exit(1);
346
+ });
@@ -0,0 +1,14 @@
1
+ import { JqConfig } from './types.js';
2
+ /**
3
+ * Execute a JQ query on a JSON file
4
+ * @param config - JQ configuration
5
+ * @param jqQuery - The JQ query to execute
6
+ * @param filePath - Absolute path to the JSON file
7
+ * @returns Promise with the query result
8
+ */
9
+ export declare function executeJqQuery(config: JqConfig, jqQuery: string, filePath: string): Promise<{
10
+ content: Array<{
11
+ type: 'text';
12
+ text: string;
13
+ }>;
14
+ }>;