ziya 0.3.0__py3-none-any.whl → 0.3.2__py3-none-any.whl

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.

Potentially problematic release.


This version of ziya might be problematic. Click here for more details.

Files changed (73) hide show
  1. app/agents/agent.py +71 -73
  2. app/agents/direct_streaming.py +1 -1
  3. app/agents/prompts.py +1 -1
  4. app/agents/prompts_manager.py +14 -10
  5. app/agents/wrappers/google_direct.py +31 -1
  6. app/agents/wrappers/nova_tool_execution.py +2 -2
  7. app/agents/wrappers/nova_wrapper.py +1 -1
  8. app/agents/wrappers/ziya_bedrock.py +53 -31
  9. app/config/models_config.py +61 -20
  10. app/config/shell_config.py +5 -1
  11. app/extensions/prompt_extensions/claude_extensions.py +27 -5
  12. app/extensions/prompt_extensions/mcp_prompt_extensions.py +82 -56
  13. app/main.py +5 -3
  14. app/mcp/client.py +19 -10
  15. app/mcp/manager.py +68 -10
  16. app/mcp/tools.py +8 -9
  17. app/mcp_servers/shell_server.py +3 -3
  18. app/middleware/streaming.py +29 -41
  19. app/routes/file_validation.py +35 -0
  20. app/routes/mcp_routes.py +54 -8
  21. app/server.py +525 -614
  22. app/streaming_tool_executor.py +748 -137
  23. app/templates/asset-manifest.json +20 -20
  24. app/templates/index.html +1 -1
  25. app/templates/static/css/{main.0297bfee.css → main.e7109b49.css} +2 -2
  26. app/templates/static/css/main.e7109b49.css.map +1 -0
  27. app/templates/static/js/14386.65fcfe53.chunk.js +2 -0
  28. app/templates/static/js/14386.65fcfe53.chunk.js.map +1 -0
  29. app/templates/static/js/35589.0368973a.chunk.js +2 -0
  30. app/templates/static/js/35589.0368973a.chunk.js.map +1 -0
  31. app/templates/static/js/{50295.ab92f61b.chunk.js → 50295.90aca393.chunk.js} +3 -3
  32. app/templates/static/js/50295.90aca393.chunk.js.map +1 -0
  33. app/templates/static/js/55734.5f0fd567.chunk.js +2 -0
  34. app/templates/static/js/55734.5f0fd567.chunk.js.map +1 -0
  35. app/templates/static/js/58542.57fed736.chunk.js +2 -0
  36. app/templates/static/js/58542.57fed736.chunk.js.map +1 -0
  37. app/templates/static/js/{68418.2554bb1e.chunk.js → 68418.f7b4d2d9.chunk.js} +3 -3
  38. app/templates/static/js/68418.f7b4d2d9.chunk.js.map +1 -0
  39. app/templates/static/js/99948.b280eda0.chunk.js +2 -0
  40. app/templates/static/js/99948.b280eda0.chunk.js.map +1 -0
  41. app/templates/static/js/main.e075582c.js +3 -0
  42. app/templates/static/js/main.e075582c.js.map +1 -0
  43. app/utils/code_util.py +5 -2
  44. app/utils/context_cache.py +11 -0
  45. app/utils/conversation_filter.py +90 -0
  46. app/utils/custom_bedrock.py +43 -1
  47. app/utils/diff_utils/validation/validators.py +32 -22
  48. app/utils/file_cache.py +5 -3
  49. app/utils/precision_prompt_system.py +116 -0
  50. app/utils/streaming_optimizer.py +100 -0
  51. {ziya-0.3.0.dist-info → ziya-0.3.2.dist-info}/METADATA +3 -2
  52. {ziya-0.3.0.dist-info → ziya-0.3.2.dist-info}/RECORD +59 -55
  53. app/templates/static/css/main.0297bfee.css.map +0 -1
  54. app/templates/static/js/14386.567bf803.chunk.js +0 -2
  55. app/templates/static/js/14386.567bf803.chunk.js.map +0 -1
  56. app/templates/static/js/35589.278ecda2.chunk.js +0 -2
  57. app/templates/static/js/35589.278ecda2.chunk.js.map +0 -1
  58. app/templates/static/js/50295.ab92f61b.chunk.js.map +0 -1
  59. app/templates/static/js/55734.90d8bd52.chunk.js +0 -2
  60. app/templates/static/js/55734.90d8bd52.chunk.js.map +0 -1
  61. app/templates/static/js/58542.08fb5cf4.chunk.js +0 -2
  62. app/templates/static/js/58542.08fb5cf4.chunk.js.map +0 -1
  63. app/templates/static/js/68418.2554bb1e.chunk.js.map +0 -1
  64. app/templates/static/js/99948.71670e91.chunk.js +0 -2
  65. app/templates/static/js/99948.71670e91.chunk.js.map +0 -1
  66. app/templates/static/js/main.1d79eac2.js +0 -3
  67. app/templates/static/js/main.1d79eac2.js.map +0 -1
  68. /app/templates/static/js/{50295.ab92f61b.chunk.js.LICENSE.txt → 50295.90aca393.chunk.js.LICENSE.txt} +0 -0
  69. /app/templates/static/js/{68418.2554bb1e.chunk.js.LICENSE.txt → 68418.f7b4d2d9.chunk.js.LICENSE.txt} +0 -0
  70. /app/templates/static/js/{main.1d79eac2.js.LICENSE.txt → main.e075582c.js.LICENSE.txt} +0 -0
  71. {ziya-0.3.0.dist-info → ziya-0.3.2.dist-info}/WHEEL +0 -0
  72. {ziya-0.3.0.dist-info → ziya-0.3.2.dist-info}/entry_points.txt +0 -0
  73. {ziya-0.3.0.dist-info → ziya-0.3.2.dist-info}/licenses/LICENSE +0 -0
