Skip to content

feat: tool search proxy for context reduction (phase 1) - #627

Closed
kingpanther13 wants to merge 13 commits into
homeassistant-ai:masterfrom
kingpanther13:feat/tool-search-proxy
Closed

feat: tool search proxy for context reduction (phase 1)#627
kingpanther13 wants to merge 13 commits into
homeassistant-ai:masterfrom
kingpanther13:feat/tool-search-proxy

Conversation

@kingpanther13

@kingpanther13 kingpanther13 commented Feb 14, 2026

Copy link
Copy Markdown
Member

What does this PR do?

Implements the server-side Tool Search Proxy pattern to dramatically reduce idle context token usage. Instead of registering all tools with MCP (consuming ~35K tokens of tool definitions on every turn), niche tools are served through 3 lightweight meta-tools.

This is the same pattern recommended by Anthropic for large tool libraries and independently validated by Speakeasy (100x token reduction on 400-tool servers).

The Problem

ha-mcp v6.6.1 registers 94+ tools. Their combined definitions consume ~35,500 tokens — 17.8% of Claude's context, 27.8% of GPT-4o's, and well over ChatGPT's hard 16K tool token limit (#614). This makes ha-mcp completely unusable on ChatGPT and wastes significant context on every turn regardless of which tools are used.

Why Not listChanged / Dynamic Registration?

listChanged was my initial preferred approach — dynamically add/remove tools as the conversation narrows while keeping them first-class. I abandoned it after research showed:

  • Uncertain and fragmented client support — it is unclear which MCP clients fully support listChanged. Many clients and LLMs may not handle dynamic tool list updates correctly or at all.
  • The LLM API is stateless — every turn re-serializes all currently-registered tool schemas. listChanged reduces the server's tool list, but the client still sends full schemas for every visible tool on every turn.
  • Server-side omniscience problem — the server does not know what the user will ask. The LLM is the entity best positioned to discover tools on demand.
  • I considered implementing a hybrid (try listChanged first, fall back to proxy), but the complexity was not justified given the uncertain client support landscape.

The proxy pattern is guaranteed to work with any MCP client. The 3 meta-tools are standard MCP tools registered via @mcp.tool() — they appear in tools/list with proper inputSchema and annotations like any other tool. Any client that can call an MCP tool can use them, regardless of which LLM is behind it.

The Solution

Three meta-tools replace direct registration for proxied tools:

Meta-Tool Purpose
ha_find_tools(query) Search by name/category/keyword. Description includes full capabilities catalog.
ha_get_tool_details(tool_name) Returns complete description + parameter schema + schema_hash
ha_execute_tool(tool_name, args, tool_schema) Validates schema proof + dispatches to real implementation

Schema enforcement: ha_execute_tool requires a tool_schema hash (12 hex chars / 48-bit) that can only come from ha_get_tool_details. This structurally prevents LLMs from calling tools without reading documentation — same principle as the guide_response pattern from #616 but applied universally.

Annotation transparency: ha_find_tools returns is_destructive for each result. ha_get_tool_details returns the full annotation set (destructiveHint, readOnlyHint, idempotentHint). ha_execute_tool is annotated as destructiveHint: True as a conservative default.

Phase 1 (This PR): 10 Tools from 5 Modules

Module Tools Proxied Type
tools_zones ha_get_zone, ha_create_zone, ha_update_zone, ha_delete_zone 1 read + 3 destructive
tools_labels ha_config_get_label, ha_config_set_label, ha_config_remove_label 1 read + 2 destructive
tools_addons ha_get_addon 1 read
tools_voice_assistant ha_get_entity_exposure 1 read
tools_traces ha_get_automation_traces 1 read

Net result: Tool count decreases by 7 (10 removed, 3 meta-tools added). Phased approach validates schema extraction, error propagation, and E2E test transparency before expanding. Expansion is trivial — add module names to PROXY_MODULES set.

Migration Roadmap

Phase 1 (this PR):  10 tools proxied  ->  ~87 registered tools
Phase 2:            +20 tools         ->  ~67 registered tools
Phase 3:            +30 tools         ->  ~37 registered tools
Phase 4:            remaining         ->    3 registered tools (final state)

Each subsequent phase is a one-line change — add module names to PROXY_MODULES set in tool_proxy.py. No tool implementation code is modified at any phase.

