ha-mcp-dev 7.5.0.dev520__tar.gz → 7.5.0.dev521__tar.gz

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.
Files changed (111) hide show
  1. {ha_mcp_dev-7.5.0.dev520/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.5.0.dev521}/PKG-INFO +38 -1
  2. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/README.md +37 -0
  3. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/pyproject.toml +1 -1
  4. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/tools/tools_search.py +44 -11
  5. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/tools/tools_system.py +45 -8
  6. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/tools/util_helpers.py +41 -0
  7. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521/src/ha_mcp_dev.egg-info}/PKG-INFO +38 -1
  8. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/LICENSE +0 -0
  9. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/MANIFEST.in +0 -0
  10. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/setup.cfg +0 -0
  11. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/__init__.py +0 -0
  12. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/__main__.py +0 -0
  13. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/_pypi_marker +0 -0
  14. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/_version.py +0 -0
  15. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/auth/__init__.py +0 -0
  16. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/auth/consent_form.py +0 -0
  17. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/auth/provider.py +0 -0
  18. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/client/__init__.py +0 -0
  19. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/client/rest_client.py +0 -0
  20. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/client/supervisor_client.py +0 -0
  21. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/client/websocket_client.py +0 -0
  22. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/client/websocket_listener.py +0 -0
  23. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/config.py +0 -0
  24. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/errors.py +0 -0
  25. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/py.typed +0 -0
  26. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  27. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  28. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  29. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  30. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  31. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  32. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  33. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  34. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  35. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  36. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  37. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  38. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  39. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  40. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  41. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  42. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  43. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  44. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  45. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  46. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/server.py +0 -0
  47. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/settings_ui.py +0 -0
  48. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/smoke_test.py +0 -0
  49. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/tools/__init__.py +0 -0
  50. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/tools/backup.py +0 -0
  51. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  52. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/tools/device_control.py +0 -0
  53. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/tools/enhanced.py +0 -0
  54. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/tools/helpers.py +0 -0
  55. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/tools/reference_validator.py +0 -0
  56. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/tools/registry.py +0 -0
  57. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/tools/smart_search.py +0 -0
  58. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/tools/tools_addons.py +0 -0
  59. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/tools/tools_areas.py +0 -0
  60. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  61. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  62. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/tools/tools_calendar.py +0 -0
  63. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/tools/tools_camera.py +0 -0
  64. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/tools/tools_categories.py +0 -0
  65. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/tools/tools_code.py +0 -0
  66. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  67. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  68. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
  69. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  70. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  71. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  72. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/tools/tools_energy.py +0 -0
  73. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/tools/tools_entities.py +0 -0
  74. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  75. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/tools/tools_groups.py +0 -0
  76. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/tools/tools_hacs.py +0 -0
  77. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/tools/tools_history.py +0 -0
  78. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/tools/tools_integrations.py +0 -0
  79. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/tools/tools_labels.py +0 -0
  80. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  81. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/tools/tools_registry.py +0 -0
  82. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/tools/tools_resources.py +0 -0
  83. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/tools/tools_service.py +0 -0
  84. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/tools/tools_services.py +0 -0
  85. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/tools/tools_todo.py +0 -0
  86. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/tools/tools_traces.py +0 -0
  87. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/tools/tools_updates.py +0 -0
  88. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/tools/tools_utility.py +0 -0
  89. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  90. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  91. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/tools/tools_zones.py +0 -0
  92. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/transforms/__init__.py +0 -0
  93. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/transforms/categorized_search.py +0 -0
  94. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  95. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/utils/__init__.py +0 -0
  96. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/utils/config_hash.py +0 -0
  97. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/utils/data_paths.py +0 -0
  98. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/utils/domain_handlers.py +0 -0
  99. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  100. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  101. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/utils/operation_manager.py +0 -0
  102. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/utils/python_sandbox.py +0 -0
  103. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp/utils/usage_logger.py +0 -0
  104. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  105. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  106. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  107. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  108. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  109. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/tests/__init__.py +0 -0
  110. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/tests/test_constants.py +0 -0
  111. {ha_mcp_dev-7.5.0.dev520 → ha_mcp_dev-7.5.0.dev521}/tests/test_env_manager.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.5.0.dev520
3
+ Version: 7.5.0.dev521
4
4
  Summary: Home Assistant MCP Server - Complete control of Home Assistant through MCP