@@ -39,8 +39,30 @@ CLAUDE FAMILY INSTRUCTIONS:
39
39
  4. Use XML tags for structured outputs when appropriate
40
40
  5. Your job is not to proclaim the greatness of the user or the success of your efforts. You are being engaged, at each exchange, to solve a problem, not to congratulate yourself or the user. Look for the problem not the success.
41
41
 
42
+ TOOL USAGE PRIORITIZATION:
43
+ 1. **Answer from available context first** - If information is available in the provided codebase, files, or conversation context, use that directly
44
+ 2. **Avoid redundant file access** - If file contents or directory structures are already included in the context, DO NOT use tools like `cat`, `ls`, or `find` to re-examine the same files or directories
45
+ 3. **Use reasoning and analysis** - Apply your knowledge and analytical capabilities before reaching for tools
46
+ 4. **Use tools for computational analysis** - DO use tools like `grep`, `sort`, `uniq`, `wc`, `sed`, etc. on provided context when you need discrete numerical values, counts, or precise pattern matching that requires computational accuracy
47
+ 5. **Tools are secondary for discovery** - Only use discovery tools when:
48
+ - Information cannot be determined from available context
49
+ - You need to perform an action (like running code, checking files, etc.)
50
+ - The user explicitly requests tool usage
51
+ - You need to check for changes since the context was captured
52
+ 6. **Don't duplicate context unnecessarily** - Avoid using tools to re-fetch information you already have
53
+
54
+ CONTEXT UTILIZATION:
55
+ When file contents, directory listings, or code structures are already provided in your context:
56
+ - Analyze that information directly rather than using tools to re-examine the same files or directories
57
+ - BUT use computational tools (grep, sort, uniq, wc, sed, etc.) when you need precise counts, numerical analysis, or pattern matching that requires computational accuracy
58
+ - The goal is to avoid redundant file access while still leveraging tools for their computational strengths
59
+
42
60
  TOOL EXECUTION AND CONTINUATION:
43
- When you need to use a tool:
61
+
62
+ INTERNAL CONTEXT CHECK:
63
+ Before using any tools, silently assess: "Do I already have the information needed in my provided context?" Only proceed with tools if the answer is clearly "no."
64
+
65
+ When you have determined that a tool is necessary:
44
66
  1. Introduce what you're about to do
45
67
  2. Execute the tool call
46
68
  3. **STOP IMMEDIATELY after </TOOL_SENTINEL>** - DO NOT CONTINUE YOUR RESPONSE
@@ -48,10 +70,10 @@ When you need to use a tool:
48
70
  5. **DO NOT** guess what the tool output will be
49
71
  6. **DO NOT** write "Based on the result..." or similar text
50
72
  7. **WAIT** for the actual tool result to be provided
