ha-mcp-dev 7.4.1.dev414__tar.gz → 7.4.1.dev416__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 (105) hide show
  1. {ha_mcp_dev-7.4.1.dev414/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.4.1.dev416}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_addons.py +43 -2
  4. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_integrations.py +42 -2
  5. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_search.py +5 -0
  6. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_utility.py +109 -5
  7. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/util_helpers.py +81 -0
  8. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  9. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/LICENSE +0 -0
  10. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/MANIFEST.in +0 -0
  11. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/README.md +0 -0
  12. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/setup.cfg +0 -0
  13. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/__init__.py +0 -0
  14. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/__main__.py +0 -0
  15. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/_pypi_marker +0 -0
  16. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/_version.py +0 -0
  17. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/auth/__init__.py +0 -0
  18. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/auth/consent_form.py +0 -0
  19. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/auth/provider.py +0 -0
  20. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/client/__init__.py +0 -0
  21. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/client/rest_client.py +0 -0
  22. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/client/websocket_client.py +0 -0
  23. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/client/websocket_listener.py +0 -0
  24. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/config.py +0 -0
  25. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/errors.py +0 -0
  26. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/py.typed +0 -0
  27. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  28. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  29. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  30. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  31. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  32. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  33. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  34. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  35. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  36. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  37. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  38. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  39. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  40. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  41. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  42. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  43. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  44. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  45. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  46. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  47. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/server.py +0 -0
  48. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/settings_ui.py +0 -0
  49. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/smoke_test.py +0 -0
  50. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/__init__.py +0 -0
  51. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/backup.py +0 -0
  52. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  53. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/device_control.py +0 -0
  54. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/enhanced.py +0 -0
  55. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/helpers.py +0 -0
  56. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/reference_validator.py +0 -0
  57. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/registry.py +0 -0
  58. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/smart_search.py +0 -0
  59. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_areas.py +0 -0
  60. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  61. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  62. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_calendar.py +0 -0
  63. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_camera.py +0 -0
  64. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_categories.py +0 -0
  65. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  66. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  67. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
  68. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  69. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  70. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_energy.py +0 -0
  71. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_entities.py +0 -0
  72. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  73. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_groups.py +0 -0
  74. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_hacs.py +0 -0
  75. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_history.py +0 -0
  76. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_labels.py +0 -0
  77. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  78. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_registry.py +0 -0
  79. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_resources.py +0 -0
  80. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_service.py +0 -0
  81. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_services.py +0 -0
  82. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_system.py +0 -0
  83. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_todo.py +0 -0
  84. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_traces.py +0 -0
  85. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_updates.py +0 -0
  86. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  87. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  88. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_zones.py +0 -0
  89. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/transforms/__init__.py +0 -0
  90. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/transforms/categorized_search.py +0 -0
  91. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/utils/__init__.py +0 -0
  92. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/utils/config_hash.py +0 -0
  93. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/utils/domain_handlers.py +0 -0
  94. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  95. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/utils/operation_manager.py +0 -0
  96. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/utils/python_sandbox.py +0 -0
  97. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/utils/usage_logger.py +0 -0
  98. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  99. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  100. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  101. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  102. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  103. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/tests/__init__.py +0 -0
  104. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/tests/test_constants.py +0 -0
  105. {ha_mcp_dev-7.4.1.dev414 → ha_mcp_dev-7.4.1.dev416}/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.4.1.dev414
