@anyshift/mcp-proxy 0.3.4 → 0.3.6

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/dist/index.js CHANGED
@@ -23,14 +23,15 @@ import { createJqTool } from './jq/index.js';
23
23
  import { truncateResponseIfNeeded } from './truncation/index.js';
24
24
  import { createFileWriter } from './fileWriter/index.js';
25
25
  import { generateToolId } from './utils/filename.js';
26
+ import { PROXY_PARAMS } from './types/index.js';
26
27
  // ============================================================================
27
28
  // HELPER FUNCTIONS
28
29
  // ============================================================================
29
30
  /**
30
- * Inject 'description' parameter into a tool's inputSchema
31
- * This ensures LLMs explain why they're calling each tool
31
+ * Inject proxy-specific parameters into a tool's inputSchema
32
+ * Uses centralized PROXY_PARAMS definitions
32
33
  */
33
- function injectDescriptionParam(tool) {
34
+ function injectProxyParams(tool) {
34
35
  // Clone the tool to avoid mutating the original
35
36
  const modifiedTool = { ...tool };
36
37
  if (!modifiedTool.inputSchema) {
@@ -43,15 +44,45 @@ function injectDescriptionParam(tool) {
43
44
  }
44
45
  // Clone properties
45
46
  modifiedTool.inputSchema.properties = { ...modifiedTool.inputSchema.properties };
46
- // Only add if not already present
47
- if (!modifiedTool.inputSchema.properties.description) {
48
- modifiedTool.inputSchema.properties.description = {
49
- type: 'string',
50
- description: 'Brief explanation of why you are calling this tool and what you expect to learn/achieve'
51
- };
47
+ // Inject all proxy params if not already present
48
+ for (const [key, value] of Object.entries(PROXY_PARAMS)) {
49
+ if (!modifiedTool.inputSchema.properties[key]) {
50
+ modifiedTool.inputSchema.properties[key] = value;
51
+ }
52
+ }
53
+ // Ensure isRetryAttempt is required (not optional)
54
+ if (!modifiedTool.inputSchema.required) {
55
+ modifiedTool.inputSchema.required = [];
56
+ }
57
+ if (!modifiedTool.inputSchema.required.includes('isRetryAttempt')) {
58
+ modifiedTool.inputSchema.required = [...modifiedTool.inputSchema.required, 'isRetryAttempt'];
52
59
  }
53
60
  return modifiedTool;
54
61
  }
62
+ /**
63
+ * Add retry metadata to a unified response if present in tool args
64
+ */
65
+ function addRetryMetadata(response, toolArgs) {
66
+ if (toolArgs.isRetryAttempt) {
67
+ response.isRetryAttempt = true;
68
+ if (toolArgs.originalToolId) {
69
+ response.originalToolId = toolArgs.originalToolId;
70
+ }
71
+ }
72
+ return response;
73
+ }
74
+ /**
75
+ * Create an error response with consistent structure
76
+ */
77
+ function createErrorResponse(tool_id, error, toolArgs) {
78
+ return addRetryMetadata({ tool_id, wroteToFile: false, error }, toolArgs);
79
+ }
80
+ /**
81
+ * Create a success response with content (not written to file)
82
+ */
83
+ function createContentResponse(tool_id, outputContent, toolArgs) {
84
+ return addRetryMetadata({ tool_id, wroteToFile: false, outputContent }, toolArgs);
85
+ }
55
86
  /**
56
87
  * ENVIRONMENT VARIABLE CONTRACT
57
88
  * =============================
@@ -299,8 +330,8 @@ async function main() {
299
330
  // ------------------------------------------------------------------------
300
331
  // 5. REGISTER ALL TOOLS (CHILD + PROXY) WITH DESCRIPTION INJECTION
301
332
  // ------------------------------------------------------------------------
302
- // Inject 'description' parameter into all child tools
303
- const enhancedChildTools = childToolsResponse.tools.map(injectDescriptionParam);
333
+ // Inject proxy parameters (description, isRetryAttempt, originalToolId) into all child tools
334
+ const enhancedChildTools = childToolsResponse.tools.map(injectProxyParams);
304
335
  const allTools = [
305
336
  ...enhancedChildTools,
306
337
  ...(jqTool ? [jqTool.toolDefinition] : []) // JQ tool already has description param
@@ -323,6 +354,9 @@ async function main() {
323
354
  if (toolArgs.description) {
324
355
  console.debug(`[mcp-proxy] Description: ${toolArgs.description}`);
325
356
  }
357
+ if (toolArgs.isRetryAttempt) {
358
+ console.debug(`[mcp-proxy] Retry of: ${toolArgs.originalToolId}`);
359
+ }
326
360
  }
327
361
  try {
328
362
  let result;
@@ -336,11 +370,7 @@ async function main() {
336
370
  });
337
371
  // JQ tool returns directly, wrap in unified format
338
372
  const tool_id = generateToolId(toolName, toolArgs, fileWriterConfig.toolAbbreviations);
339
- const unifiedResponse = {
340
- tool_id,
341
- wroteToFile: false,
342
- outputContent: result.content?.[0]?.text
343
- };
373
+ const unifiedResponse = createContentResponse(tool_id, result.content?.[0]?.text, toolArgs);
344
374
  return {
345
375
  content: [{
346
376
  type: 'text',
@@ -352,15 +382,10 @@ async function main() {
352
382
  // Forward all other tools to child MCP (if child exists)
353
383
  if (!childClient) {
354
384
  const tool_id = generateToolId(toolName, toolArgs, fileWriterConfig.toolAbbreviations);
355
- const errorResponse = {
356
- tool_id,
357
- wroteToFile: false,
358
- error: `Tool ${toolName} not available in standalone mode (no child MCP)`
359
- };
360
385
  return {
361
386
  content: [{
362
387
  type: 'text',
363
- text: JSON.stringify(errorResponse, null, 2)
388
+ text: JSON.stringify(createErrorResponse(tool_id, `Tool ${toolName} not available in standalone mode (no child MCP)`, toolArgs), null, 2)
364
389
  }],
365
390
  isError: true
366
391
  };
@@ -372,11 +397,27 @@ async function main() {
372
397
  name: toolName,
373
398
  arguments: toolArgs
374
399
  });
400
+ // Check if child MCP returned an error
401
+ const childReturnedError = !!result.isError;
375
402
  // Process result through file writer to get unified response
376
403
  if (result.content && Array.isArray(result.content) && result.content.length > 0) {
377
404
  const item = result.content[0];
378
405
  if (item.type === 'text' && typeof item.text === 'string') {
379
406
  const originalLength = item.text.length;
407
+ // If child returned error, pass through directly without file writing
408
+ if (childReturnedError) {
409
+ const tool_id = generateToolId(toolName, toolArgs, fileWriterConfig.toolAbbreviations);
410
+ if (ENABLE_LOGGING) {
411
+ console.debug(`[mcp-proxy] Child MCP returned error for ${toolName}: ${item.text.substring(0, 100)}...`);
412
+ }
413
+ return {
414
+ content: [{
415
+ type: 'text',
416
+ text: JSON.stringify(createErrorResponse(tool_id, item.text, toolArgs), null, 2)
417
+ }],
418
+ isError: true
419
+ };
420
+ }
380
421
  // Get unified response from file writer
381
422
  const unifiedResponse = await fileWriter.handleResponse(toolName, toolArgs, {
382
423
  content: [{ type: 'text', text: item.text }]
@@ -408,7 +449,8 @@ async function main() {
408
449
  }
409
450
  }
410
451
  }
411
- // Return unified response as JSON
452
+ // Add retry metadata and return unified response as JSON
453
+ addRetryMetadata(unifiedResponse, toolArgs);
412
454
  return {
413
455
  content: [{
414
456
  type: 'text',
@@ -420,30 +462,21 @@ async function main() {
420
462
  }
421
463
  // Fallback: return result with generated tool_id
422
464
  const tool_id = generateToolId(toolName, toolArgs, fileWriterConfig.toolAbbreviations);
423
- const fallbackResponse = {
424
- tool_id,
425
- wroteToFile: false,
426
- outputContent: result
427
- };
428
465
  return {
429
466
  content: [{
430
467
  type: 'text',
431
- text: JSON.stringify(fallbackResponse, null, 2)
432
- }]
468
+ text: JSON.stringify(createContentResponse(tool_id, result, toolArgs), null, 2)
469
+ }],
470
+ isError: childReturnedError
433
471
  };
434
472
  }
435
473
  catch (error) {
436
474
  console.error(`[mcp-proxy] Error executing tool ${toolName}:`, error);
437
475
  const tool_id = generateToolId(toolName, toolArgs, fileWriterConfig.toolAbbreviations);
438
- const errorResponse = {
439
- tool_id,
440
- wroteToFile: false,
441
- error: `Error executing ${toolName}: ${error.message || String(error)}`
442
- };
443
476
  return {
444
477
  content: [{
445
478
  type: 'text',
446
- text: JSON.stringify(errorResponse, null, 2)
479
+ text: JSON.stringify(createErrorResponse(tool_id, `Error executing ${toolName}: ${error.message || String(error)}`, toolArgs), null, 2)
447
480
  }],
448
481
  isError: true
449
482
  };
@@ -14,6 +14,18 @@ export declare function createJqTool(config: JqConfig): {
14
14
  inputSchema: {
15
15
  type: string;
16
16
  properties: {
17
+ description: {
18
+ readonly type: "string";
19
+ readonly description: "Brief explanation of why you are calling this tool and what you expect to learn/achieve";
20
+ };
21
+ isRetryAttempt: {
22
+ readonly type: "boolean";
23
+ readonly description: "Set to true if this call is a follow-up attempt after a previous failure. This includes: (1) exact retries for transient errors, (2) corrected attempts fixing typos or parameters based on error feedback. Any call attempting to achieve the same goal as a failed call is a retry.";
24
+ };
25
+ originalToolId: {
26
+ readonly type: "string";
27
+ readonly description: "The tool_id from the FIRST failed attempt in this retry chain. Always reference the original tool_id, not intermediate failures. Example: if tool_id_1 fails, tool_id_2 (retry) fails, tool_id_3 (retry) succeeds - both tool_id_2 and tool_id_3 should reference tool_id_1. Required when isRetryAttempt is true.";
28
+ };
17
29
  jq_query: {
18
30
  type: string;
19
31
  description: string;
@@ -22,10 +34,6 @@ export declare function createJqTool(config: JqConfig): {
22
34
  type: string;
23
35
  description: string;
24
36
  };
25
- description: {
26
- type: string;
27
- description: string;
28
- };
29
37
  };
30
38
  required: string[];
31
39
  };
package/dist/jq/tool.d.ts CHANGED
@@ -1,19 +1,19 @@
1
1
  import { z } from 'zod';
2
2
  /**
3
3
  * Zod schema for JQ query execution
4
+ * Note: Proxy params (description, isRetryAttempt, originalToolId) are handled
5
+ * by the proxy layer, not validated here. They're defined in JQ_TOOL_DEFINITION's
6
+ * inputSchema for LLM visibility.
4
7
  */
5
8
  export declare const ExecuteJqQuerySchema: z.ZodObject<{
6
9
  jq_query: z.ZodString;
7
10
  file_path: z.ZodString;
8
- description: z.ZodOptional<z.ZodString>;
9
11
  }, "strip", z.ZodTypeAny, {
10
12
  jq_query: string;
11
13
  file_path: string;
12
- description?: string | undefined;
13
14
  }, {
14
15
  jq_query: string;
15
16
  file_path: string;
16
- description?: string | undefined;
17
17
  }>;
18
18
  /**
19
19
  * Tool definition for JQ query execution with enhanced prompts
@@ -25,6 +25,18 @@ export declare const JQ_TOOL_DEFINITION: {
25
25
  inputSchema: {
26
26
  type: string;
27
27
  properties: {
28
+ description: {
29
+ readonly type: "string";
30
+ readonly description: "Brief explanation of why you are calling this tool and what you expect to learn/achieve";
31
+ };
32
+ isRetryAttempt: {
33
+ readonly type: "boolean";
34
+ readonly description: "Set to true if this call is a follow-up attempt after a previous failure. This includes: (1) exact retries for transient errors, (2) corrected attempts fixing typos or parameters based on error feedback. Any call attempting to achieve the same goal as a failed call is a retry.";
35
+ };
36
+ originalToolId: {
37
+ readonly type: "string";
38
+ readonly description: "The tool_id from the FIRST failed attempt in this retry chain. Always reference the original tool_id, not intermediate failures. Example: if tool_id_1 fails, tool_id_2 (retry) fails, tool_id_3 (retry) succeeds - both tool_id_2 and tool_id_3 should reference tool_id_1. Required when isRetryAttempt is true.";
39
+ };
28
40
  jq_query: {
29
41
  type: string;
30
42
  description: string;
@@ -33,10 +45,6 @@ export declare const JQ_TOOL_DEFINITION: {
33
45
  type: string;
34
46
  description: string;
35
47
  };
36
- description: {
37
- type: string;
38
- description: string;
39
- };
40
48
  };
41
49
  required: string[];
42
50
  };
package/dist/jq/tool.js CHANGED
@@ -1,6 +1,10 @@
1
1
  import { z } from 'zod';
2
+ import { PROXY_PARAMS } from '../types/index.js';
2
3
  /**
3
4
  * Zod schema for JQ query execution
5
+ * Note: Proxy params (description, isRetryAttempt, originalToolId) are handled
6
+ * by the proxy layer, not validated here. They're defined in JQ_TOOL_DEFINITION's
7
+ * inputSchema for LLM visibility.
4
8
  */
5
9
  export const ExecuteJqQuerySchema = z.object({
6
10
  jq_query: z
@@ -9,10 +13,6 @@ export const ExecuteJqQuerySchema = z.object({
9
13
  file_path: z
10
14
  .string()
11
15
  .describe('Absolute path starting with "/" pointing to the JSON or JSONL file to process. Must be a valid, existing file with .json or .jsonl extension. The file will be validated for existence and readability before processing.'),
12
- description: z
13
- .string()
14
- .optional()
15
- .describe('Brief explanation of why you are calling this tool and what you expect to learn/achieve'),
16
16
  });
17
17
  /**
18
18
  * Tool definition for JQ query execution with enhanced prompts
@@ -111,11 +111,8 @@ export const JQ_TOOL_DEFINITION = {
111
111
  type: 'string',
112
112
  description: 'Absolute path starting with "/" pointing to the JSON or JSONL file to process. Must be a valid, existing file with .json or .jsonl extension. The file will be validated for existence and readability before processing.',
113
113
  },
114
- description: {
115
- type: 'string',
116
- description: 'Brief explanation of why you are calling this tool and what you expect to learn/achieve',
117
- },
114
+ ...PROXY_PARAMS,
118
115
  },
119
- required: ['jq_query', 'file_path'],
116
+ required: ['jq_query', 'file_path', 'isRetryAttempt'],
120
117
  },
121
118
  };
@@ -1,3 +1,21 @@
1
+ /**
2
+ * Centralized proxy parameter definitions
3
+ * Used by both injectProxyParams and JQ tool definitions
4
+ */
5
+ export declare const PROXY_PARAMS: {
6
+ readonly description: {
7
+ readonly type: "string";
8
+ readonly description: "Brief explanation of why you are calling this tool and what you expect to learn/achieve";
9
+ };
10
+ readonly isRetryAttempt: {
11
+ readonly type: "boolean";
12
+ readonly description: "Set to true if this call is a follow-up attempt after a previous failure. This includes: (1) exact retries for transient errors, (2) corrected attempts fixing typos or parameters based on error feedback. Any call attempting to achieve the same goal as a failed call is a retry.";
13
+ };
14
+ readonly originalToolId: {
15
+ readonly type: "string";
16
+ readonly description: "The tool_id from the FIRST failed attempt in this retry chain. Always reference the original tool_id, not intermediate failures. Example: if tool_id_1 fails, tool_id_2 (retry) fails, tool_id_3 (retry) succeeds - both tool_id_2 and tool_id_3 should reference tool_id_1. Required when isRetryAttempt is true.";
17
+ };
18
+ };
1
19
  /**
2
20
  * Configuration for the file writer module
3
21
  */
@@ -35,27 +53,6 @@ export interface FileWriterResult {
35
53
  text: string;
36
54
  }>;
37
55
  }
38
- /**
39
- * Schema analysis result for a JSON structure
40
- */
41
- export interface JsonSchema {
42
- type: string;
43
- properties?: Record<string, unknown>;
44
- items?: unknown;
45
- length?: number;
46
- _hasNulls?: boolean;
47
- _keysAreNumeric?: boolean;
48
- _accessPattern?: string;
49
- }
50
- /**
51
- * Nullable fields extracted from schema
52
- */
53
- export interface NullableFields {
54
- /** Fields that are always null */
55
- alwaysNull: string[];
56
- /** Fields that can be null (mixed types) */
57
- nullable: string[];
58
- }
59
56
  /**
60
57
  * Unified response format for all tool calls
61
58
  * Provides a consistent structure for LLM consumption
@@ -73,4 +70,8 @@ export interface UnifiedToolResponse {
73
70
  outputContent?: unknown;
74
71
  /** Error message if the tool call failed */
75
72
  error?: string;
73
+ /** Whether this was a retry attempt */
74
+ isRetryAttempt?: boolean;
75
+ /** The original tool_id this call is retrying */
76
+ originalToolId?: string;
76
77
  }
@@ -1 +1,18 @@
1
- export {};
1
+ /**
2
+ * Centralized proxy parameter definitions
3
+ * Used by both injectProxyParams and JQ tool definitions
4
+ */
5
+ export const PROXY_PARAMS = {
6
+ description: {
7
+ type: 'string',
8
+ description: 'Brief explanation of why you are calling this tool and what you expect to learn/achieve'
9
+ },
10
+ isRetryAttempt: {
11
+ type: 'boolean',
12
+ description: 'Set to true if this call is a follow-up attempt after a previous failure. This includes: (1) exact retries for transient errors, (2) corrected attempts fixing typos or parameters based on error feedback. Any call attempting to achieve the same goal as a failed call is a retry.'
13
+ },
14
+ originalToolId: {
15
+ type: 'string',
16
+ description: 'The tool_id from the FIRST failed attempt in this retry chain. Always reference the original tool_id, not intermediate failures. Example: if tool_id_1 fails, tool_id_2 (retry) fails, tool_id_3 (retry) succeeds - both tool_id_2 and tool_id_3 should reference tool_id_1. Required when isRetryAttempt is true.'
17
+ }
18
+ };
@@ -6,11 +6,3 @@
6
6
  * @returns Tool ID like "1697834567123_met_qry_a3b4c5"
7
7
  */
8
8
  export declare const generateToolId: (toolName: string, args: Record<string, unknown>, toolAbbreviations?: Record<string, string>) => string;
9
- /**
10
- * Generate LLM-friendly compact filename
11
- * @param toolName - Name of the tool that generated the data
12
- * @param args - Arguments passed to the tool
13
- * @param toolAbbreviations - Optional custom abbreviations for tool names
14
- * @returns Compact filename like "1697834567123_met_qry_a3b4c5.json"
15
- */
16
- export declare const generateCompactFilename: (toolName: string, args: Record<string, unknown>, toolAbbreviations?: Record<string, string>) => string;
@@ -40,13 +40,3 @@ export const generateToolId = (toolName, args, toolAbbreviations) => {
40
40
  const argsHash = hashArgs(args);
41
41
  return `${timestamp}_${toolAbbrev}_${argsHash}`;
42
42
  };
43
- /**
44
- * Generate LLM-friendly compact filename
45
- * @param toolName - Name of the tool that generated the data
46
- * @param args - Arguments passed to the tool
47
- * @param toolAbbreviations - Optional custom abbreviations for tool names
48
- * @returns Compact filename like "1697834567123_met_qry_a3b4c5.json"
49
- */
50
- export const generateCompactFilename = (toolName, args, toolAbbreviations) => {
51
- return `${generateToolId(toolName, args, toolAbbreviations)}.json`;
52
- };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anyshift/mcp-proxy",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "description": "Generic MCP proxy that adds truncation, file writing, and JQ capabilities to any MCP server",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",