51
-
52
- After I provide the tool result, I will ask you to continue. Only then should you resume your response.
53
-
54
- Example: "I'll check the current directory." [TOOL CALL] [STOP AND WAIT] [TOOL RESULT PROVIDED] "Based on the result, the current directory is..."
73
+
74
+ CRITICAL: Use ONLY native tool calling. Never generate fake tool calling syntax like ```tool:mcp_run_shell_command. Use the provided tools directly. Regular markdown code blocks like ```bash for examples are perfectly fine.
75
+
76
+ If the provided context doesn't fully answer the user's request, use tools to gather the missing information. However, if file contents or directory structures are already shown in the context, work with that information directly instead of re-examining files. When you find relevant files through exploration, examine their contents. Check that all the required parameters for each tool call are provided or can reasonably be inferred from context. IF there are no relevant tools or there are missing values for required parameters, ask the user to supply these values; otherwise proceed with the tool calls. If the user provides a specific value for a parameter (for example provided in quotes), make sure to use that value EXACTLY. DO NOT make up values for or ask about optional parameters. Carefully analyze descriptive terms in the request as they may indicate required parameter values that should be included even if not explicitly quoted.
55
77
  """
56
78
 
57
79
  # Find a good place to insert the instructions
@@ -61,16 +61,28 @@ def mcp_usage_guidelines(prompt: str, context: dict) -> str:
61
61
  logger.info("MCP_GUIDELINES: Skipping for gemini-2.5-pro due to prompt size limits")
62
62
  return prompt
63
63
 
64
+ # Check if MCP is enabled
65
+ import os
66
+ if not os.environ.get("ZIYA_ENABLE_MCP", "true").lower() in ("true", "1", "yes"):
67
+ logger.info("MCP_GUIDELINES: MCP is disabled, returning original prompt")
68
+ return prompt
69
+
64
70
  # Check if MCP tools are available in the context
65
71
  # This would be passed from the agent system when MCP is initialized
66
- mcp_tools_available = context.get("mcp_tools_available", False)
67
- from app.mcp.tools import create_mcp_tools
68
- all_mcp_tools = create_mcp_tools()
69
- available_tools = [tool.name for tool in all_mcp_tools]
72
+ # Get server-specific tools only (exclude MCPResourceTool which is always present)
73
+ try:
74
+ from app.mcp.manager import get_mcp_manager
75
+ mcp_manager = get_mcp_manager()
76
+ server_tools = mcp_manager.get_all_tools() if mcp_manager.is_initialized else []
77
+ available_tools = [f"mcp_{tool.name}" if not tool.name.startswith("mcp_") else tool.name for tool in server_tools]
78
+ except Exception as e:
79
+ logger.warning(f"Could not get MCP server tools: {e}")
80
+ available_tools = []
70
81
 
71
82
  if not available_tools:
72
83
  logger.info("MCP_GUIDELINES: No MCP tools available or list is empty, returning original prompt.")
73
84
  return prompt
85
+
74
86
 
75
87
  # For Google models, native function calling is used. Do not add XML tool instructions.
76
88
  if is_google_endpoint:
@@ -87,6 +99,19 @@ def mcp_usage_guidelines(prompt: str, context: dict) -> str:
87
99
  # For other models (Bedrock, etc.), provide XML-based tool instructions
88
100
  mcp_guidelines = """
89
101
 
102
+ 🚨 CRITICAL FILE MODIFICATION PROHIBITION 🚨
103
+ ═══════════════════════════════════════════════
104
+ NEVER use tools to:
105
+ - Copy files (cp, backup, etc.)
106
+ - Modify files directly (sed, awk, etc.)
107
+ - Create new files
108
+ - Move or rename files
109
+ - Change file permissions
110
+
111
+ ONLY suggest changes through Git diff patches in your response text.
112
+ If you catch yourself about to modify a file with a tool - STOP and provide a diff instead.
113
+ ═══════════════════════════════════════════════
114
+
90
115
  ## MCP Tool Usage - CRITICAL INSTRUCTIONS
91
116
  **EXECUTE TOOLS WHEN REQUESTED - Never simulate or describe what you would do.**
92
117
 
@@ -96,13 +121,33 @@ def mcp_usage_guidelines(prompt: str, context: dict) -> str:
96
121
  """ + _get_tool_call_formats_from_mcp(available_tools) + """
97
122
 
98
123
  **Usage Rules:**