3
+ Version: 7.4.1.dev416
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ha-mcp-dev"
7
- version = "7.4.1.dev414"
7
+ version = "7.4.1.dev416"
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"
@@ -295,11 +295,50 @@ async def get_addon_info(client: HomeAssistantClient, slug: str) -> dict[str, An
295
295
 
296
296
  Returns:
297
297
  Dictionary with add-on details including ingress info, state, options, etc.
298
+ Top-level ``log_level`` is surfaced when the add-on exposes one via its
299
+ Supervisor options or schema (e.g., ``"debug"``, ``"info"``, etc.).
298
300
  """
299
301
  response = await _supervisor_api_call(client, f"/addons/{slug}/info")
300
302
  if not response.get("success"):
301
303
  return response # TODO(tech-debt): should raise ToolError per AGENTS.md Pattern B
302
- return {"success": True, "addon": response["result"]}
304
+
305
+ addon = response["result"] if isinstance(response["result"], dict) else {}
306
+ result: dict[str, Any] = {"success": True, "addon": addon}
307
+
308
+ log_level = _extract_addon_log_level(addon)
309
+ if log_level is not None:
310
+ result["log_level"] = log_level
311
+
312
+ return result
313
+
314
+
315
+ def _extract_addon_log_level(addon: dict[str, Any]) -> str | None:
316
+ """Return the add-on's configured log level, if any.
317
+
318
+ Checks the add-on's current options first (``options.log_level`` — what the
319
+ user set), then falls back to the schema (Supervisor serializes ``schema``
320
+ as a list of ``{name, type, ...}`` field descriptors) so add-ons that ship a
321
+ log_level option without a value still surface ``"default"``. Returns
322
+ ``None`` when the add-on exposes no log_level option at all.
323
+
324
+ The lower-case ``"default"`` is the literal Supervisor sentinel; the
325
+ integration path uses ``"DEFAULT"`` (uppercase) — these are distinct values
326
+ by design and should not be cross-compared.
327
+ """
328
+ options = addon.get("options")
329
+ if isinstance(options, dict):
330
+ level = options.get("log_level")
331
+ if isinstance(level, str) and level.strip():
332
+ return level
333
+
334
+ schema = addon.get("schema")
335
+ if isinstance(schema, list) and any(
336
+ isinstance(item, dict) and item.get("name") == "log_level"
337
+ for item in schema
338
+ ):
339
+ return "default"
340
+
341
+ return None
303
342
 
304
343
 
305
344
  async def list_addons(
@@ -1153,7 +1192,9 @@ def register_addon_tools(mcp: Any, client: HomeAssistantClient, **kwargs: Any) -
1153
1192
  **Note:** This tool only works with Home Assistant OS or Supervised installations.
1154
1193
 
1155
1194
  **SINGLE ADD-ON (slug provided):**
1156
- Returns comprehensive details including ingress entry, ports, options, and state.
1195
+ Returns comprehensive details including ingress entry, ports, options, state,
1196
+ and (when the add-on exposes one) a top-level ``log_level`` reflecting the
1197
+ current Supervisor option — useful for confirming ha_manage_addon log_level changes.
1157
1198
  Useful for discovering what APIs an add-on exposes before calling ha_manage_addon.
1158
1199
 
1159
1200
  **INSTALLED ADD-ONS (source='installed'):**
@@ -34,6 +34,7 @@ from .util_helpers import (
34
34
  build_pagination_metadata,
35
35
  coerce_bool_param,
36
36
  coerce_int_param,
37
+ get_logger_levels,
37
38
  wait_for_entity_removed,
38
39
  )
39
40
 
@@ -233,6 +234,19 @@ class IntegrationTools:
233
234
 
234
235
  STATES: 'loaded', 'setup_error', 'setup_retry', 'not_loaded',
235
236
  'failed_unload', 'migration_error'.
237
+
238
+ Each entry carries:
239
+
240
+ - ``log_level``: the canonical Python logger level name
241
+ (``DEBUG``/``INFO``/``WARNING``/``ERROR``/``CRITICAL``) when the
242
+ integration has a ``logger.set_level`` override, or ``"DEFAULT"``
243
+ (uppercase sentinel) when no override is set.
244
+ - ``log_level_raw``: the original numeric level (e.g. ``10`` for DEBUG)
245
+ when HA returned an int, ``None`` otherwise (no override set, or HA
246
+ provided a level name as a string).
247
+
248
+ This is distinct from the add-on side, where ``ha_get_addon`` returns
249
+ Supervisor's lowercase ``"default"`` literal — do not cross-compare.
236
250
  """
237
251
  try:
238
252
  include_opts = coerce_bool_param(
@@ -280,12 +294,21 @@ class IntegrationTools:
280
294
  """Fetch a single config entry by ID, optionally including its options schema."""
281
295
  try:
282
296
  result = await self._client.get_config_entry(entry_id)
297
+ entry_domain = result.get("domain") if isinstance(result, dict) else None
283
298
  resp: dict[str, Any] = {
284
299
  "success": True,
285
300
  "entry_id": entry_id,
286
301
  "entry": result,
287
302
  }
288
303
 
304
+ # Surface the effective Python logger level for this integration
305
+ # so users can confirm logger.set_level changes took effect.
306
+ # Emit unconditionally for symmetry with the list path (_format_entry).
307
+ logger_levels = await get_logger_levels(self._client)
308
+ level_info = logger_levels.get(entry_domain or "")
309
+ resp["log_level"] = level_info["name"] if level_info else "DEFAULT"
310
+ resp["log_level_raw"] = level_info["raw"] if level_info else None
311
+
289
312
  # Optionally fetch options flow schema (logically read-only: start+abort)
290
313
  if include_schema and result.get("supports_options"):
291
314
  await self._fetch_options_schema(entry_id, resp)
@@ -368,9 +391,12 @@ class IntegrationTools:
368
391
  e for e in entries if e.get("domain", "").lower() == domain_lower
369
392
  ]