Known Trade-offs

  • Proxied tools are not in tools/list — this is intentional (the point is to remove idle context), but means MCP clients that previously called these tools directly will need to use the meta-tool workflow. For this ha-mcp server specifically, these tools have never been in a stable release with direct registration.
  • String dispatchha_execute_tool takes args as a JSON string rather than typed parameters. Server-side validation (schema hash + required param check + JSON parse) mitigates this.
  • Extra round-trips — A proxied tool call takes 2-3 LLM turns instead of 1. However: ~600 tokens idle + ~1,500 tokens per tool use vs ~35,000 tokens idle on every turn. In a 20-turn conversation with 3-4 tool uses, the proxy saves ~650K tokens.

What is NOT Changed

  • Zero tool descriptions modified — full descriptions live verbatim in the server-side registry, returned word-for-word by ha_get_tool_details
  • Zero tool implementations modified — same code, same behavior, different routing
  • ENABLED_TOOL_MODULES still works — respects existing module filtering

LLM Testing Plan

Test Prompts

The following prompts should be given to LLM agents connected to ha-mcp without any warning that the tool model has changed.

Test 1 — Tool Discovery (Basic)

"What zones do I have set up in Home Assistant?"

Expected: Agent discovers ha_get_zone via ha_find_tools, reads details, executes successfully.

Test 2 — Destructive Operation (Schema Enforcement)

"Create a new zone called 'Office' at latitude 40.7128, longitude -74.0060 with a 150 meter radius"

Expected: Agent discovers ha_create_zone, gets full schema, passes correct args with schema_hash.

Test 3 — Cross-Category Discovery

"Show me all my labels and also check which entities are exposed to Alexa"

Expected: Agent finds both ha_config_get_label and ha_get_entity_exposure in separate searches.

Test 4 — Addon Query

"What add-ons do I have installed on my Home Assistant?"

Expected: Agent discovers ha_get_addon, reads details, executes with no args or source='installed'.

Test 5 — Debugging Workflow

"My motion light automation isn't working. Can you check the execution traces?"

Expected: Agent discovers ha_get_automation_traces, asks for automation entity_id, executes.

Test 6 — Unknown Capability

"Can you manage my Home Assistant zones?"

Expected: Agent sees "zone" tools in ha_find_tools catalog and proceeds. Should NOT say "I don't have that capability."

Test 7 — Direct Tool Access Still Works

"Turn on the living room lights"

Expected: Agent uses ha_call_service directly (still registered normally). Proxy does not interfere.

Target Clients for Testing

References

Changes

  • src/ha_mcp/tools/tool_proxy.py — New: proxy registry, meta-tools, MockMCP with SDK guard, schema enforcement
  • src/ha_mcp/tools/registry.py — Modified: skip proxy modules, wire up proxy registration
  • tests/src/unit/test_tool_proxy.py — New: 26 unit tests covering registry, meta-tools, enforcement
  • tests/src/e2e/conftest.py — Modified: _ProxyAwareClient wrapper for transparent E2E routing

Type of change

  • Bug fix
  • New feature
  • Documentation
  • Maintenance/refactor
  • Breaking change (proxied tools removed from tools/list — accessible via meta-tools)

Testing

  • I have tested these changes with a LLM agent
  • All automated tests pass (uv run pytest)
  • Code follows style guidelines (uv run ruff check)

Checklist

  • I have updated documentation if needed

Implements the server-side Tool Search Proxy pattern recommended by
Anthropic (code execution with MCP blog) and validated by Speakeasy
(100x token reduction). Instead of registering all tools with MCP,
niche tools are served through 3 meta-tools: ha_find_tools,
ha_get_tool_details, ha_execute_tool.

Phase 1 proxies 10 tools from 5 modules (zones, labels, addons,
voice_assistant, traces). Net tool count decreases by 7 (10 removed,
3 meta-tools added). Tool descriptions and implementations are
completely unchanged — they're stored server-side and returned on
demand.

Schema enforcement via required tool_schema hash prevents LLMs from
calling tools without reading documentation first.

Migration roadmap in tool_proxy.py PROXY_MODULES comments.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@kingpanther13
kingpanther13 requested a review from a team February 14, 2026 20:13
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @kingpanther13, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a significant architectural change to optimize LLM interaction with Home Assistant tools. By implementing a server-side tool search proxy, it addresses the critical issue of excessive context token consumption caused by registering all tool definitions upfront. This new pattern allows for dynamic tool discovery and execution through a set of lightweight meta-tools, drastically reducing the LLM's idle context footprint and enabling compatibility with models that have stricter token limits. This initial phase proxies 10 tools, laying the groundwork for a complete migration.