99
- 0. **Prefer local context and AST over tools when either can provide similar information**
100
- 1. **Always use actual tool results** - Never fabricate output
101
- 2. **Shell commands**: Use read-only commands (ls, cat, grep) when possible; format output as terminal session
102
- 3. **Time queries**: Always use tool rather than guessing current time
103
- 4. **Error handling**: Show actual errors and try alternatives
104
- 5. **Verification**: Use tools to verify system state rather than making assumptions
105
- 6. **No Empty Calls**: Do not generate empty or incomplete tool calls. Only output a tool call block if you have a valid command to execute.
124
+ 0. **Answer from context first** - Only use tools when you need information not available in the provided context
125
+ 1. **Prefer local context and AST over tools** when either can provide similar information
126
+ 2. **When using tools, use actual results** - Never fabricate output
127
+
128
+ ⚠️ BEFORE EVERY TOOL CALL ASK YOURSELF: ⚠️
129
+ "Do I need information not in the context? Am I about to modify a file? If modifying files, I must provide a Git diff patch instead!"
130
+ 3. **Shell commands**: Use read-only commands (ls, cat, grep) when possible; format output as terminal session
131
+ 4. **Time queries**: Use tool only when current time is actually needed
132
+ 5. **Error handling**: Show actual errors and try alternatives
133
+ 6. **Verification**: Use tools to verify system state only when assumptions aren't sufficient
134
+ 7. **No Empty Calls**: Do not generate empty or incomplete tool calls. Only output a tool call block if you have a valid command to execute.
135
+ """
136
+
137
+ # Add shell-specific warning if shell command tool is available
138
+ if any("shell" in tool.lower() or "run_shell_command" in tool for tool in available_tools):
139
+ mcp_guidelines += """
140
+
141
+ 🛑 SHELL COMMAND RESTRICTIONS 🛑
142
+ Tools are for READING and ANALYZING code, not changing it.
143
+ When using shell commands, stick to read-only operations like:
144
+ - ls, find, grep, cat, head, tail, wc, du, df
145
+ - git status, git log, git show, git diff
146
+
147
+ PROHIBITED shell operations:
148
+ - File modifications: cp, mv, rm, touch, mkdir, chmod, chown
149
+ - Text editing: sed, awk with -i, nano, vim, echo >
150
+ - System changes: sudo, su, systemctl, service
106
151
  """
107
152
 
108
153
  logger.info(f"MCP_GUIDELINES: Original prompt length: {len(prompt)}")
@@ -112,13 +157,8 @@ def mcp_usage_guidelines(prompt: str, context: dict) -> str:
112
157
  logger.info(f"MCP_GUIDELINES: Last 500 chars of modified prompt: ...{modified_prompt[-500:]}")
113
158
  return modified_prompt
114
159
 
115
- def _get_tool_description(tool_name: str) -> str:
116
- """Get a brief description of what an MCP tool is used for."""
117
- descriptions = {
118
- "mcp_get_current_time": "checking current system time and date",
119
- "mcp_run_shell_command": "executing safe shell commands to inspect system state",
120
- }
121
- return descriptions.get(tool_name, "specialized system operations")
160
+ # Removed _get_tool_description() function as it was hardcoding shell tool descriptions
161
+ # even when shell server was disabled. Now we only show descriptions for actually enabled tools.
122
162
 
123
163
  def _get_tool_descriptions_from_mcp(available_tools: list) -> str:
124
164
  """Get tool descriptions from actual MCP tool definitions."""
@@ -130,6 +170,7 @@ def _get_tool_descriptions_from_mcp(available_tools: list) -> str:
130
170
 
131
171
  if mcp_manager.is_initialized:
132
172
  # Get all MCP tools with their descriptions
173
+ # This already filters by enabled servers only
133
174
  mcp_tools = mcp_manager.get_all_tools()
134
175
  tool_map = {tool.name: tool.description for tool in mcp_tools}
135
176
 
@@ -138,20 +179,23 @@ def _get_tool_descriptions_from_mcp(available_tools: list) -> str:
138
179
  clean_name = tool_name[4:] if tool_name.startswith("mcp_") else tool_name
139
180
  description = tool_map.get(clean_name, "Specialized system operations")
140
181
 
141
- display_name = f"mcp_{clean_name}" if not tool_name.startswith("mcp_") else tool_name
142
- tool_descriptions.append(f"- **{display_name}**: {description}")
182
+ # Only add description if the tool is actually available from enabled servers
183
+ if clean_name in tool_map:
184
+ display_name = f"mcp_{clean_name}" if not tool_name.startswith("mcp_") else tool_name
185
+ tool_descriptions.append(f"- **{display_name}**: {description}")
143
186
  else:
144
- # Fallback if MCP manager not initialized
145
- for tool_name in available_tools:
146
- display_name = f"mcp_{tool_name}" if not tool_name.startswith("mcp_") else tool_name
147
- tool_descriptions.append(f"- **{display_name}**: Specialized system operations")
187
+ # If MCP manager not initialized, don't show any tool descriptions
188
+ logger.warning("MCP manager not initialized, no tool descriptions available")
189
+ return ""
148
190
 
149
191
  except Exception as e:
150
192
  logger.warning(f"Could not get MCP tool descriptions: {e}")
151
- # Fallback to generic descriptions
152
- for tool_name in available_tools:
153
- display_name = f"mcp_{tool_name}" if not tool_name.startswith("mcp_") else tool_name
154
- tool_descriptions.append(f"- **{display_name}**: Specialized system operations")
193
+ # Don't provide fallback descriptions - only show what's actually available
194
+ return ""
195
+
196
+ if not tool_descriptions:
197
+ logger.info("No tool descriptions available from enabled servers")
198
+ return "No tools currently available."
155
199
 
156
200
  return "\n".join(tool_descriptions)
157
201
 
@@ -164,7 +208,7 @@ def _get_tool_call_formats_from_mcp(available_tools: list) -> str:
164
208
  if not mcp_manager.is_initialized:
165
209
  return _get_fallback_tool_formats(available_tools)
166
210
 
167
- # Get all MCP tools with their schemas
211
+ # Get all MCP tools with their schemas (already filters by enabled servers only)
168
212
  mcp_tools = mcp_manager.get_all_tools()
169
213
  tool_schemas = {tool.name: tool.inputSchema for tool in mcp_tools}
170
214
 
@@ -174,8 +218,9 @@ def _get_tool_call_formats_from_mcp(available_tools: list) -> str:
174
218
  clean_name = tool_name[4:] if tool_name.startswith("mcp_") else tool_name
175
219
  display_name = f"mcp_{clean_name}" if not tool_name.startswith("mcp_") else tool_name
176
220
 
221
+ # Only generate format examples for tools that are actually available from enabled servers
177
222
  schema = tool_schemas.get(clean_name)
178
- if schema and "properties" in schema:
223
+ if clean_name in tool_schemas and schema and "properties" in schema:
179
224
  # Generate example arguments from schema
180
225
  example_args = _generate_example_args_from_schema(schema, clean_name)
181
226
 
@@ -190,11 +235,14 @@ def _get_tool_call_formats_from_mcp(available_tools: list) -> str:
190
235
  if format_sections:
191
236
  return "\n\n".join(format_sections)
192
237
  else:
193
- return _get_fallback_tool_formats(available_tools)
238
+ # Don't use fallback formats - only show what's actually available
239
+ logger.info("No tool format examples available from enabled servers")
240
+ return "No tool formats currently available."
194
241
 
195
242
  except Exception as e:
196
243
  logger.warning(f"Could not get MCP tool schemas: {e}")
197
- return _get_fallback_tool_formats(available_tools)
244
+ # Don't provide fallback formats - only show what's actually available
245
+ return ""
198
246
 
199
247
  def _generate_example_args_from_schema(schema: dict, tool_name: str) -> str:
200
248
  """Generate example arguments JSON from tool schema."""
@@ -240,30 +288,8 @@ def _get_example_value_for_property(prop_info: dict, prop_name: str, tool_name:
240
288
  else:
241
289
  return f"your_{prop_name}_here"
242
290
 
243
- def _get_fallback_tool_formats(available_tools: list) -> str:
244
- """Fallback tool format examples when schema info isn't available."""
245
- formats = []
246
-
247
- for tool_name in available_tools:
248
- clean_name = tool_name[4:] if tool_name.startswith("mcp_") else tool_name
249
- display_name = f"mcp_{clean_name}" if not tool_name.startswith("mcp_") else tool_name
250
-
251
- if clean_name == "run_shell_command":
252
- example_args = '{{"command": "ls -la"}}'
253
- elif clean_name == "get_current_time":
254
- example_args = '{{"format": "readable"}}'
255
- else:
256
- example_args = '{{"key": "value"}}'
257
-
258
- formats.append(f"""**{display_name} Format:**
259
- ```
260
- {TOOL_SENTINEL_OPEN}
261
- <name>{display_name}</name>
262
- <arguments>{example_args}</arguments>
263
- {TOOL_SENTINEL_CLOSE}
264
- ```""")
265
-
266
- return "\n\n".join(formats)
291
+ # Removed _get_fallback_tool_formats() function as it was hardcoding shell tool examples
292
+ # even when shell server was disabled. Now we only show formats for actually enabled tools.
267
293
 