370
393
 
394
+ # Fetch current logger levels once; enrich each entry with its effective level.
395
+ logger_levels = await get_logger_levels(self._client)
396
+
371
397
  # Format entries for response
372
398
  formatted_entries = [
373
- self._format_entry(entry, include_opts) for entry in entries
399
+ self._format_entry(entry, include_opts, logger_levels) for entry in entries
374
400
  ]
375
401
 
376
402
  # Apply search filter if query provided
@@ -403,7 +429,11 @@ class IntegrationTools:
403
429
  return result_data
404
430
 
405
431
  @staticmethod
406
- def _format_entry(entry: dict[str, Any], include_opts: bool | None) -> dict[str, Any]:
432
+ def _format_entry(
433
+ entry: dict[str, Any],
434
+ include_opts: bool | None,
435
+ logger_levels: dict[str, dict[str, Any]] | None = None,
436
+ ) -> dict[str, Any]:
407
437
  """Format a raw config entry into the response shape."""
408
438
  formatted_entry: dict[str, Any] = {
409
439
  "entry_id": entry.get("entry_id"),
@@ -416,6 +446,16 @@ class IntegrationTools:
416
446
  "disabled_by": entry.get("disabled_by"),
417
447
  }
418
448
 
449
+ # Surface the effective Python logger level for this integration
450
+ # ("DEFAULT" = no override; falls back to the root logger level).
451
+ # `log_level_raw` is the original numeric level (None when no override
452
+ # exists or HA returned a string instead of an int).
453
+ if logger_levels is not None:
454
+ domain = entry.get("domain") or ""
455
+ level_info = logger_levels.get(domain)
456
+ formatted_entry["log_level"] = level_info["name"] if level_info else "DEFAULT"
457
+ formatted_entry["log_level_raw"] = level_info["raw"] if level_info else None
458
+
419
459
  # Include options when requested (for auditing template definitions, etc.)
420
460
  if include_opts:
421
461
  formatted_entry["options"] = entry.get("options", {})
@@ -666,6 +666,11 @@ def register_search_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
666
666
  "safe_mode": config.get("safe_mode", False),
667
667
  "internal_url": config.get("internal_url"),
668
668
  "external_url": config.get("external_url"),
669
+ # No default: distinguish HA-not-exposing-the-key (None)
670
+ # from empty-allowlist ([]) — security-relevant for agents.
671
+ "allowlist_external_dirs": config.get(
672
+ "allowlist_external_dirs"
673
+ ),
669
674
  }
670
675
  )
671
676
  result["system_info"] = system_info
@@ -15,7 +15,12 @@ from fastmcp.exceptions import ToolError
15
15
  from ..client.rest_client import HomeAssistantAPIError, HomeAssistantConnectionError
16
16
  from ..errors import ErrorCode, create_error_response
17
17
  from .helpers import exception_to_structured_error, log_tool_usage, raise_tool_error
18
- from .util_helpers import add_timezone_metadata, coerce_bool_param, coerce_int_param
18
+ from .util_helpers import (
19
+ add_timezone_metadata,
20
+ coerce_bool_param,
21
+ coerce_int_param,
22
+ normalize_log_level,
23
+ )
19
24
 
20
25
  logger = logging.getLogger(__name__)
21
26
 
@@ -79,7 +84,7 @@ def register_utility_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
79
84
  )
80
85
  @log_tool_usage