5
5
  Author-email: Julien <github@qc-h.net>
6
6
  License: MIT
@@ -264,6 +264,43 @@ Skills can still be installed manually for clients that prefer local skill files
264
264
 
265
265
  ---
266
266
 
267
+ ## 🔍 Tool Discovery for AI Agents
268
+
269
+ By default, the full tool catalog (~86 tools) is listed to the client through the standard MCP `tools/list` response. Clients with deferred / on-demand tool loading (Claude Sonnet, Claude Opus,) handle that fine — tools are pulled into context only when needed, so idle context cost is near-zero.
270
+
271
+ For models *without* deferred tool support — Claude Haiku, Gemini, ChatGPT OpenAI-compatible local models, smaller open-weights models — listing 86 tools eats ~46K tokens of idle context. To address that, the server ships with a **search-based discovery mode** built on top of FastMCP's BM25 search transform.
272
+
273
+ ### Enable search-based discovery
274
+
275
+ Set ENABLE_TOOL_SEARCH=true (or toggle the option in the HA add-on). The full catalog is replaced in the tool list with four entry points plus a small set of always-visible "pinned" tools (ha_search_entities, ha_get_overview, ha_restart, etc.). All tools remain callable directly by name once discovered:
276
+
277
+ | Tool | Purpose |
278
+ |------|---------|
279
+ | `ha_search_tools` | BM25 keyword search across all tools. Returns name, description, parameters, and annotations (`readOnlyHint` / `destructiveHint`) so the agent can pick the right one. |
280
+ | `ha_call_read_tool` | Execute a `readOnlyHint` tool by name. Safe — clients can auto-approve. |
281
+ | `ha_call_write_tool` | Execute a write tool that creates or updates data. |
282
+ | `ha_call_delete_tool` | Execute a tool that removes / deletes data. |
283
+
284
+ The proxy split lets MCP clients apply different permission policies per category (e.g. auto-approve reads, prompt for writes, confirm deletes) without parsing tool docstrings.
285
+
286
+ | Setting | Default | Description |
287
+ |---------|---------|-------------|
288
+ | `ENABLE_TOOL_SEARCH` | `false` | Replace full tool catalog with search-based discovery (~46K → ~5K idle tokens). |
289
+ | `TOOL_SEARCH_MAX_RESULTS` | `5` | Max results returned by `ha_search_tools` (range 2–10). |
290
+ | `PINNED_TOOLS` | empty | Comma-separated tool names to keep always visible. The web settings UI is the primary way to manage this. |
291
+
292
+ ### When to enable
293
+
294
+ - **Claude Haiku, OpenAI-compatible local models, Gemini, ChatGPT or any model without native deferred tool support** — large idle-context savings.
295
+ - MCP clients that cap total tool count (some cap at 100) — surfaces a minimal set (~10 tools) instead of 86.
296
+ - **Cost-sensitive deployments** — fewer idle tokens per turn.
297
+
298
+ Leave it off when using Claude Sonnet/Opus or any client with deferred tool loading; the full catalog has no idle cost there and direct calls skip the search step. If you choose to use our toolsearch then you should disable the native Claude Opus/Sonnet toolsearch, which is called deferred tools in the settings.
299
+
300
+ For the HA add-on, the same option is documented in [`homeassistant-addon/DOCS.md`](homeassistant-addon/DOCS.md#enable_tool_search) along with the in-add-on settings UI for fine-grained tool enable/disable/pin.
301
+
302
+ ---
303
+
267
304
  ## 🧪 Dev Channel
268
305
 
269
306
  Want early access to new features and fixes? Dev releases (`.devN`) are published on every push to master.
@@ -234,6 +234,43 @@ Skills can still be installed manually for clients that prefer local skill files
234
234
 
235
235
  ---
236
236
 
237
+ ## 🔍 Tool Discovery for AI Agents
238
+
239
+ By default, the full tool catalog (~86 tools) is listed to the client through the standard MCP `tools/list` response. Clients with deferred / on-demand tool loading (Claude Sonnet, Claude Opus,) handle that fine — tools are pulled into context only when needed, so idle context cost is near-zero.
240
+
241
+ For models *without* deferred tool support — Claude Haiku, Gemini, ChatGPT OpenAI-compatible local models, smaller open-weights models — listing 86 tools eats ~46K tokens of idle context. To address that, the server ships with a **search-based discovery mode** built on top of FastMCP's BM25 search transform.
242
+
243
+ ### Enable search-based discovery
244
+
245
+ Set ENABLE_TOOL_SEARCH=true (or toggle the option in the HA add-on). The full catalog is replaced in the tool list with four entry points plus a small set of always-visible "pinned" tools (ha_search_entities, ha_get_overview, ha_restart, etc.). All tools remain callable directly by name once discovered:
246
+
247
+ | Tool | Purpose |
248
+ |------|---------|
249
+ | `ha_search_tools` | BM25 keyword search across all tools. Returns name, description, parameters, and annotations (`readOnlyHint` / `destructiveHint`) so the agent can pick the right one. |
250
+ | `ha_call_read_tool` | Execute a `readOnlyHint` tool by name. Safe — clients can auto-approve. |
251
+ | `ha_call_write_tool` | Execute a write tool that creates or updates data. |
252
+ | `ha_call_delete_tool` | Execute a tool that removes / deletes data. |
253
+
254
+ The proxy split lets MCP clients apply different permission policies per category (e.g. auto-approve reads, prompt for writes, confirm deletes) without parsing tool docstrings.
255
+
256
+ | Setting | Default | Description |
257
+ |---------|---------|-------------|
258
+ | `ENABLE_TOOL_SEARCH` | `false` | Replace full tool catalog with search-based discovery (~46K → ~5K idle tokens). |
259
+ | `TOOL_SEARCH_MAX_RESULTS` | `5` | Max results returned by `ha_search_tools` (range 2–10). |
260
+ | `PINNED_TOOLS` | empty | Comma-separated tool names to keep always visible. The web settings UI is the primary way to manage this. |
261
+
262
+ ### When to enable
263
+
264
+ - **Claude Haiku, OpenAI-compatible local models, Gemini, ChatGPT or any model without native deferred tool support** — large idle-context savings.
265
+ - MCP clients that cap total tool count (some cap at 100) — surfaces a minimal set (~10 tools) instead of 86.
266
+ - **Cost-sensitive deployments** — fewer idle tokens per turn.
267
+
268
+ Leave it off when using Claude Sonnet/Opus or any client with deferred tool loading; the full catalog has no idle cost there and direct calls skip the search step. If you choose to use our toolsearch then you should disable the native Claude Opus/Sonnet toolsearch, which is called deferred tools in the settings.
269
+
270
+ For the HA add-on, the same option is documented in [`homeassistant-addon/DOCS.md`](homeassistant-addon/DOCS.md#enable_tool_search) along with the in-add-on settings UI for fine-grained tool enable/disable/pin.
271
+
272
+ ---
273
+
237
274
  ## 🧪 Dev Channel
238
275
 
239
276
  Want early access to new features and fixes? Dev releases (`.devN`) are published on every push to master.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ha-mcp-dev"
7
- version = "7.5.0.dev520"
7
+ version = "7.5.0.dev521"
8
8
  description = "Home Assistant MCP Server - Complete control of Home Assistant through MCP"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13,<3.14"
@@ -22,7 +22,9 @@ from .util_helpers import (
22
22
  build_pagination_metadata,
23
23
  coerce_bool_param,
24
24
  coerce_int_param,
25
+ filter_active_repairs,
25
26
  parse_string_list_param,
27
+ project_repair_fields,
26
28
  public_fields,
27
29
  )
28
30
 
@@ -852,6 +854,16 @@ def register_search_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
852
854
  description="Include active persistent notifications (default: True). Set False to skip.",
853
855
  ),
854
856
  ] = True,
