Skip to content

Commit 2998414

Browse files
kingpanther13claude
andcommitted
refactor: address Gemini review feedback
- Collapse verbose if/continue search chain into single boolean (tool_proxy.py) - Remove unnecessary isinstance(params, dict) guards in _schema_hash, _make_summary, and _format_parameters — register_tool() guarantees dict - Replace fragile startswith("ha_find_tool") with _META_TOOLS constant set - Remove dead _unwrap_proxy_error method (ToolError now propagates directly) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 7f7a53b commit 2998414

2 files changed

Lines changed: 15 additions & 67 deletions

File tree

src/ha_mcp/tools/tool_proxy.py

Lines changed: 10 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -93,26 +93,14 @@ def find_tools(self, query: str) -> list[dict[str, Any]]:
9393
results = []
9494

9595
for name, tool in self._tools.items():
96-
# Match on tool name
97-
if query_lower in name.lower():
98-
results.append(self._make_summary(tool))
99-
continue
100-
101-
# Match on category
102-
if query_lower in tool["category"].lower():
103-
results.append(self._make_summary(tool))
104-
continue
105-
106-
# Match on description keywords
107-
if query_lower in tool["description"].lower():
108-
results.append(self._make_summary(tool))
109-
continue
110-
111-
# Match on annotation tags
11296
tags = tool["annotations"].get("tags", [])
113-
if any(query_lower in tag.lower() for tag in tags):
97+
if (
98+
query_lower in name.lower()
99+
or query_lower in tool["category"].lower()
100+
or query_lower in tool["description"].lower()
101+
or any(query_lower in tag.lower() for tag in tags)
102+
):
114103
results.append(self._make_summary(tool))
115-
continue
116104

117105
return results
118106

@@ -156,28 +144,24 @@ def _schema_hash(self, tool_name: str) -> str:
156144
if not tool:
157145
return ""
158146
params = tool["parameters"]
159-
param_keys = sorted(params.get("properties", {}).keys()) if isinstance(params, dict) else []
160-
required = sorted(params.get("required", [])) if isinstance(params, dict) else []
147+
param_keys = sorted(params.get("properties", {}).keys())
148+
required = sorted(params.get("required", []))
161149
fingerprint = f"{tool_name}:{','.join(param_keys)}:{','.join(required)}"
162150
return hashlib.md5(fingerprint.encode()).hexdigest()[:8]
163151

164152
def _make_summary(self, tool: dict[str, Any]) -> dict[str, Any]:
165153
params = tool["parameters"]
166-
props = params.get("properties", {}) if isinstance(params, dict) else {}
167154
return {
168155
"tool_name": tool["name"],
169156
"summary": tool["description"].strip().split("\n")[0],
170157
"category": tool["category"],
171158
"is_destructive": tool["annotations"].get("destructiveHint", False),
172-
"parameters": list(props.keys()),
173-
"required_parameters": params.get("required", []) if isinstance(params, dict) else [],
159+
"parameters": list(params.get("properties", {}).keys()),
160+
"required_parameters": params.get("required", []),
174161
}
175162

176163
def _format_parameters(self, params: dict[str, Any]) -> list[dict[str, Any]]:
177164
"""Format parameter schema into LLM-friendly list."""
178-
if not isinstance(params, dict):
179-
return []
180-
181165
properties = params.get("properties", {})
182166
required = set(params.get("required", []))
183167
result = []

tests/src/e2e/conftest.py

Lines changed: 5 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,9 @@ async def mcp_server(
336336
# Server cleanup handled by server.close()
337337

338338

339+
_META_TOOLS = {"ha_find_tools", "ha_get_tool_details", "ha_execute_tool"}
340+
341+
339342
class _ProxyAwareClient:
340343
"""Wraps a FastMCP Client to transparently route proxied tools.
341344
@@ -356,9 +359,7 @@ async def call_tool(self, tool_name: str, args: dict[str, Any] | None = None) ->
356359
args = {}
357360

358361
# Meta-tools and known non-proxied tools skip the proxy
359-
if tool_name in self._direct_tools or tool_name.startswith("ha_find_tool") or tool_name in (
360-
"ha_get_tool_details", "ha_execute_tool",
361-
):
362+
if tool_name in self._direct_tools or tool_name in _META_TOOLS:
362363
return await self._client.call_tool(tool_name, args)
363364

364365
# Discover whether this tool is proxied (result cached)
@@ -372,7 +373,7 @@ async def call_tool(self, tool_name: str, args: dict[str, Any] | None = None) ->
372373
return await self._client.call_tool(tool_name, args)
373374
self._schema_cache[tool_name] = details["schema_hash"]
374375

375-
result = await self._client.call_tool(
376+
return await self._client.call_tool(
376377
"ha_execute_tool",
377378
{
378379
"tool_name": tool_name,
@@ -381,43 +382,6 @@ async def call_tool(self, tool_name: str, args: dict[str, Any] | None = None) ->
381382
},
382383
)
383384

384-
# Unwrap proxy error responses so tests see the original tool output.
385-
# When a proxied tool raises an exception, ha_execute_tool catches it
386-
# and wraps the response as {"error": {"message": "Tool execution
387-
# failed: {original_json}"}}. We extract the original JSON so tests
388-
# don't need to know about the proxy layer.
389-
return self._unwrap_proxy_error(result)
390-
391-
@staticmethod
392-
def _unwrap_proxy_error(result: Any) -> Any:
393-
"""If *result* is a proxy-wrapped error containing the original tool
394-
response as stringified JSON, return a synthetic result with the
395-
unwrapped JSON so callers see the original response shape."""
396-
try:
397-
data = parse_mcp_result(result)
398-
except Exception:
399-
return result
400-
401-
error = data.get("error")
402-
if not isinstance(error, dict):
403-
return result
404-
405-
msg = error.get("message", "")
406-
prefix = "Tool execution failed: "
407-
if not msg.startswith(prefix):
408-
return result
409-
410-
# Try to parse the original tool response out of the message
411-
try:
412-
original = json.loads(msg[len(prefix):])
413-
except (json.JSONDecodeError, TypeError):
414-
return result
415-
416-
# Rebuild as a synthetic MCP text-content result matching what
417-
# the original tool would have returned directly.
418-
from mcp.types import TextContent
419-
return [TextContent(type="text", text=json.dumps(original))]
420-
421385
# Forward everything else (list_tools, session, etc.) to the real client
422386
def __getattr__(self, name: str) -> Any:
423387
return getattr(self._client, name)

0 commit comments

Comments
 (0)