81
86
  async def ha_get_logs(
82
- source: Literal["logbook", "system", "error_log", "supervisor"] = "logbook",
87
+ source: Literal["logbook", "system", "error_log", "supervisor", "logger"] = "logbook",
83
88
  # Shared parameters
84
89
  limit: int | str | None = None,
85
90
  search: str | None = None,
@@ -102,8 +107,9 @@ def register_utility_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
102
107
  - "system": Structured system log entries (errors, warnings) via system_log/list
103
108
  - "error_log": Raw home-assistant.log text
104
109
  - "supervisor": Add-on container logs (requires slug parameter)
110
+ - "logger": Effective log level per integration via logger/log_info (confirms logger.set_level changes took effect)
105
111
 
106
- **Shared params:** limit, search (keyword filter on entries/lines)
112
+ **Shared params:** limit, search (keyword filter on entries/lines; matches integration domain for source='logger')
107
113
  **Logbook params:** hours_back, entity_id, end_time, offset, compact (default True — strips attribute dicts to save context)
108
114
  **System/error_log params:** level (ERROR, WARNING, INFO, DEBUG)
109
115
  **Supervisor params:** slug (add-on slug, e.g. "core_mosquitto")
@@ -130,10 +136,10 @@ def register_utility_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
130
136
  f"Parameters {', '.join(ignored)} only apply to source='logbook'; "
131
137
  f"ignored for source='{source}'"
132
138
  )
133
- if source == "logbook" and level is not None:
139
+ if source in ("logbook", "logger", "supervisor") and level is not None:
134
140
  warnings.append(
135
141
  "Parameter 'level' only applies to source='system' or 'error_log'; "
136
- "ignored for source='logbook'"
142
+ f"ignored for source='{source}'"
137
143
  )
138
144
 
139
145
  # --- source="logbook" ---
@@ -173,6 +179,13 @@ def register_utility_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
173
179
  result["warnings"] = warnings
174
180
  return result
175
181
 
182
+ # --- source="logger" ---
183
+ if source == "logger":
184
+ result = await _get_logger_info(limit=limit, search=search)
185
+ if warnings:
186
+ result["warnings"] = warnings
187
+ return result
188
+
176
189
  # --- source="supervisor" ---
177
190
  # source == "supervisor" (Literal type guarantees this)
178
191
  if not slug:
@@ -508,6 +521,97 @@ def register_utility_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
508
521
  ],
509
522
  )
510
523
 
524
+ # ---- Logger info source ----
525
+
526
+ async def _get_logger_info(
527
+ limit: int | str | None = None,
528
+ search: str | None = None,
529
+ ) -> dict[str, Any]:
530
+ """Fetch per-integration log levels via the ``logger/log_info`` WS command."""
531
+ effective_limit = _coerce_limit(limit)
532
+
533
+ try:
534
+ result = await client.send_websocket_message({"type": "logger/log_info"})
535
+
536
+ if not result.get("success"):
537
+ raise_tool_error(
538
+ create_error_response(
539
+ ErrorCode.SERVICE_CALL_FAILED,
540
+ result.get("error", "Failed to retrieve logger info"),
541
+ suggestions=[
542
+ "Verify the 'logger' integration is enabled in Home Assistant",
543
+ "Check Home Assistant WebSocket connection",
544
+ ],
545
+ )
546
+ )
547
+
548
+ raw_entries = result.get("result", [])
549
+ if not isinstance(raw_entries, list):
550
+ raw_entries = []
551
+
552
+ loggers: list[dict[str, Any]] = []
553
+ for entry in raw_entries:
554
+ if not isinstance(entry, dict):
555
+ continue
556
+ domain = entry.get("domain")
557
+ if not isinstance(domain, str) or not domain:
558
+ continue
559
+ raw_level = entry.get("level")
560
+ level_name = normalize_log_level(raw_level)
561
+ if level_name is None:
562
+ continue
563
+ loggers.append(
564
+ {
565
+ "domain": domain,
566
+ "level": level_name,
567
+ "level_raw": raw_level if isinstance(raw_level, int) else None,
568
+ }
569
+ )
570
+
571
+ filters_applied: dict[str, str] = {}
572
+ if search:
573
+ search_lower = search.lower()
574
+ loggers = [
575
+ entry for entry in loggers
576
+ if search_lower in entry["domain"].lower()
577
+ ]
578
+ filters_applied["search"] = search
579
+
580
+ loggers.sort(key=lambda entry: entry["domain"])
581
+
582
+ total_entries = len(loggers)
583
+ loggers = loggers[:effective_limit]
584
+
585
+ data: dict[str, Any] = {
586
+ "success": True,
587
+ "source": "logger",
588
+ "loggers": loggers,
589
+ "total_entries": total_entries,
590
+ "returned_entries": len(loggers),
591
+ "limit": effective_limit,
592
+ }
593
+ if filters_applied:
594
+ data["filters_applied"] = filters_applied
595
+
596
+ return data
597
+
598
+ except ToolError:
599
+ raise
600
+ except (
601
+ HomeAssistantConnectionError,
602
+ HomeAssistantAPIError,
603
+ TimeoutError,
604
+ OSError,
605
+ ) as e:
606
+ exception_to_structured_error(
607
+ e,
608
+ context={"source": "logger"},
609
+ suggestions=[
610
+ "Check Home Assistant WebSocket connection",
611
+ "Verify the 'logger' integration is enabled",
612
+ ],
613
+ )
614
+
511
615
  # ---- Supervisor log source ----