857
+ include_dismissed_repairs: Annotated[
858
+ bool | str | None,
859
+ Field(
860
+ default=False,
861
+ description=(
862
+ "Include user-dismissed/ignored repairs (default: False). "
863
+ "Matches the HA Repairs UI which hides dismissed items by default."
864
+ ),
865
+ ),
866
+ ] = False,
855
867
  ) -> dict[str, Any]:
856
868
  """Get AI-friendly system overview with intelligent categorization.
857
869
 
@@ -873,6 +885,13 @@ def register_search_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
873
885
  include_notifications_bool = coerce_bool_param(
874
886
  include_notifications, "include_notifications", default=True
875
887
  )
888
+ include_dismissed_repairs_bool = bool(
889
+ coerce_bool_param(
890
+ include_dismissed_repairs,
891
+ "include_dismissed_repairs",
892
+ default=False,
893
+ )
894
+ )
876
895
 
877
896
  # Parse domains filter
878
897
  parsed_domains = parse_string_list_param(domains, "domains", allow_csv=True)
@@ -951,25 +970,39 @@ def register_search_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
951
970
  except Exception as e:
952
971
  logger.warning(f"Failed to fetch notifications for overview: {e}")
953
972
 
954
- # Include active repair issues
973
+ # Active repairs only by default — matches the HA Repairs UI so agents
974
+ # don't chase problems the user already dismissed.
955
975
  result["repair_count"] = 0
956
976
  try:
957
977
  repairs_result = await client.send_websocket_message(
958
978
  {"type": "repairs/list_issues"}
959
979
  )
960
980
  if repairs_result.get("success"):
961
- issues = repairs_result.get("result", {}).get("issues", [])
962
- result["repair_count"] = len(issues)
963
- if issues:
981
+ all_issues = repairs_result.get("result", {}).get("issues", [])
982
+ visible_issues = filter_active_repairs(
983
+ all_issues,
984
+ include_dismissed=include_dismissed_repairs_bool,
985
+ )
986
+ result["repair_count"] = len(visible_issues)
987
+ if not include_dismissed_repairs_bool:
988
+ dismissed_count = len(all_issues) - len(visible_issues)
989
+ if dismissed_count:
990
+ result["dismissed_repair_count"] = dismissed_count
991
+ if visible_issues:
964
992
  result["repairs"] = [
965
- {
966
- "issue_id": r.get("issue_id"),
967
- "domain": r.get("domain"),
968
- "severity": r.get("severity"),
969
- "translation_key": r.get("translation_key"),
970
- }
971
- for r in issues
993
+ project_repair_fields(r) for r in visible_issues
972
994
  ]
995
+ else:
996
+ err = repairs_result.get("error") or {}
997
+ err_msg = (
998
+ err.get("message")
999
+ if isinstance(err, dict)
1000
+ else str(err)
1001
+ ) or "unknown error"
1002
+ logger.warning(
1003
+ "repairs/list_issues returned success=false: %s", err_msg
1004
+ )
1005
+ result["repairs_error"] = f"Could not fetch repairs: {err_msg}"
973
1006
  except Exception as e:
974
1007
  logger.warning("Failed to fetch repairs for overview: %s", e)
975
1008
  result["repairs_error"] = f"Could not fetch repairs: {e}"
@@ -21,7 +21,7 @@ from .helpers import (
21
21
  raise_tool_error,
22
22
  register_tool_methods,
23
23
  )
24
- from .util_helpers import coerce_bool_param
24
+ from .util_helpers import coerce_bool_param, filter_active_repairs
25
25
 
26
26
  logger = logging.getLogger(__name__)
27
27
 
@@ -339,6 +339,7 @@ class SystemTools:
339
339
  async def ha_get_system_health(
340
340
  self,
341
341
  include: str | None = None,
342
+ include_dismissed_repairs: bool | str | None = False,
342
343
  ) -> dict[str, Any]:
343
344
  """