Highlights

  • Context Token Reduction: Implemented a server-side Tool Search Proxy pattern to significantly reduce idle context token usage for LLMs by replacing direct registration of numerous tools with three lightweight meta-tools.
  • Dynamic Tool Discovery: Introduced three meta-tools: ha_find_tools for searching tools, ha_get_tool_details for retrieving full documentation and parameter schemas, and ha_execute_tool for validated tool execution.
  • Schema Enforcement: The ha_execute_tool meta-tool now requires a tool_schema hash obtained from ha_get_tool_details, structurally preventing LLMs from calling tools without first reviewing their documentation.
  • Phase 1 Implementation: Ten specific tools from five modules (tools_zones, tools_labels, tools_addons, tools_voice_assistant, tools_traces) have been migrated to the proxy, reducing the directly registered tool count by 7.
  • Migration Roadmap Established: Defined a clear, multi-phase roadmap for progressively migrating more tools to the proxy, with each subsequent phase requiring only a one-line configuration change.
Changelog
  • src/ha_mcp/tools/registry.py
    • Updated the module docstring to describe the new tool proxy mechanism.
    • Modified the register_all_tools method to identify and skip modules designated for proxying.
    • Integrated calls to discover_proxy_tools and register_proxy_tools to handle proxied modules and meta-tool registration.
    • Adjusted logging to reflect the number of modules handled by the proxy.
  • src/ha_mcp/tools/tool_proxy.py
    • Added a new module to implement the core logic of the tool search proxy.
    • Defined PROXY_MODULES to specify which tool modules are to be proxied.
    • Introduced ToolProxyRegistry for server-side storage of proxied tool metadata and implementations.
    • Included helper functions _extract_tool_metadata and _python_type_to_json for parsing tool function details.
    • Implemented discover_proxy_tools to import and capture tool metadata from proxied modules using a mock MCP.
    • Created _MockMCP to simulate MCP registration, capturing tool functions and their metadata without actual registration.
    • Defined register_proxy_tools to register the three meta-tools (ha_find_tools, ha_get_tool_details, ha_execute_tool) with the real MCP.
    • Implemented the logic for ha_find_tools to search for tools, ha_get_tool_details to retrieve detailed schema information with a hash, and ha_execute_tool to validate schema and execute proxied tools.
  • tests/src/unit/test_tool_proxy.py
    • Added a new file containing comprehensive unit tests for the tool proxy functionality.
    • Included tests for ToolProxyRegistry covering tool registration, searching, and detail retrieval, including schema hash generation and validation.
    • Provided tests for _MockMCP to ensure correct capturing of decorated functions and their metadata.
    • Implemented integration tests for the meta-tools (ha_find_tools, ha_get_tool_details, ha_execute_tool), verifying their functionality, schema enforcement, and error handling.
    • Added tests to validate the configuration and structure of PROXY_MODULES.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@kingpanther13
kingpanther13 marked this pull request as draft February 14, 2026 20:14

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a significant architectural improvement with the tool search proxy pattern, which will drastically reduce token usage. The implementation is well-thought-out and includes a good set of unit tests for the new proxy logic.

My review has identified a few key areas for improvement:

  • Error Handling: The new meta-tools currently use custom dictionary-based error returns. The repository style guide requires using the standardized structured error helpers from errors.py to ensure consistency. This is a high-severity issue.
  • Testing: While the unit tests are comprehensive, the style guide also requires E2E tests for new MCP tools. Adding automated E2E tests for these foundational meta-tools is critical and is flagged as a high-severity issue.
  • Code Style: There are minor code style issues, such as local imports within functions, which should be moved to the top of the file for better readability and adherence to PEP 8.

Comment thread src/ha_mcp/tools/tool_proxy.py
Comment thread src/ha_mcp/tools/tool_proxy.py Outdated
Comment thread src/ha_mcp/tools/tool_proxy.py Outdated
kingpanther13 and others added 7 commits February 14, 2026 15:39
- Fix ruff lint: I001 import sorting, SIM300 Yoda condition, PERF401 list.extend
- Use structured error helpers from errors.py in all meta-tool error paths
  (Gemini HIGH: create_resource_not_found_error, create_validation_error, etc.)
- Move hashlib/typing/types imports to top of file (Gemini MEDIUM: PEP 8)
- Update E2E tests to route proxied tools through ha_execute_tool
- Add proxy_helpers.py utility for transparent proxy routing in E2E tests
- Add E2E tests for meta-tools (Gemini HIGH: ha_find_tools, ha_get_tool_details,
  ha_execute_tool) covering search, details, execution, and error rejection
