fix: keep intent out of mcp tool arguments

This commit is contained in:
JOJO 2026-04-27 14:53:39 +08:00
parent de15fbe970
commit 83e037c526
3 changed files with 183 additions and 1 deletions

View File

@ -126,6 +126,10 @@ class MainTerminalToolsDefinitionMixin:
}
for tool in tools:
func = tool.get("function") or {}
tool_name = str(func.get("name") or "").strip()
# MCP 扩展工具参数需与远端 schema 严格一致,不注入 intent
if tool_name.startswith("mcp__"):
continue
params = func.get("parameters") or {}
if not isinstance(params, dict):
continue

View File

@ -695,10 +695,15 @@ class MainTerminalToolsExecutionMixin:
if manager is None:
result = {"success": False, "error": "MCP 管理器不可用"}
else:
# intent 仅用于原生工具的人类可读展示,不应透传给 MCP 远端工具
mcp_arguments = arguments
if isinstance(arguments, dict) and "intent" in arguments:
mcp_arguments = dict(arguments)
mcp_arguments.pop("intent", None)
result = await asyncio.to_thread(
manager.call_tool_by_alias,
tool_name,
arguments,
mcp_arguments,
alias_map=getattr(self, "mcp_tool_alias_map", None),
)
elif custom_tool:

View File

@ -118,6 +118,179 @@ class MCPIntegrationTest(unittest.TestCase):
self.assertTrue(list_data.get("success"), list_data)
self.assertGreaterEqual(int(list_data.get("count", 0)), 1, list_data)
def test_main_terminal_mcp_call_drops_intent_argument(self):
strict_script = self._write_temp_server(
"strict_args_mcp_server.py",
"""
import json
import sys
def send(payload):
sys.stdout.write(json.dumps(payload, ensure_ascii=False) + "\\n")
sys.stdout.flush()
while True:
line = sys.stdin.readline()
if not line:
break
line = line.strip()
if not line:
continue
req = json.loads(line)
method = req.get("method")
msg_id = req.get("id")
params = req.get("params") or {}
if msg_id is None:
continue
if method == "initialize":
send({
"jsonrpc": "2.0",
"id": msg_id,
"result": {
"protocolVersion": "2025-06-18",
"capabilities": {"tools": {}},
"serverInfo": {"name": "strict-args", "version": "1.0.0"},
},
})
continue
if method == "tools/list":
send({
"jsonrpc": "2.0",
"id": msg_id,
"result": {
"tools": [
{
"name": "echo_strict",
"description": "仅接受 text 参数",
"inputSchema": {
"type": "object",
"properties": {"text": {"type": "string"}},
"required": ["text"],
},
}
]
},
})
continue
if method == "tools/call":
name = str(params.get("name") or "")
arguments = params.get("arguments") if isinstance(params.get("arguments"), dict) else {}
keys = sorted(arguments.keys())
if name != "echo_strict":
send({
"jsonrpc": "2.0",
"id": msg_id,
"result": {
"isError": True,
"content": [{"type": "text", "text": "unknown tool"}],
},
})
continue
if keys != ["text"]:
send({
"jsonrpc": "2.0",
"id": msg_id,
"result": {
"isError": True,
"content": [{"type": "text", "text": "unexpected keys:" + ",".join(keys)}],
"structuredContent": {"received_keys": keys},
},
})
continue
text = str(arguments.get("text") or "")
send({
"jsonrpc": "2.0",
"id": msg_id,
"result": {
"isError": False,
"content": [{"type": "text", "text": text.upper()}],
"structuredContent": {"received_keys": keys, "text": text.upper()},
},
})
continue
send({
"jsonrpc": "2.0",
"id": msg_id,
"error": {"code": -32601, "message": f"Method not found: {method}"},
})
""",
)
self.registry.upsert_server(
{
"id": "strict_stdio",
"name": "Strict MCP",
"transport": "stdio",
"enabled": True,
"command": sys.executable,
"args": [str(strict_script)],
"timeout_seconds": 10,
}
)
sync_result = self.manager.sync_servers(server_id="strict_stdio")
self.assertTrue(sync_result.get("success"), sync_result)
terminal = MainTerminal(
project_path=str(self.project_dir),
thinking_mode=False,
data_dir=str(self.data_dir),
)
terminal.mcp_tools_enabled = True
terminal.mcp_server_registry = self.registry
terminal.mcp_client_manager = self.manager
alias = None
for item in terminal.define_tools():
name = ((item or {}).get("function") or {}).get("name")
if isinstance(name, str) and name.startswith("mcp__strict_stdio__"):
alias = name
break
self.assertTrue(alias, "missing strict mcp alias")
raw = asyncio.run(terminal.handle_tool_call(alias, {"intent": "测试调用", "text": "abc"}))
data = json.loads(raw)
self.assertTrue(data.get("success"), data)
self.assertIn("ABC", json.dumps(data, ensure_ascii=False))
self.assertEqual((data.get("structured_content") or {}).get("received_keys"), ["text"], data)
def test_define_tools_skip_intent_in_mcp_alias(self):
sync_result = self.manager.sync_servers(server_id="demo_stdio")
self.assertTrue(sync_result.get("success"), sync_result)
terminal = MainTerminal(
project_path=str(self.project_dir),
thinking_mode=False,
data_dir=str(self.data_dir),
)
terminal.mcp_tools_enabled = True
terminal.mcp_server_registry = self.registry
terminal.mcp_client_manager = self.manager
terminal.tool_intent_enabled = True
tool_defs = terminal.define_tools()
mcp_tool = None
native_tool = None
for item in tool_defs:
fn = (item or {}).get("function") or {}
name = str(fn.get("name") or "")
if not native_tool and name == "run_command":
native_tool = item
if not mcp_tool and name.startswith("mcp__demo_stdio__"):
mcp_tool = item
if mcp_tool and native_tool:
break
self.assertIsNotNone(native_tool, "missing native run_command tool")
self.assertIsNotNone(mcp_tool, "missing mcp alias tool")
native_params = ((native_tool or {}).get("function") or {}).get("parameters") or {}
native_props = native_params.get("properties") if isinstance(native_params, dict) else {}
self.assertIn("intent", native_props or {})
mcp_params = ((mcp_tool or {}).get("function") or {}).get("parameters") or {}
mcp_props = mcp_params.get("properties") if isinstance(mcp_params, dict) else {}
self.assertNotIn("intent", mcp_props or {})
self.assertNotIn("intent", mcp_params.get("required") or [])
def test_main_terminal_mcp_blocked_in_docker_mode(self):
terminal = MainTerminal(
project_path=str(self.project_dir),