268
294
  def register_extensions(manager):
269
295
  """
app/main.py CHANGED
@@ -78,11 +78,13 @@ def parse_arguments():
78
78
  parser.add_argument("--list-models", action="store_true",
79
79
  help="List all supported endpoints and their available models")
80
80
  parser.add_argument("--ast", action="store_true",
81
- help="Enable AST-based code understanding capabilities")
81
+ help="Enable AST-based code understanding capabilities (disabled by default)")
82
82
  parser.add_argument("--ast-resolution", choices=['disabled', 'minimal', 'medium', 'detailed', 'comprehensive'],
83
83
  default='medium', help="AST context resolution level (default: medium)")
84
- parser.add_argument("--mcp", action="store_true",
85
- help="Enable MCP (Model Context Protocol) server integration")
84
+ parser.add_argument("--mcp", action="store_true", default=True,
85
+ help="Enable MCP (Model Context Protocol) server integration (enabled by default)")
86
+ parser.add_argument("--no-mcp", action="store_false", dest="mcp",
87
+ help="Disable MCP (Model Context Protocol) server integration")
86
88
  return parser.parse_args()
87
89
 
88
90
 
app/mcp/client.py CHANGED
@@ -237,6 +237,9 @@ class MCPClient:
237
237
 
238
238
  async def _send_request(self, method: str, params: Optional[Dict[str, Any]] = None, _retry_count: int = 0) -> Optional[Dict[str, Any]]:
239
239
  """Send a JSON-RPC request to the MCP server."""
240
+ import time
241
+ start_time = time.time()
242
+
240
243
  max_retries = 3
241
244
 
242
245
  if not self.process or not self.process.stdin:
@@ -270,12 +273,18 @@ class MCPClient:
270
273
 
271
274
  try:
272
275
  request_json = json.dumps(request) + "\n"
276
+ write_start = time.time()
273
277
  self.process.stdin.write(request_json)
274
278
  self.process.stdin.flush()
279
+ write_time = time.time() - write_start
280
+ logger.info(f"🔍 MCP_TIMING: Write took {write_time*1000:.1f}ms")
275
281
 
276
282
  # Read response
277
283
  try:
284
+ read_start = time.time()
278
285
  response_line = self.process.stdout.readline()
286
+ read_time = time.time() - read_start
287
+ logger.info(f"🔍 MCP_TIMING: Read took {read_time*1000:.1f}ms")
279
288
  timeout_occurred = False
280
289
  except Exception as e:
281
290
  logger.error(f"Error reading from MCP server: {e}")
@@ -300,25 +309,23 @@ class MCPClient:
300
309
  if "error" in response:
301
310
  error_info = response['error']
302
311
  error_code = error_info.get("code", -1)
303
- error_message = error_info.get("message", "Unknown MCP server error")
312
+ error_message = str(error_info.get("message", "Unknown error"))
304
313
 
305
314
  # Check if this is a timeout error and we haven't exhausted retries
306
315
  is_timeout = (error_code == -32603 and
307
316
  ("timed out" in error_message.lower() or "timeout" in error_message.lower()))
308
317
 
309
- if is_timeout and _retry_count < max_retries:
310
- logger.warning(f"MCP server timeout (attempt {_retry_count + 1}/{max_retries + 1}), retrying silently: {error_message}")
311
- # Wait a brief moment before retry
318
+ # Timeouts should fail immediately to let the model choose a lighter alternative
319
+ if not is_timeout and _retry_count < max_retries:
320
+ logger.error(f"MCP server error: {error_info}")
321
+ # Only retry non-timeout errors
312
322
  await asyncio.sleep(0.5)
313
323
  return await self._send_request(method, params, _retry_count + 1)
314
324
 
315
- # Log error (only after retries exhausted for timeouts)
316
- if is_timeout:
317
- logger.error(f"MCP server timeout after {max_retries + 1} attempts: {error_info}")
318
- else:
319
- logger.error(f"MCP server error: {error_info}")
325
+ # Log all errors (timeouts fail immediately, others after retries)
326
+ logger.error(f"MCP server {'timeout' if is_timeout else 'error'}: {error_info}")
320
327
 
321
- # Return error information so it can be displayed to the user
328
+ # Create error result
322
329
  return {
323
330
  "error": True,
324
331
  "message": error_message,
@@ -327,6 +334,8 @@ class MCPClient:
327
334
 
328
335
  # Update successful call timestamp
329
336
  self._last_successful_call = time.time()
337
+ total_time = time.time() - start_time
338
+ logger.info(f"🔍 MCP_TIMING: Total request took {total_time*1000:.1f}ms for method '{method}'")
330
339
  return response.get("result")
331
340
 
332
341
  except Exception as e:
app/mcp/manager.py CHANGED
@@ -45,6 +45,11 @@ class MCPManager:
45
45
  self._reconnection_attempts: Dict[str, float] = {} # Track last reconnection attempt per server
46
46
  self._failed_servers: set = set() # Servers that have failed too many times
47
47
 
48
+ # Loop detection for repetitive tool calls
49
+ self._recent_tool_calls: List[tuple] = [] # (tool_name, arguments, timestamp)
50
+ self._max_recent_calls = 10
51
+ self._loop_detection_window = 30 # seconds
52
+
48
53
  def _get_builtin_server_definitions(self) -> Dict[str, Dict[str, Any]]:
49
54
  """Defines configurations for built-in MCP servers."""
50
55
  builtin_servers = {}
@@ -334,16 +339,16 @@ class MCPManager:
334
339
  logger.error(f"No configuration found for server '{server_name}' during restart.")
335
340
  return False
336
341
 
337
- # Create and connect new client
338
- client = MCPClient(server_config)
339
- self.clients[server_name] = client
340
- success = await self._connect_server(server_name, client)
341
-
342
- # Invalidate cache when client configuration changes
343
- self.invalidate_tools_cache()
344
-
345
- logger.info(f"Server {server_name} restart {'successful' if success else 'failed'}")
346
- return success
342
+ # Create and connect new client
343
+ client = MCPClient(server_config)
344
+ self.clients[server_name] = client
345
+ success = await self._connect_server(server_name, client)
346
+
347
+ # Invalidate cache when client configuration changes
348
+ self.invalidate_tools_cache()
349
+
350
+ logger.info(f"Server {server_name} restart {'successful' if success else 'failed'}")
351
+ return success
347
352
 
348
353
  except Exception as e:
349
354
  logger.error(f"Error restarting server {server_name}: {str(e)}")
@@ -394,6 +399,8 @@ class MCPManager:
394
399
  server_config = self.server_configs.get(server_name, {})
395
400
  is_enabled = server_config.get("enabled", True)
396
401
 
402
+ logger.info(f"MCP_MANAGER.get_all_tools: Server '{server_name}' - connected: {client.is_connected}, enabled: {is_enabled}")
403
+
397
404
  if client.is_connected and is_enabled:
398
405
  client_tools = client.tools
399
406
  logger.info(f"MCP_MANAGER.get_all_tools: Server '{server_name}' has {len(client_tools)} tools: {[t.name for t in client_tools]}")
@@ -464,6 +471,30 @@ class MCPManager:
464
471
  return content
465
472
  return None
466
473
 
474
+ def _is_repetitive_call(self, tool_name: str, arguments: Dict[str, Any]) -> bool:
475
+ """Check if this tool call is repetitive within the detection window."""
476
+ current_time = time.time()
477
+ call_signature = (tool_name, str(arguments))
478
+
479
+ # Clean old calls outside the window
480
+ self._recent_tool_calls = [
481
+ (name, args, timestamp) for name, args, timestamp in self._recent_tool_calls
482
+ if current_time - timestamp <= self._loop_detection_window
483
+ ]
484
+
485
+ # Count identical calls in the window
486
+ identical_calls = sum(1 for name, args, _ in self._recent_tool_calls
487
+ if (name, args) == call_signature)
488
+
489
+ # Add current call
490
+ self._recent_tool_calls.append((tool_name, str(arguments), current_time))
491
+
492
+ # Keep only recent calls
493
+ if len(self._recent_tool_calls) > self._max_recent_calls:
494
+ self._recent_tool_calls = self._recent_tool_calls[-self._max_recent_calls:]
495
+
496
+ return identical_calls >= 3 # Allow max 3 identical calls
497
+
467
498
  async def call_tool(self, tool_name: str, arguments: Dict[str, Any], server_name: Optional[str] = None) -> Optional[Dict[str, Any]]:
468
499
  """