- Update unit test assertions for structured error format

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Update test_label_operations.py and test_lifecycle.py (labels)
  to use proxy_call_tool for ha_config_set/get/remove_label
- Update test_voice_assistant.py to use proxy_call_tool for
  ha_get_entity_exposure
- Update test_entity_rename.py to use proxy_call_tool for
  ha_get_entity_exposure
- Update test_lifecycle.py (zones) to use ProxyMCPAssertions
  for ha_get/create/update/delete_zone
- Make proxy_call_tool transparent: falls back to direct call
  for non-proxied tools, so it works as drop-in replacement
- Add ProxyMCPAssertions class for tests using MCPAssertions
  pattern with proxied tools

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…test

The valid_assistants field is stringified inside error.message when
routed through the tool proxy, so check both top-level and error string.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move the proxy-aware tool routing from individual test files into the
mcp_client fixture in conftest.py. This eliminates all modifications to
existing test files and means future proxy phases require zero test
changes — just add module names to PROXY_MODULES.

- Delete proxy_helpers.py (143 lines)
- Revert 7 test files to master (labels, zones, traces, voice, rename)
- Add _ProxyAwareClient wrapper in conftest.py with error unwrapping
- Trim tool_proxy.py docstrings and roadmap comments (-64 lines)

Previous state tagged as pre-trim-v1 for easy revert.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Unit tests: 365 -> 224 lines (DRY fixture, merged similar tests)
- E2E tests: 265 -> 133 lines (helper method, removed logger noise)
- ha_execute_tool: simplified args parsing and error messages

Same test coverage, same assertions, just concise.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Let ToolError propagate through proxy instead of wrapping it, so
  FastMCP handles isError flag correctly (fixes voice assistant test)
- Remove trailing whitespace on blank lines in test_traces.py (W293)
- Fix Yoda condition in test_tool_proxy.py (SIM300)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@kingpanther13

Copy link
Copy Markdown
Member Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a well-designed tool search proxy to significantly reduce token usage, which is a major architectural improvement. The implementation is robust, leveraging a mock MCP for non-invasive tool discovery and providing a transparent proxy for E2E tests. The code is well-documented and includes comprehensive unit and E2E tests. I have a few suggestions to improve maintainability and fix a couple of minor issues.

Comment thread src/ha_mcp/tools/tool_proxy.py
Comment thread src/ha_mcp/tools/tool_proxy.py Outdated
Comment thread src/ha_mcp/tools/tool_proxy.py Outdated
Comment thread tests/src/e2e/conftest.py Outdated
- 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>
@kingpanther13

Copy link
Copy Markdown
Member Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This is an excellent pull request that introduces a powerful pattern for managing a large number of tools, significantly reducing token usage. The implementation of the tool proxy with meta-tools (ha_find_tools, ha_get_tool_details, ha_execute_tool) is well-structured and follows best practices. The use of a mock MCP to discover tools without registering them is particularly clever. The changes to the test infrastructure, especially the _ProxyAwareClient, ensure that existing tests remain valid with minimal changes, which is a great example of thoughtful engineering. I've identified a bug in the type-to-JSON schema conversion and an opportunity to further improve the generated schemas for array types. Overall, this is a high-quality contribution that will greatly improve the scalability and usability of ha-mcp.

Comment thread src/ha_mcp/tools/tool_proxy.py Outdated
Comment thread src/ha_mcp/tools/tool_proxy.py
kingpanther13 and others added 2 commits February 14, 2026 17:50
Use origin type for type_map lookup so list[str] -> "array",
dict[str, int] -> "object", etc. instead of falling back to "string".

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add _get_array_items_schema() to enrich array-type parameters with
proper JSON Schema items (e.g. list[str] -> items: {type: string}).
Handles Union unwrapping for list[str] | None patterns. Needed for
future proxy phases where many tools use list[str] parameters.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@sergeykad

Copy link
Copy Markdown
Collaborator

Review: PR #627 — Tool Search Proxy for Context Reduction (Phase 1)

What the PR Does

This PR replaces direct MCP registration of 10 tools (from 5 modules: zones, labels, addons, voice_assistant, traces) with 3 meta-tools:

Meta-Tool Purpose
ha_find_tools(query) Search proxied tools by name, category, or keyword
ha_get_tool_details(tool_name) Return full schema and a schema_hash
ha_execute_tool(tool_name, args, tool_schema) Validate hash, then execute

