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.
- app/agents/agent.py +71 -73
- app/agents/direct_streaming.py +1 -1
- app/agents/prompts.py +1 -1
- app/agents/prompts_manager.py +14 -10
- app/agents/wrappers/google_direct.py +31 -1
- app/agents/wrappers/nova_tool_execution.py +2 -2
- app/agents/wrappers/nova_wrapper.py +1 -1
- app/agents/wrappers/ziya_bedrock.py +53 -31
- app/config/models_config.py +61 -20
- app/config/shell_config.py +5 -1
- app/extensions/prompt_extensions/claude_extensions.py +27 -5
- app/extensions/prompt_extensions/mcp_prompt_extensions.py +82 -56
- app/main.py +5 -3
- app/mcp/client.py +19 -10
- app/mcp/manager.py +68 -10
- app/mcp/tools.py +8 -9
- app/mcp_servers/shell_server.py +3 -3
- app/middleware/streaming.py +29 -41
- app/routes/file_validation.py +35 -0
- app/routes/mcp_routes.py +54 -8
- app/server.py +525 -614
- app/streaming_tool_executor.py +748 -137
- app/templates/asset-manifest.json +20 -20
- app/templates/index.html +1 -1
- app/templates/static/css/{main.0297bfee.css → main.e7109b49.css} +2 -2
- app/templates/static/css/main.e7109b49.css.map +1 -0
- app/templates/static/js/14386.65fcfe53.chunk.js +2 -0
- app/templates/static/js/14386.65fcfe53.chunk.js.map +1 -0
- app/templates/static/js/35589.0368973a.chunk.js +2 -0
- app/templates/static/js/35589.0368973a.chunk.js.map +1 -0
- app/templates/static/js/{50295.ab92f61b.chunk.js → 50295.90aca393.chunk.js} +3 -3
- app/templates/static/js/50295.90aca393.chunk.js.map +1 -0
- app/templates/static/js/55734.5f0fd567.chunk.js +2 -0
- app/templates/static/js/55734.5f0fd567.chunk.js.map +1 -0
- app/templates/static/js/58542.57fed736.chunk.js +2 -0
- app/templates/static/js/58542.57fed736.chunk.js.map +1 -0
- app/templates/static/js/{68418.2554bb1e.chunk.js → 68418.f7b4d2d9.chunk.js} +3 -3
- app/templates/static/js/68418.f7b4d2d9.chunk.js.map +1 -0
- app/templates/static/js/99948.b280eda0.chunk.js +2 -0
- app/templates/static/js/99948.b280eda0.chunk.js.map +1 -0
- app/templates/static/js/main.e075582c.js +3 -0
- app/templates/static/js/main.e075582c.js.map +1 -0
- app/utils/code_util.py +5 -2
- app/utils/context_cache.py +11 -0
- app/utils/conversation_filter.py +90 -0
- app/utils/custom_bedrock.py +43 -1
- app/utils/diff_utils/validation/validators.py +32 -22
- app/utils/file_cache.py +5 -3
- app/utils/precision_prompt_system.py +116 -0
- app/utils/streaming_optimizer.py +100 -0
- {ziya-0.3.0.dist-info → ziya-0.3.2.dist-info}/METADATA +3 -2
- {ziya-0.3.0.dist-info → ziya-0.3.2.dist-info}/RECORD +59 -55
- app/templates/static/css/main.0297bfee.css.map +0 -1
- app/templates/static/js/14386.567bf803.chunk.js +0 -2
- app/templates/static/js/14386.567bf803.chunk.js.map +0 -1
- app/templates/static/js/35589.278ecda2.chunk.js +0 -2
- app/templates/static/js/35589.278ecda2.chunk.js.map +0 -1
- app/templates/static/js/50295.ab92f61b.chunk.js.map +0 -1
- app/templates/static/js/55734.90d8bd52.chunk.js +0 -2
- app/templates/static/js/55734.90d8bd52.chunk.js.map +0 -1
- app/templates/static/js/58542.08fb5cf4.chunk.js +0 -2
- app/templates/static/js/58542.08fb5cf4.chunk.js.map +0 -1
- app/templates/static/js/68418.2554bb1e.chunk.js.map +0 -1
- app/templates/static/js/99948.71670e91.chunk.js +0 -2
- app/templates/static/js/99948.71670e91.chunk.js.map +0 -1
- app/templates/static/js/main.1d79eac2.js +0 -3
- app/templates/static/js/main.1d79eac2.js.map +0 -1
- /app/templates/static/js/{50295.ab92f61b.chunk.js.LICENSE.txt → 50295.90aca393.chunk.js.LICENSE.txt} +0 -0
- /app/templates/static/js/{68418.2554bb1e.chunk.js.LICENSE.txt → 68418.f7b4d2d9.chunk.js.LICENSE.txt} +0 -0
- /app/templates/static/js/{main.1d79eac2.js.LICENSE.txt → main.e075582c.js.LICENSE.txt} +0 -0
- {ziya-0.3.0.dist-info → ziya-0.3.2.dist-info}/WHEEL +0 -0
- {ziya-0.3.0.dist-info → ziya-0.3.2.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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. **
|
|
100
|
-
1. **
|
|
101
|
-
2. **
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
116
|
-
|
|
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
|
-
|
|
142
|
-
|
|
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
|
-
#
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
#
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
244
|
-
|
|
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
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
|
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
|
-
#
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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: <
|
|
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
|
|
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
|
|
115
|
-
logger.error(f"🔍 PARSE_DEBUG: Both original and repair parsing failed: {
|
|
116
|
-
print(f"🔍 PARSE_DEBUG: Both original and repair parsing failed: {
|
|
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)
|
app/mcp_servers/shell_server.py
CHANGED
|
@@ -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": "
|
|
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",
|