469
500
  Call an MCP tool.
@@ -476,6 +507,11 @@ class MCPManager:
476
507
  Returns:
477
508
  Tool execution result or None if not found
478
509
  """
510
+ # Check for repetitive calls
511
+ if self._is_repetitive_call(tool_name, arguments):
512
+ logger.warning(f"🔍 MCP_MANAGER: Blocking repetitive tool call: {tool_name} with {arguments}")
513
+ return {"content": [{"type": "text", "text": "Tool call blocked due to repetitive execution pattern"}]}
514
+
479
515
  # Remove mcp_ prefix if present for internal tool lookup
480
516
  internal_tool_name = tool_name
481
517
  if tool_name.startswith("mcp_"):
@@ -559,6 +595,28 @@ class MCPManager:
559
595
  _mcp_manager: Optional[MCPManager] = None
560
596
  def get_mcp_manager() -> MCPManager:
561
597
  """Get the global MCP manager instance."""
598
+ import os
599
+
600
+ # Check if MCP is enabled before creating manager
601
+ if not os.environ.get("ZIYA_ENABLE_MCP", "true").lower() in ("true", "1", "yes"):
602
+ # Return a dummy manager that's never initialized when MCP is disabled
603
+ class DisabledMCPManager:
604
+ def __init__(self):
605
+ self.is_initialized = False
606
+ self.clients = {}
607
+ self.server_configs = {}
608
+
609
+ async def initialize(self):
610
+ pass
611
+
612
+ def get_all_tools(self):
613
+ return []
614
+
615
+ def get_server_status(self):
616
+ return {}
617
+
618
+ return DisabledMCPManager()
619
+
562
620
  global _mcp_manager
563
621
  if _mcp_manager is None:
564
622
  _mcp_manager = MCPManager()
app/mcp/tools.py CHANGED
@@ -82,9 +82,10 @@ def parse_tool_call(content: str) -> Optional[Dict[str, Any]]:
82
82
  sentinel_open_escaped = re.escape(TOOL_SENTINEL_OPEN)
83
83
  sentinel_close_escaped = re.escape(TOOL_SENTINEL_CLOSE)
84
84
 
85
- # Format 1: <n> format with complete closing tag
85
+ # Format 1: Handle both <name> and <n> formats
86
+ # Pattern: <TOOL_SENTINEL><name>tool_name</name><arguments>{...}</arguments></TOOL_SENTINEL>
86
87
  # Pattern: <TOOL_SENTINEL><n>tool_name</n><arguments>{...}</arguments></TOOL_SENTINEL>
87
- complete_pattern = f'{sentinel_open_escaped}\\s*<n>([^<]+)</n>\\s*<arguments>\\s*(\\{{.*?\\}})\\s*</arguments>\\s*{sentinel_close_escaped}'
88
+ complete_pattern = f'{sentinel_open_escaped}\\s*<(?:name|n)>([^<]+)</(?:name|n)>\\s*<arguments>\\s*(\\{{.*?\\}})\\s*</arguments>\\s*{sentinel_close_escaped}'
88
89
  match = re.search(complete_pattern, content, re.DOTALL)
89
90
  if match:
90
91
  tool_name = match.group(1).strip()
@@ -93,11 +94,11 @@ def parse_tool_call(content: str) -> Optional[Dict[str, Any]]:
93
94
  logger.info(f"🔍 PARSE_DEBUG: Raw arguments string: '{match.group(2)}'")
94
95
  logger.info(f"🔍 PARSE_DEBUG: Parsed arguments: {arguments}")
95
96
  print(f"🔍 PARSE_DEBUG: Raw arguments string: '{match.group(2)}', Parsed: {arguments}")
96
- logger.debug(f"🔍 PARSE: Successfully parsed complete <n> format - tool: {tool_name}, args: {arguments}")
97
+ logger.debug(f"🔍 PARSE: Successfully parsed tool format - tool: {tool_name}, args: {arguments}")
97
98
  logger.info(f"🔍 PARSE SUCCESS: tool_name='{tool_name}', arguments={arguments}")
98
99
  print(f"🔍 PARSE SUCCESS: tool_name='{tool_name}', arguments={arguments}")
99
100
  return {"tool_name": tool_name, "arguments": arguments}
100
- except json.JSONDecodeError:
101
+ except json.JSONDecodeError as e:
101
102
  # Try to fix common JSON parsing issues with shell commands
102
103
  try:
103
104
  # Extract the raw arguments string and attempt to repair it
@@ -111,13 +112,11 @@ def parse_tool_call(content: str) -> Optional[Dict[str, Any]]:
111
112
  print(f"🔍 PARSE REPAIRED: tool_name='{tool_name}', arguments={arguments}")
112
113
  logger.debug(f"🔍 PARSE: Successfully parsed repaired JSON - tool: {tool_name}, args: {arguments}")
113
114
  return {"tool_name": tool_name, "arguments": arguments}
114
- except Exception as e:
115
- logger.error(f"🔍 PARSE_DEBUG: Both original and repair parsing failed: {e}")
116
- print(f"🔍 PARSE_DEBUG: Both original and repair parsing failed: {e}")
115
+ except Exception as repair_error:
116
+ logger.error(f"🔍 PARSE_DEBUG: Both original and repair parsing failed: {repair_error}")
117
+ print(f"🔍 PARSE_DEBUG: Both original and repair parsing failed: {repair_error}")
117
118
  logger.warning(f"Failed to parse JSON arguments for tool {tool_name}: {e}")
118
119
  return None
119
-
120
- # Format 1b: <name> format with complete closing tag (alternative format)
121
120
  # Pattern: <TOOL_SENTINEL><name>tool_name</name><arguments>{...}</arguments></TOOL_SENTINEL>
122
121
  complete_name_pattern = f'{sentinel_open_escaped}\\s*<name>([^<]+)</name>\\s*<arguments>\\s*(\\{{.*?\\}})\\s*</arguments>\\s*{sentinel_close_escaped}'
123
122
  match = re.search(complete_name_pattern, content, re.DOTALL)
@@ -15,7 +15,7 @@ from typing import Dict, Any, Optional
15
15
 
16
16
  # Import centralized shell configuration
17
17
  sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
18
- from config.shell_config import DEFAULT_SHELL_CONFIG
18
+ from app.config.shell_config import DEFAULT_SHELL_CONFIG
19
19
 
20
20
 
21
21
  # Global timeout tracking
@@ -185,13 +185,13 @@ class ShellServer:
185
185
  "tools": [
186
186
  {
187
187
  "name": "run_shell_command",
188
- "description": f"Execute a shell command. Allowed commands: {self.get_allowed_commands_description()}",
188
+ "description": f"Execute a complete, non-interactive shell command. Commands must be self-contained with all arguments provided - do NOT use interactive mode (e.g., use 'echo \"2+2\" | bc' not just 'bc'). Allowed commands: {self.get_allowed_commands_description()}",
189
189
  "inputSchema": {
190
190
  "type": "object",
191
191
  "properties": {
192
192
  "command": {
193
193
  "type": "string",
194
- "description": "The shell command to execute"
194
+ "description": "A complete, non-interactive shell command with all required arguments (e.g., 'ls -la', 'grep pattern file', 'echo \"2+2\" | bc'). CRITICAL: Commands must be complete operations that do not require interactive input. For calculators like bc, pipe the expression: 'echo \"expression\" | bc'. Do not use incomplete commands or interactive modes."
195
195
  },
196
196
  "timeout": {
197
197
  "type": "number",