512
616
 
513
617
  async def _get_supervisor_log(
@@ -268,6 +268,87 @@ def unwrap_service_response(result: dict[str, Any]) -> dict[str, Any]:
268
268
  return sr if isinstance(sr, dict) else result
269
269
 
270
270
 
271
+ # Python logging numeric-level → canonical level name.
272
+ # Mirrors the values in HA's LOGSEVERITY constant (components/logger/const.py).
273
+ _LOG_LEVEL_NAMES: dict[int, str] = {
274
+ 0: "NOTSET",
275
+ 10: "DEBUG",
276
+ 20: "INFO",
277
+ 30: "WARNING",
278
+ 40: "ERROR",
279
+ 50: "CRITICAL",
280
+ }
281
+
282
+
283
+ def normalize_log_level(level: Any) -> str | None:
284
+ """Normalize a numeric or string log level to its canonical uppercase name.
285
+
286
+ Returns None if the value can't be recognized as a log level.
287
+ """
288
+ if isinstance(level, bool): # bool is an int subclass — reject explicitly
289
+ return None
290
+ if isinstance(level, int):
291
+ return _LOG_LEVEL_NAMES.get(level, f"LEVEL_{level}")
292
+ if isinstance(level, str):
293
+ stripped = level.strip().upper()
294
+ if not stripped:
295
+ return None
296
+ return stripped
297
+ return None
298
+
299
+
300
+ async def get_logger_levels(client: Any) -> dict[str, dict[str, Any]]:
301
+ """Fetch current HA integration log levels via the ``logger/log_info`` WS command.
302
+
303
+ Returns a mapping of integration domain (e.g. ``"mqtt"``) to a dict with:
304
+
305
+ - ``name``: canonical level name (``"DEBUG"``, ``"INFO"``, ``"WARNING"``,
306
+ ``"ERROR"``, ``"CRITICAL"``, ``"NOTSET"``, or ``"LEVEL_<n>"`` for
307
+ non-standard ints).
308
+ - ``raw``: the original numeric level (``int``) when HA returned an int,
309
+ otherwise ``None`` (e.g. when the level was already provided as a string).
310
+
311
+ Best-effort enrichment: returns an empty dict on connection/IO failures so
312
+ callers can treat it as "no custom levels". Programming errors are not
313
+ suppressed — they surface as bugs during development/CI.
314
+ """
315
+ try:
316
+ result = await client.send_websocket_message({"type": "logger/log_info"})
317
+ except (
318
+ HomeAssistantConnectionError,
319
+ HomeAssistantAPIError,
320
+ HomeAssistantAuthError,
321
+ TimeoutError,
322
+ OSError,
323
+ ) as exc:
324
+ logger.debug("logger/log_info fetch failed: %s", exc)
325
+ return {}
326
+
327
+ if not isinstance(result, dict) or not result.get("success"):
328
+ return {}
329
+
330
+ entries = result.get("result", [])
331
+ if not isinstance(entries, list):
332
+ return {}
333
+
334
+ levels: dict[str, dict[str, Any]] = {}
335
+ for entry in entries:
336
+ if not isinstance(entry, dict):
337
+ continue
338
+ domain = entry.get("domain")
339
+ if not isinstance(domain, str) or not domain:
340
+ continue
341
+ raw_level = entry.get("level")
342
+ name = normalize_log_level(raw_level)
343
+ if name is None:
344
+ continue
345
+ levels[domain] = {
346
+ "name": name,
347
+ "raw": raw_level if isinstance(raw_level, int) and not isinstance(raw_level, bool) else None,
348
+ }
349
+ return levels
350
+
351
+
271
352
  async def add_timezone_metadata(client: Any, data: dict[str, Any]) -> dict[str, Any]:
272
353
  """Add Home Assistant timezone to tool responses for local time context."""
273
354
  try:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.4.1.dev414
3
+ Version: 7.4.1.dev416
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