344
345
  Get Home Assistant system health, including Zigbee (ZHA) and Z-Wave JS network diagnostics.
@@ -348,13 +349,21 @@ class SystemTools:
348
349
 
349
350
  **Parameters:**
350
351
  - include: Optional comma-separated list of additional data to include.
351
- - "repairs": Repair items from Settings > System > Repairs
352
+ - "repairs": Repair items from Settings > System > Repairs (active only by default; pass `include_dismissed_repairs=True` for all)
352
353
  - "zha_network": ZHA Zigbee devices with radio signal summary (name, LQI, RSSI)
353
354
  - "zha_network_full": ZHA Zigbee devices with all device details (can be large on 100+ device networks; prefer "zha_network" for summary)
354
355
  - "zwave_network": Z-Wave JS network status and node summary (status, security, routing)
355
356
  - Example: include="repairs,zha_network,zwave_network"
357
+ - include_dismissed_repairs: Include user-dismissed/ignored repairs (default: False). Only meaningful when "repairs" is in `include`.
356
358
  """
357
359
  includes = self._parse_includes(include)
360
+ include_dismissed_repairs_bool = bool(
361
+ coerce_bool_param(
362
+ include_dismissed_repairs,
363
+ "include_dismissed_repairs",
364
+ default=False,
365
+ )
366
+ )
358
367
 
359
368
  ws_client = None
360
369
 
@@ -369,7 +378,10 @@ class SystemTools:
369
378
 
370
379
  # Fetch optional sections
371
380
  if "repairs" in includes:
372
- result["repairs"] = await self._fetch_repairs(ws_client)
381
+ result["repairs"] = await self._fetch_repairs(
382
+ ws_client,
383
+ include_dismissed=include_dismissed_repairs_bool,
384
+ )
373
385
 
374
386
  zha_full = "zha_network_full" in includes
375
387
  zha_summary = "zha_network" in includes
@@ -454,17 +466,42 @@ class SystemTools:
454
466
 
455
467
 
456
468
  @staticmethod
457
- async def _fetch_repairs(ws_client: Any) -> dict[str, Any]:
458
- """Fetch repair issues from Home Assistant."""
469
+ async def _fetch_repairs(
470
+ ws_client: Any, *, include_dismissed: bool = False
471
+ ) -> dict[str, Any]:
472
+ """Fetch repair issues from Home Assistant.
473
+
474
+ Filters out user-dismissed ("ignored") repairs by default to match the
475
+ HA Repairs UI. Pass ``include_dismissed=True`` to return all issues
476
+ and report the dismissed count alongside the active count.
477
+ """
459
478
  repairs: dict[str, Any] = {"issues": [], "count": 0}
460
479
  try:
461
480
  repairs_result = await ws_client.send_command("repairs/list_issues")
462
481
  if repairs_result.get("success"):
463
- repairs_list = repairs_result.get("result", {}).get("issues", [])
482
+ all_issues = repairs_result.get("result", {}).get("issues", [])
483
+ visible_issues = filter_active_repairs(
484
+ all_issues, include_dismissed=include_dismissed
485
+ )
464
486
  repairs = {
465
- "issues": repairs_list,
466
- "count": len(repairs_list),
487
+ "issues": visible_issues,
488
+ "count": len(visible_issues),
467
489
  }
490
+ if not include_dismissed:
491
+ dismissed_count = len(all_issues) - len(visible_issues)
492
+ if dismissed_count:
493
+ repairs["dismissed_count"] = dismissed_count
494
+ else:
495
+ err = repairs_result.get("error") or {}
496
+ err_msg = (
497
+ err.get("message")
498
+ if isinstance(err, dict)
499
+ else str(err)
500
+ ) or "unknown error"
501
+ logger.warning(
502
+ "repairs/list_issues returned success=false: %s", err_msg
503
+ )
504
+ repairs["error"] = f"Repairs data not available: {err_msg}"
468
505
  except Exception as e:
469
506
  logger.warning("Failed to fetch repairs: %s", e)
470
507
  repairs["error"] = f"Repairs data not available: {e}"
@@ -315,6 +315,47 @@ def unwrap_service_response(result: dict[str, Any]) -> dict[str, Any]:
315
315
  return sr if isinstance(sr, dict) else result
316
316
 
317
317
 
318
+ # Fields surfaced from each repair issue. Includes `ignored` / `dismissed_version`
319
+ # so callers can distinguish active vs. user-dismissed repairs when both are
320
+ # returned (e.g., `include_dismissed_repairs=True`).
321
+ _REPAIR_PROJECTION_FIELDS = (
322
+ "issue_id",
323
+ "domain",
324
+ "severity",
325
+ "translation_key",
326
+ "ignored",
327
+ "dismissed_version",
328
+ "is_fixable",
329
+ "breaks_in_ha_version",
330
+ "created",
331
+ "issue_domain",
332
+ )
333
+
334
+
335
+ def filter_active_repairs(
336
+ issues: list[dict[str, Any]], *, include_dismissed: bool = False
337
+ ) -> list[dict[str, Any]]:
338
+ """Drop user-dismissed repairs unless ``include_dismissed`` is set.
339
+
340
+ HA's `repairs/list_issues` returns both active and ignored repairs (the
341
+ Repairs UI hides ignored ones by default). Mirror that UI default so
342
+ overview / system-health responses don't surface repairs the user has
343
+ already dismissed.
344
+ """
345
+ if include_dismissed:
346
+ return list(issues)
347
+ return [r for r in issues if not r.get("ignored")]
348
+
349
+
350
+ def project_repair_fields(issue: dict[str, Any]) -> dict[str, Any]:
351
+ """Project a repair issue dict to the public-facing field subset.
352
+
353
+ Drops verbose fields (`translation_placeholders`, `learn_more_url`) to
354
+ keep overview payloads compact.
355
+ """
356
+ return {k: issue[k] for k in _REPAIR_PROJECTION_FIELDS if k in issue}
357
+
358
+
318
359
  # Python logging numeric-level → canonical level name.
319
360
  # Mirrors the values in HA's LOGSEVERITY constant (components/logger/const.py).
320
361
  _LOG_LEVEL_NAMES: dict[int, str] = {
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.5.0.dev520
3
+ Version: 7.5.0.dev521
4
4
  Summary: Home Assistant MCP Server - Complete control of Home Assistant through MCP
5
5
  Author-email: Julien <github@qc-h.net>
6
6
  License: MIT
@@ -264,6 +264,43 @@ Skills can still be installed manually for clients that prefer local skill files
264
264
 
265
265
  ---
266
266
 
267
+ ## 🔍 Tool Discovery for AI Agents
268
+
269
+ By default, the full tool catalog (~86 tools) is listed to the client through the standard MCP `tools/list` response. Clients with deferred / on-demand tool loading (Claude Sonnet, Claude Opus,) handle that fine — tools are pulled into context only when needed, so idle context cost is near-zero.
270
+
271
+ For models *without* deferred tool support — Claude Haiku, Gemini, ChatGPT OpenAI-compatible local models, smaller open-weights models — listing 86 tools eats ~46K tokens of idle context. To address that, the server ships with a **search-based discovery mode** built on top of FastMCP's BM25 search transform.
272
+
273
+ ### Enable search-based discovery
274
+
275
+ Set ENABLE_TOOL_SEARCH=true (or toggle the option in the HA add-on). The full catalog is replaced in the tool list with four entry points plus a small set of always-visible "pinned" tools (ha_search_entities, ha_get_overview, ha_restart, etc.). All tools remain callable directly by name once discovered:
276
+
277
+ | Tool | Purpose |
278
+ |------|---------|
279
+ | `ha_search_tools` | BM25 keyword search across all tools. Returns name, description, parameters, and annotations (`readOnlyHint` / `destructiveHint`) so the agent can pick the right one. |
280
+ | `ha_call_read_tool` | Execute a `readOnlyHint` tool by name. Safe — clients can auto-approve. |
281
+ | `ha_call_write_tool` | Execute a write tool that creates or updates data. |
282
+ | `ha_call_delete_tool` | Execute a tool that removes / deletes data. |
283
+
284
+ The proxy split lets MCP clients apply different permission policies per category (e.g. auto-approve reads, prompt for writes, confirm deletes) without parsing tool docstrings.
285
+
286
+ | Setting | Default | Description |
287
+ |---------|---------|-------------|
288
+ | `ENABLE_TOOL_SEARCH` | `false` | Replace full tool catalog with search-based discovery (~46K → ~5K idle tokens). |
289
+ | `TOOL_SEARCH_MAX_RESULTS` | `5` | Max results returned by `ha_search_tools` (range 2–10). |
290
+ | `PINNED_TOOLS` | empty | Comma-separated tool names to keep always visible. The web settings UI is the primary way to manage this. |
291
+
292
+ ### When to enable
293
+
294
+ - **Claude Haiku, OpenAI-compatible local models, Gemini, ChatGPT or any model without native deferred tool support** — large idle-context savings.
295
+ - MCP clients that cap total tool count (some cap at 100) — surfaces a minimal set (~10 tools) instead of 86.
296
+ - **Cost-sensitive deployments** — fewer idle tokens per turn.
297
+
298
+ Leave it off when using Claude Sonnet/Opus or any client with deferred tool loading; the full catalog has no idle cost there and direct calls skip the search step. If you choose to use our toolsearch then you should disable the native Claude Opus/Sonnet toolsearch, which is called deferred tools in the settings.
299
+
300
+ For the HA add-on, the same option is documented in [`homeassistant-addon/DOCS.md`](homeassistant-addon/DOCS.md#enable_tool_search) along with the in-add-on settings UI for fine-grained tool enable/disable/pin.
301
+
302
+ ---
303
+
267
304
  ## 🧪 Dev Channel
268
305
 
269
306
  Want early access to new features and fixes? Dev releases (`.devN`) are published on every push to master.