The goal is to reduce idle context consumption. Today, 94+ tools occupy ~35,500 tokens — 17.8 % of Claude's window and 27.8 % of GPT-4o's. The problem is real and well-quantified.

MCP Compliance

The 3 meta-tools are valid MCP tools: they register normally, appear in tools/list, and are invoked via tools/call. At the wire-protocol level, nothing is broken. But the approach works against the protocol's design intent in four ways.

1. Tools vanish from tools/list

MCP designates tools/list as the discovery mechanism. Once proxied, the 10 underlying tools become invisible to every standard MCP host — Claude Desktop, Cursor, Copilot, or any other client that relies on tools/list. Those clients would need to know the custom find-then-details-then-execute workflow to reach the hidden tools.

2. Typed invocation is replaced by string dispatch

Each MCP tool normally carries its own inputSchema (JSON Schema), letting both client and LLM know exactly what parameters are expected. The proxy collapses this into:
ha_execute_tool(
tool_name="ha_create_zone",
args='{"name":"Home","latitude":40.7}',
tool_schema="a1b2c3d4"
)
args is an opaque JSON string. Neither the MCP client nor protocol-level validation can verify its structure before the call reaches the server.

3. Tool annotations are hidden

MCP annotations (readOnlyHint, destructiveHint, idempotentHint) let clients enforce safety policies — for example, requiring user confirmation for destructive operations. The proxy tracks these internally (exposing is_destructive in search results), but MCP clients cannot see them. ha_execute_tool carries no annotation reflecting the danger level of whatever it dispatches.

4. It rebuilds protocol-level features at the application layer

The PR constructs a bespoke discovery-and-invocation protocol on top of MCP. The spec already provides:

  • Pagination on tools/list for large tool sets.
  • notifications/tools/list_changed for dynamic tool sets.
  • SEP-1821 — an active draft proposal that adds a query parameter to tools/list, which is functionally identical to ha_find_tools.

Other Technical Concerns

Schema-hash gate offers false confidence. The hash proves the LLM called ha_get_tool_details, not that it understood the schema. An LLM can retrieve the hash and immediately call ha_execute_tool with wrong arguments. The 8-hex-character (32-bit) hash is also small enough to hallucinate.

_MockMCP is fragile. It mimics the SDK's @mcp.tool() decorator to intercept registrations. Any change to the SDK's decorator API will break the mock silently.

Extra round-trips. A single operation now requires 2–3 LLM turns (find, get details, execute), trading context tokens for latency and added reasoning burden.

Breaking change in practice. The PR states "zero breaking changes," but any MCP client that was calling ha_create_zone directly will find it gone from tools/list. That is a breaking change.

What's Done Well

  • Problem identification — clearly motivated, well-quantified.
  • Test coverage — 30+ unit tests, E2E tests, and a transparent _ProxyAwareClient wrapper so existing tests keep working.
  • Registry designToolProxyRegistry with search, catalog, and validation is cleanly structured.
  • Phased rollout — starting with 5 low-traffic modules is pragmatic.
  • No changes to existing tool implementations — a sound architectural constraint.

MCP-Aligned Alternatives

  1. Dynamic registration via listChanged. Register a base set, then add or remove tools as the conversation narrows. Every tool stays first-class with proper schemas and annotations.
  2. Adopt SEP-1821. Contribute to or implement the draft query parameter on tools/list — the same filtering this PR wants, but at the protocol level where all clients benefit.
  3. Multiple MCP servers. Split by domain (zones, labels, etc.). Clients already support multiple server connections.
  4. Rich annotations. Tag every tool with categories so clients can filter and prioritize on their side.
  5. Connection-time negotiation. Use a configuration tool or init-phase handshake so the client declares which domains it needs; register only those.

Summary

Dimension Assessment
Problem identification Strong — real and well-quantified
Wire-level MCP compliance Compliant — meta-tools are valid
Design-intent MCP alignment Misaligned — hides tools, strips annotations, replaces typed invocation
Code quality Good — clean registry, thorough tests
Architectural direction Concerning — parallel discovery protocol diverges from SEP-1821
Breaking changes Yes — 10 tools disappear from tools/list
Security model Weak — schema hash provides false confidence

Bottom line: Well-engineered code solving a real problem at the wrong layer. It builds a custom tool-discovery protocol on top of MCP instead of leveraging MCP's own mechanisms. The approach will cause compatibility issues with standard MCP clients and diverges from the direction the spec is heading. The project would be better served by adopting or contributing to protocol-level solutions for tool-set scaling.

@kingpanther13

kingpanther13 commented Feb 14, 2026

Copy link
Copy Markdown
Member Author

Thanks for the thorough review, Sergey. A few notes upfront, then I'll address each point.

This Is a Draft PR

This PR is marked Draft and was not ready for review. It's a working proof-of-concept for discussion, not a merge candidate. That said, I appreciate the detailed feedback — let me address every concern.

Why Only 10 Tools (Phase 1)

The phased approach is intentional. We're proxying 5 low-traffic modules (zones, labels, addons, voice_assistant, traces) to validate the pattern works end-to-end — correct schema extraction, error propagation, E2E test transparency — before expanding. If the approach is accepted, expanding to all tools is trivial (add module names to PROXY_MODULES). If it's better to go all-at-once, that's fine too.

The "Extra Round-Trips" Argument

This framing is misleading. Yes, a single proxied tool call takes 2-3 LLM turns instead of 1. But the math overwhelmingly favors the proxy:

Without proxy: 94 tools x ~375 tokens each = ~35,000 tokens of idle schema on every single turn, whether or not any tool is called.

With proxy (all tools proxied): 3 meta-tools x ~200 tokens = ~600 tokens idle. A find->details->execute flow adds ~1,500 tokens across 3 turns.

That's ~600 tokens idle + ~1,500 tokens per actual tool use vs ~35,000 tokens on every turn regardless. In a 20-turn conversation where the user invokes 3-4 tools, the proxy saves roughly 650K tokens over the session. The "extra round-trips" cost is noise compared to the idle context elimination.

This is exactly why Anthropic and Speakeasy independently recommend the meta-tool/progressive-disclosure pattern — and why Speakeasy measured 96-160x token reduction with essentially the same architecture.

Why listChanged Does Not Solve This

listChanged operates at the wrong layer. Here is why:

  1. Full re-fetch, no delta. When list_changed fires, the client calls tools/list and gets the entire current tool catalog. There is no diff mechanism. If you pare from 94 to 20 tools, the client still receives all 20 complete schemas.

  2. The LLM API is stateless. Every API call serializes all currently-registered tool schemas into the request. The LLM does not remember what tools it saw last turn. So even with listChanged, 20 tools x ~375 tokens = ~7,500 tokens on every turn — better than 35K, but the server has to somehow predict which 20 tools the user needs before the user asks.

  3. Server-side omniscience problem. The server does not know what the user will ask about. To dynamically expose the right tools, the MCP server would need to understand the conversation context — which it does not have. The LLM is the entity best positioned to discover tools on demand, which is exactly what ha_find_tools enables.

  4. Client support is uncertain and fragmented. It is unclear which MCP clients fully support listChanged — many clients and weaker LLMs may not handle dynamic tool list updates correctly or at all. The clients that do support it still re-serialize all visible tools to the LLM on every turn. The proxy pattern is guaranteed to work with any MCP client since the 3 meta-tools are standard MCP tools that appear in tools/list with proper schemas and annotations.

  5. Security concerns. VS Code's implementation resets all prior tool approvals when list_changed fires (to prevent rug-pull attacks). Dynamic tool swapping raises trust issues that the proxy pattern avoids entirely.

SEP-1821 Assessment

I researched this thoroughly. SEP-1821 proposes adding a query parameter to tools/list — functionally similar to ha_find_tools.

It is essentially dead:

  • Status: Dormant, no sponsor, labeled dormant on the PR
  • No MCP maintainers have engaged with the technical content
  • Zero formal reviews, zero approvals
  • The author himself acknowledges "ambiguity in the query parameter format"
  • The closely related SEP-1300 (tool filtering with groups/tags), which had far more discussion (59 comments, a sponsor, reference implementations), was rejected outright

Even if it were adopted, it has the same cold-start problem: the LLM cannot generate a meaningful query if it has never seen the tool catalog. And it requires every client to implement the query parameter — none do today.

I would happily adopt SEP-1821 if it were ratified and clients supported it. But building on a dormant, sponsorless proposal with no implementation timeline would be irresponsible. The proxy pattern works today, with every MCP client, without any spec changes.

Addressing Each Specific Concern

"Tools vanish from tools/list"

Correct — that is the point. The 10 proxied tools are low-traffic, niche tools (zones, labels, addons, voice assistant, traces). They are still fully accessible via the meta-tools. Any MCP client can call ha_find_tools("zone") and discover them. This is no different from how Speakeasy's dynamic toolsets work, or how Anthropic's own Tool Search Tool operates with defer_loading.

"Typed invocation replaced by string dispatch"

The args parameter is a JSON string, yes — but ha_execute_tool parses and validates it server-side before dispatch. The schema_hash ensures the LLM retrieved the current schema. This is the same pattern as any RPC dispatch layer. The trade-off (string args vs typed schema) is the cost of progressive disclosure, and it is the same trade-off every meta-tool pattern makes.

"Tool annotations are hidden"

ha_find_tools returns is_destructive in search results. ha_get_tool_details returns the full annotation set (destructiveHint, readOnlyHint, idempotentHint). The annotations are surfaced at discovery time. Additionally, ha_execute_tool itself is already annotated with destructiveHint: True as a conservative default — so MCP clients that enforce confirmation for destructive operations will still prompt the user.

"Schema hash provides false confidence"

The hash is not a security mechanism — it is a staleness check. It ensures the LLM is working with the current schema, not a hallucinated or cached one. Could an LLM hallucinate a valid hash? Theoretically, but it would also need to hallucinate valid args that pass server-side validation. The hash is defense-in-depth, not the only guard.

Update: Increased hash from 8 hex chars (32-bit) to 12 hex chars (48-bit / ~281 trillion values) to further reduce hallucination risk (commit 3665ace).

"_MockMCP is fragile"

Fair point. _MockMCP intercepts @mcp.tool() to capture registrations. If the SDK's decorator API changes, it could break.

Update: Added an SDK compatibility check in _MockMCP.__init__ that verifies FastMCP.tool() exists at import time. If the SDK changes, this fails fast with a warning instead of silently producing wrong metadata (commit 3665ace).

"Breaking change"

You are right that a client previously calling ha_create_zone directly would need to use the meta-tools. For this HA MCP server specifically, these tools have never been in a stable release with direct registration, so no existing client integration breaks. But the point is taken — updated the PR description to check the "Breaking change" box with a note explaining the scope.

The Alternatives

Alternative Assessment
listChanged Wrong layer, does not solve token bloat, requires server-side omniscience, fragmented client support
SEP-1821 Dormant, no sponsor, rejected predecessor, no implementation timeline
Multiple MCP servers Valid but orthogonal — splits tools but does not reduce total schema tokens across servers
Rich annotations Does not reduce tokens — annotations are additional metadata, not a filtering mechanism
Connection-time negotiation Interesting but no spec support, requires custom client-side implementation

Bottom Line

This PR implements a pattern that Anthropic recommends, Speakeasy independently validated (96-160x token reduction), and multiple open-source projects have adopted. The "extra round-trips" cost is a rounding error compared to 35K tokens of idle context per turn.

That said — this is a draft, and I am open to any architectural direction the maintainers prefer. If there is a protocol-level solution on the horizon that makes this unnecessary, I am happy to wait for it or contribute to it.

- Add FastMCP SDK compatibility check in _MockMCP.__init__ that warns
  if the SDK's tool() method is missing, failing fast on breaking changes
- Increase schema_hash from 8 to 12 hex chars (48 bits / 281 trillion
  values) to reduce LLM hallucination risk

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@kingpanther13

kingpanther13 commented Feb 14, 2026

Copy link
Copy Markdown
Member Author

One thing I should have mentioned above — listChanged was actually my first choice before building the proxy. Dynamic registration where every tool stays first-class with proper schemas and annotations would be the ideal solution. I abandoned it after discovering:

  1. Uncertain client support — it's unclear which MCP clients fully support listChanged. Many clients and weaker LLMs may not handle dynamic tool list updates correctly or at all. The support landscape is fragmented and poorly documented.
  2. The clients that DO support it still re-serialize all visible tools on every LLM turn — so it reduces the server-side tool list but not necessarily the context window impact
  3. The server doesn't know what the user will ask about, so it can't predict which tools to expose

I considered building a hybrid — detect listChanged support at init, use dynamic registration for compatible clients, fall back to proxy for others — but the complexity wasn't justified given the uncertain client support.

The proxy pattern is guaranteed to work with any MCP client. The 3 meta-tools register via standard @mcp.tool() — they appear in tools/list with proper inputSchema and annotations like any other tool. Any client that can call an MCP tool can use them, regardless of which LLM is behind it (Claude, GPT, Qwen, Llama, etc.).

If/when listChanged becomes universal and clients implement smart per-tool lazy loading, the proxy pattern becomes unnecessary and can be removed. The architecture is designed for that — PROXY_MODULES is a simple set that can be emptied.


Also pushed a commit (3665ace) addressing two code-level concerns from the review:

  • _MockMCP fragility: Added an SDK compatibility check in __init__ that verifies FastMCP.tool() exists at import time. If the SDK changes, this fails fast with a warning instead of silently producing wrong metadata.
  • Schema hash hallucination risk: Increased from 8 hex chars (32-bit, ~4 billion values) to 12 hex chars (48-bit, ~281 trillion values). Still compact but 65,536x harder to hallucinate.

Missed the E2E assertion when increasing schema_hash from 8 to 12 chars.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@kingpanther13

Copy link
Copy Markdown
Member Author

After doing some LLM testing I've determined that this one would actually work quite well if we converted all of the tools over at once, BUT doing a phased conversion definitely would completely break the addon. So it's all or nothing. I'm leaning toward #616 (if I get it optimized) since it can be phased.

However, I think this idea might still be useful when it comes to the thought of consolidating some tools especially lesser used ones, instead of consolidating everything into one mega group. I intend to group together some things using this method, like for example ha_config_set_helper and ha_create_config_entry_helper canCould wrap both behind a single ha_config_set_helper that routes internally based on type. The agent shouldn't need to know the difference between storage vs config-entry helpers.

So @julienld can I just completely rework this PR then make it ready for review once I have it figured out, or should I create a whole new PR just using this as a backbone? I think that it would still be able to play nice with #616 too. This would really be best for just reducing tool count drastically so we could add more tools in the future.

@julienld

Copy link
Copy Markdown
Member

Some thoughts: this could be developed as a general tool: a FastMCP tool search proxy or even a FastMCP plugin. It would work with any large MCP or collection of MCPs.

It might also benefit from semantic search : BM25, vectorization, ...

kingpanther13 added a commit to kingpanther13/ha-mcp-fork that referenced this pull request Feb 17, 2026
Replace direct MCP registration of lesser-used tools with domain-named
gateway tools that combine discovery and execution in a single step.

Phase 1 implements ha_manage_dashboards, which consolidates 12 dashboard
and resource tools behind one gateway. This reduces idle context tokens
while maintaining full functionality through 1-step access:

- gateway() → lists all sub-tools with full parameter schemas
- gateway(tool="ha_config_set_dashboard", args='{...}') → executes

This approach addresses discoverability issues found in PR homeassistant-ai#627's
3-step meta-tool pattern (ha_find_tools → ha_get_tool_details →
ha_execute_tool), where LLMs failed to discover proxied capabilities.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@kingpanther13

Copy link
Copy Markdown
Member Author

I'm going to go ahead and close it for now, opening #637 as a better option with similar concept.

kingpanther13 added a commit to kingpanther13/ha-mcp-fork that referenced this pull request Feb 22, 2026
Replace direct MCP registration of lesser-used tools with domain-named
gateway tools that combine discovery and execution in a single step.

Phase 1 implements ha_manage_dashboards, which consolidates 12 dashboard
and resource tools behind one gateway. This reduces idle context tokens
while maintaining full functionality through 1-step access:

- gateway() → lists all sub-tools with full parameter schemas
- gateway(tool="ha_config_set_dashboard", args='{...}') → executes

This approach addresses discoverability issues found in PR homeassistant-ai#627's
3-step meta-tool pattern (ha_find_tools → ha_get_tool_details →
ha_execute_tool), where LLMs failed to discover proxied capabilities.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
kingpanther13 added a commit to kingpanther13/ha-mcp-fork that referenced this pull request Mar 13, 2026
Replace direct MCP registration of lesser-used tools with domain-named
gateway tools that combine discovery and execution in a single step.

Phase 1 implements ha_manage_dashboards, which consolidates 12 dashboard
and resource tools behind one gateway. This reduces idle context tokens
while maintaining full functionality through 1-step access:

- gateway() → lists all sub-tools with full parameter schemas
- gateway(tool="ha_config_set_dashboard", args='{...}') → executes

This approach addresses discoverability issues found in PR homeassistant-ai#627's
3-step meta-tool pattern (ha_find_tools → ha_get_tool_details →
ha_execute_tool), where LLMs failed to discover proxied capabilities.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] error while connecting to openAI chat GPT

3 participants