ha-mcp-dev 7.2.0.dev320__tar.gz → 7.2.0.dev322__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 (100) hide show
  1. {ha_mcp_dev-7.2.0.dev320/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.2.0.dev322}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/pyproject.toml +2 -1
  3. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_integrations.py +37 -32
  4. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_registry.py +106 -58
  5. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_search.py +26 -18
  6. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_services.py +105 -44
  7. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_yaml_config.py +2 -2
  8. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/util_helpers.py +67 -24
  9. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  10. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/LICENSE +0 -0
  11. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/MANIFEST.in +0 -0
  12. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/README.md +0 -0
  13. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/setup.cfg +0 -0
  14. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/__init__.py +0 -0
  15. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/__main__.py +0 -0
  16. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/_pypi_marker +0 -0
  17. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/auth/__init__.py +0 -0
  18. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/auth/consent_form.py +0 -0
  19. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/auth/provider.py +0 -0
  20. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/client/__init__.py +0 -0
  21. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/client/rest_client.py +0 -0
  22. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/client/websocket_client.py +0 -0
  23. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/client/websocket_listener.py +0 -0
  24. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/config.py +0 -0
  25. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/errors.py +0 -0
  26. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/py.typed +0 -0
  27. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  28. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  29. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  30. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  31. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  32. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  33. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  34. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  35. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  36. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  37. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  38. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  39. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  40. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  41. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  42. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  43. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  44. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  45. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  46. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  47. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/server.py +0 -0
  48. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/smoke_test.py +0 -0
  49. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/__init__.py +0 -0
  50. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/backup.py +0 -0
  51. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  52. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/device_control.py +0 -0
  53. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/enhanced.py +0 -0
  54. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/helpers.py +0 -0
  55. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/registry.py +0 -0
  56. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/smart_search.py +0 -0
  57. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_addons.py +0 -0
  58. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_areas.py +0 -0
  59. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  60. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  61. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_calendar.py +0 -0
  62. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_camera.py +0 -0
  63. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_categories.py +0 -0
  64. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  65. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  66. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
  67. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  68. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  69. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_entities.py +0 -0
  70. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  71. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_groups.py +0 -0
  72. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_hacs.py +0 -0
  73. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_history.py +0 -0
  74. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_labels.py +0 -0
  75. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  76. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_resources.py +0 -0
  77. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_service.py +0 -0
  78. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_system.py +0 -0
  79. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_todo.py +0 -0
  80. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_traces.py +0 -0
  81. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_updates.py +0 -0
  82. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_utility.py +0 -0
  83. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  84. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_zones.py +0 -0
  85. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/transforms/__init__.py +0 -0
  86. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/transforms/categorized_search.py +0 -0
  87. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/utils/__init__.py +0 -0
  88. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/utils/domain_handlers.py +0 -0
  89. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  90. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/utils/operation_manager.py +0 -0
  91. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/utils/python_sandbox.py +0 -0
  92. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/utils/usage_logger.py +0 -0
  93. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  94. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  95. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  96. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  97. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  98. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/tests/__init__.py +0 -0
  99. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/tests/test_constants.py +0 -0
  100. {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/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.2.0.dev320
3
+ Version: 7.2.0.dev322
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.2.0.dev320"
7
+ version = "7.2.0.dev322"
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"
@@ -175,6 +175,7 @@ dev = [
175
175
  "pytest-xdist>=3.8.0",
176
176
  "requests>=2.25.0",
177
177
  "lefthook>=1.10.0",
178
+ "ruamel.yaml>=0.18.0",
178
179
  "ruff>=0.12.12",
179
180
  "testcontainers>=4.13.0",
180
181
  "ast-grep-cli>=0.42.0",
@@ -13,7 +13,7 @@ from pydantic import Field
13
13
 
14
14
  from ..errors import ErrorCode, create_error_response
15
15
  from .helpers import exception_to_structured_error, log_tool_usage, raise_tool_error
16
- from .util_helpers import coerce_bool_param
16
+ from .util_helpers import build_pagination_metadata, coerce_bool_param, coerce_int_param
17
17
 
18
18
  logger = logging.getLogger(__name__)
19
19
 
@@ -26,8 +26,8 @@ def register_integration_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
26
26
  annotations={
27
27
  "idempotentHint": True,
28
28
  "readOnlyHint": True,
29
- "title": "Get Integration"
30
- }
29
+ "title": "Get Integration",
30
+ },
31
31
  )
32
32
  @log_tool_usage
33
33
  async def ha_get_integration(
@@ -83,35 +83,36 @@ def register_integration_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
83
83
  default=True,
84
84
  ),
85
85
  ] = True,
86
+ limit: Annotated[
87
+ int | str,
88
+ Field(
89
+ default=50,
90
+ description="Max entries to return per page in list mode (default: 50)",
91
+ ),
92
+ ] = 50,
93
+ offset: Annotated[
94
+ int | str,
95
+ Field(
96
+ default=0,
97
+ description="Number of entries to skip for pagination (default: 0)",
98
+ ),
99
+ ] = 0,
86
100
  ) -> dict[str, Any]:
87
- """
88
- Get integration (config entry) information - list all or get a specific one.
101
+ """Get integration (config entry) information with pagination.
89
102
 
90
103
  Without an entry_id: Lists all configured integrations with optional filters.
91
104
  With an entry_id: Returns detailed information including full options/configuration.
92
105
 
93
- Use this to audit existing configurations (e.g. template sensor Jinja code).
94
- When creating new functionality, prefer UI-based helpers over templates when possible.
95
-
96
106
  EXAMPLES:
97
107
  - List all integrations: ha_get_integration()
98
- - Search integrations: ha_get_integration(query="zigbee")
108
+ - Paginate: ha_get_integration(offset=50)
109
+ - Search: ha_get_integration(query="zigbee")
99
110
  - Get specific entry: ha_get_integration(entry_id="abc123")
100
111
  - Get entry with editable fields: ha_get_integration(entry_id="abc123", include_schema=True)
101
- - List template entries with definitions: ha_get_integration(domain="template")
102
- - List all with options: ha_get_integration(include_options=True)
112
+ - List template entries: ha_get_integration(domain="template")
103
113
 
104
- STATES: 'loaded' (running), 'setup_error', 'setup_retry', 'not_loaded',
114
+ STATES: 'loaded', 'setup_error', 'setup_retry', 'not_loaded',
105
115
  'failed_unload', 'migration_error'.
106
-
107
- RETURNS (when listing):
108
- - entries: List of integrations with domain, title, state, capabilities
109
- - state_summary: Count of entries in each state
110
- - When domain filter or include_options is set, each entry includes the 'options' object
111
-
112
- RETURNS (when getting specific entry):
113
- - entry: Full config entry details including options/configuration
114
- - options_schema: Options flow schema when include_schema=True and supports_options=true
115
116
  """
116
117
  try:
117
118
  include_opts = coerce_bool_param(
@@ -123,6 +124,10 @@ def register_integration_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
123
124
  exact_match_bool = coerce_bool_param(
124
125
  exact_match, "exact_match", default=True
125
126
  )
127
+ limit_int = coerce_int_param(
128
+ limit, "limit", default=50, min_value=1, max_value=200
129
+ )
130
+ offset_int = coerce_int_param(offset, "offset", default=0, min_value=0)
126
131
  # Auto-enable options when domain filter is set
127
132
  if domain is not None:
128
133
  include_opts = True
@@ -266,16 +271,22 @@ def register_integration_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
266
271
  matches.sort(key=lambda x: x[0], reverse=True)
267
272
  formatted_entries = [match[1] for match in matches]
268
273
 
269
- # Group by state for summary
274
+ # Group by state for summary (computed before pagination for full picture)
270
275
  state_summary: dict[str, int] = {}
271
276
  for entry in formatted_entries:
272
277
  state = entry.get("state", "unknown")
273
278
  state_summary[state] = state_summary.get(state, 0) + 1
274
279
 
280
+ # Apply pagination
281
+ total_entries = len(formatted_entries)
282
+ paginated_entries = formatted_entries[offset_int : offset_int + limit_int]
283
+
275
284
  result_data: dict[str, Any] = {
276
285
  "success": True,
277
- "total": len(formatted_entries),
278
- "entries": formatted_entries,
286
+ **build_pagination_metadata(
287
+ total_entries, offset_int, limit_int, len(paginated_entries)
288
+ ),
289
+ "entries": paginated_entries,
279
290
  "state_summary": state_summary,
280
291
  "query": query if query else None,
281
292
  }
@@ -298,10 +309,7 @@ def register_integration_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
298
309
 
299
310
  @mcp.tool(
300
311
  tags={"Integrations"},
301
- annotations={
302
- "destructiveHint": True,
303
- "title": "Set Integration Enabled"
304
- }
312
+ annotations={"destructiveHint": True, "title": "Set Integration Enabled"},
305
313
  )
306
314
  @log_tool_usage
307
315
  async def ha_set_integration_enabled(
@@ -365,10 +373,7 @@ def register_integration_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
365
373
 
366
374
  @mcp.tool(
367
375
  tags={"Integrations"},
368
- annotations={
369
- "destructiveHint": True,
370
- "title": "Delete Config Entry"
371
- }
376
+ annotations={"destructiveHint": True, "title": "Delete Config Entry"},
372
377
  )
373
378
  @log_tool_usage
374
379
  async def ha_delete_config_entry(
@@ -11,7 +11,7 @@ Important: Device renaming does NOT cascade to entities - they are independent r
11
11
 
12
12
  import logging
13
13
  import re
14
- from typing import Annotated, Any
14
+ from typing import Annotated, Any, Literal
15
15
 
16
16
  from fastmcp.exceptions import ToolError
17
17
  from pydantic import Field
@@ -24,7 +24,12 @@ from .helpers import (
24
24
  log_tool_usage,
25
25
  raise_tool_error,
26
26
  )
27
- from .util_helpers import coerce_bool_param, parse_string_list_param
27
+ from .util_helpers import (
28
+ build_pagination_metadata,
29
+ coerce_bool_param,
30
+ coerce_int_param,
31
+ parse_string_list_param,
32
+ )
28
33
 
29
34
  # Known voice assistant identifiers
30
35
  KNOWN_ASSISTANTS = ["conversation", "cloud.alexa", "cloud.google_assistant"]
@@ -341,7 +346,10 @@ def register_registry_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
341
346
  context={"device_id": device_id},
342
347
  )
343
348
 
344
- @mcp.tool(tags={"Device Registry"}, annotations={"destructiveHint": True, "title": "Rename Entity"})
349
+ @mcp.tool(
350
+ tags={"Device Registry"},
351
+ annotations={"destructiveHint": True, "title": "Rename Entity"},
352
+ )
345
353
  @log_tool_usage
346
354
  async def ha_rename_entity(
347
355
  entity_id: Annotated[
@@ -504,7 +512,11 @@ def register_registry_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
504
512
  results["device_rename"] = {
505
513
  "skipped": True,
506
514
  "reason": reason,
507
- **({"warning": "Registry query failed; retry may succeed"} if registry_lookup_failed else {}),
515
+ **(
516
+ {"warning": "Registry query failed; retry may succeed"}
517
+ if registry_lookup_failed
518
+ else {}
519
+ ),
508
520
  }
509
521
 
510
522
  # Build success response
@@ -568,8 +580,8 @@ def register_registry_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
568
580
  annotations={
569
581
  "idempotentHint": True,
570
582
  "readOnlyHint": True,
571
- "title": "Get Device (incl. Zigbee/ZHA/Z2M and Z-Wave)"
572
- }
583
+ "title": "Get Device (incl. Zigbee/ZHA/Z2M and Z-Wave)",
584
+ },
573
585
  )
574
586
  @log_tool_usage
575
587
  async def ha_get_device(
@@ -608,42 +620,58 @@ def register_registry_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
608
620
  default=None,
609
621
  ),
610
622
  ] = None,
623
+ limit: Annotated[
624
+ int | str,
625
+ Field(
626
+ default=50,
627
+ description="Max devices to return per page in list mode (default: 50)",
628
+ ),
629
+ ] = 50,
630
+ offset: Annotated[
631
+ int | str,
632
+ Field(
633
+ default=0,
634
+ description="Number of devices to skip for pagination (default: 0)",
635
+ ),
636
+ ] = 0,
637
+ detail_level: Annotated[
638
+ Literal["summary", "full"],
639
+ Field(
640
+ default="summary",
641
+ description=(
642
+ "'summary': basic device info and protocol identifiers (default for list mode). "
643
+ "'full': include entities and all integration details. "
644
+ "Single device lookups always return full detail."
645
+ ),
646
+ ),
647
+ ] = "summary",
611
648
  ) -> dict[str, Any]:
612
- """
613
- Get device information, including Zigbee (ZHA/Z2M) and Z-Wave JS devices with protocol-specific details.
649
+ """Get device information with pagination, including Zigbee (ZHA/Z2M) and Z-Wave JS devices.
614
650
 
615
- Without device_id/entity_id: Lists all devices with optional filters.
616
- With device_id or entity_id: Returns detailed info for that specific device.
651
+ Without device_id/entity_id: Lists devices with optional filters and pagination.
652
+ With device_id or entity_id: Returns full detail for that specific device.
617
653
 
618
- **List all devices:**
619
- - All devices: ha_get_device()
654
+ **List devices (paginated):**
655
+ - First page: ha_get_device()
656
+ - Next page: ha_get_device(offset=50)
620
657
  - By area: ha_get_device(area_id="living_room")
621
- - By manufacturer: ha_get_device(manufacturer="Philips")
622
658
  - By integration: ha_get_device(integration="zigbee2mqtt")
623
- - Combined filters: ha_get_device(integration="zha", area_id="kitchen")
659
+ - Full details in list: ha_get_device(detail_level="full", limit=10)
624
660
 
625
- **Single device lookup:**
661
+ **Single device lookup (always full detail):**
626
662
  - By device_id: ha_get_device(device_id="abc123")
627
663
  - By entity_id: ha_get_device(entity_id="light.living_room")
628
664
 
629
- **Zigbee device support:**
630
- - Use integration="zha" or integration="zigbee2mqtt" to list Zigbee devices
631
- - Returns ieee_address, integration_type, and radio metrics (LQI/RSSI) for ZHA devices
632
- - ZHA triggers: Use `ieee_address` for zha_event triggers
633
- - Z2M triggers: Use `friendly_name` for MQTT topics (zigbee2mqtt/{friendly_name}/action)
634
-
635
- **Z-Wave JS device support:**
636
- - Use integration="zwave_js" to list Z-Wave devices
637
- - Returns node_id extracted from device identifiers
638
- - Single device lookup includes node_status (security class, routing, Z-Wave+ version)
639
-
640
- **Returns (list mode):**
641
- - List of devices with device_id, name, manufacturer, model, area_id
642
-
643
- **Returns (single device):**
644
- - Full device details including integration_type, ieee_address/node_id, radio_metrics/node_status, entities
665
+ **Zigbee:** integration="zha" or "zigbee2mqtt". Returns ieee_address, radio metrics.
666
+ **Z-Wave:** integration="zwave_js". Returns node_id, node_status.
645
667
  """
646
668
  try:
669
+ limit_int = coerce_int_param(
670
+ limit, "limit", default=50, min_value=1, max_value=200
671
+ )
672
+ offset_int = coerce_int_param(offset, "offset", default=0, min_value=0)
673
+ effective_detail = detail_level
674
+
647
675
  # Get device registry
648
676
  list_message: dict[str, Any] = {"type": "config/device_registry/list"}
649
677
  list_result = await client.send_websocket_message(list_message)
@@ -665,7 +693,9 @@ def register_registry_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
665
693
  entity_result.get("result", []) if entity_result.get("success") else []
666
694
  )
667
695
 
668
- # Build entity -> device_id map
696
+ # Build entity -> device_id map (always needed for entity_id param lookup)
697
+ # Build device -> entities map only when needed (single device lookup or full detail)
698
+ need_entity_details = device_id or entity_id or effective_detail == "full"
669
699
  entity_to_device: dict[str, str] = {}
670
700
  device_to_entities: dict[str, list[dict[str, Any]]] = {}
671
701
  for e in all_entities:
@@ -673,15 +703,16 @@ def register_registry_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
673
703
  did = e.get("device_id")
674
704
  if eid and did:
675
705
  entity_to_device[eid] = did
676
- if did not in device_to_entities:
677
- device_to_entities[did] = []
678
- device_to_entities[did].append(
679
- {
680
- "entity_id": eid,
681
- "name": e.get("name") or e.get("original_name"),
682
- "platform": e.get("platform"),
683
- }
684
- )
706
+ if need_entity_details:
707
+ if did not in device_to_entities:
708
+ device_to_entities[did] = []
709
+ device_to_entities[did].append(
710
+ {
711
+ "entity_id": eid,
712
+ "name": e.get("name") or e.get("original_name"),
713
+ "platform": e.get("platform"),
714
+ }
715
+ )
685
716
 
686
717
  # If entity_id provided, find the device_id
687
718
  if entity_id and not device_id:
@@ -836,7 +867,12 @@ def register_registry_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
836
867
  "lqi": zha_dev.get("lqi"),
837
868
  "rssi": zha_dev.get("rssi"),
838
869
  }
839
- except (HomeAssistantConnectionError, HomeAssistantAPIError, TimeoutError, OSError) as e:
870
+ except (
871
+ HomeAssistantConnectionError,
872
+ HomeAssistantAPIError,
873
+ TimeoutError,
874
+ OSError,
875
+ ) as e:
840
876
  logger.warning(
841
877
  "Could not fetch ZHA radio metrics for device %s: %s",
842
878
  device_info.get("device_id"),
@@ -844,9 +880,9 @@ def register_registry_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
844
880
  )
845
881
 
846
882
  # Enrich Z-Wave JS devices with node status
847
- if device_info.get("integration_type") == "zwave_js" and device_info.get(
848
- "node_id"
849
- ):
883
+ if device_info.get(
884
+ "integration_type"
885
+ ) == "zwave_js" and device_info.get("node_id"):
850
886
  try:
851
887
  zwave_result = await client.send_websocket_message(
852
888
  {"type": "zwave_js/node_status", "device_id": device_id}
@@ -868,7 +904,12 @@ def register_registry_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
868
904
  "is_controller_node"
869
905
  ),
870
906
  }
871
- except (HomeAssistantConnectionError, HomeAssistantAPIError, TimeoutError, OSError) as e:
907
+ except (
908
+ HomeAssistantConnectionError,
909
+ HomeAssistantAPIError,
910
+ TimeoutError,
911
+ OSError,
912
+ ) as e:
872
913
  logger.warning(
873
914
  "Could not fetch Z-Wave node status for device %s: %s",
874
915
  device_info.get("device_id"),
@@ -911,21 +952,31 @@ def register_registry_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
911
952
  if integration_lower in named_types:
912
953
  if device_info["integration_type"] != integration_lower:
913
954
  continue
914
- elif (
915
- integration_lower
916
- not in device_info.get("integration_sources", [])
955
+ elif integration_lower not in device_info.get(
956
+ "integration_sources", []
917
957
  ):
918
958
  continue
919
959
 
920
- device_info["entities"] = device_to_entities.get(device.get("id"), [])
960
+ # In summary mode, omit entity lists to reduce response size
961
+ if effective_detail == "full":
962
+ device_info["entities"] = device_to_entities.get(
963
+ device.get("id"), []
964
+ )
921
965
  matched_devices.append(device_info)
922
966
 
967
+ # Apply pagination
968
+ total_matched = len(matched_devices)
969
+ paginated_devices = matched_devices[offset_int : offset_int + limit_int]
970
+
923
971
  # Build result
924
972
  result: dict[str, Any] = {
925
973
  "success": True,
926
- "count": len(matched_devices),
974
+ **build_pagination_metadata(
975
+ total_matched, offset_int, limit_int, len(paginated_devices)
976
+ ),
927
977
  "total_devices": len(all_devices),
928
- "devices": matched_devices,
978
+ "devices": paginated_devices,
979
+ "detail_level": effective_detail,
929
980
  }
930
981
 
931
982
  # Add filter info
@@ -982,10 +1033,7 @@ def register_registry_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
982
1033
 
983
1034
  @mcp.tool(
984
1035
  tags={"Device Registry"},
985
- annotations={
986
- "destructiveHint": True,
987
- "title": "Update Device"
988
- }
1036
+ annotations={"destructiveHint": True, "title": "Update Device"},
989
1037
  )
990
1038
  @log_tool_usage
991
1039
  async def ha_update_device(
@@ -1072,8 +1120,8 @@ def register_registry_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
1072
1120
  annotations={
1073
1121
  "destructiveHint": True,
1074
1122
  "idempotentHint": True,
1075
- "title": "Remove Device"
1076
- }
1123
+ "title": "Remove Device",
1124
+ },
1077
1125
  )
1078
1126
  @log_tool_usage
1079
1127
  async def ha_remove_device(
@@ -17,6 +17,7 @@ from ..transforms.categorized_search import DEFAULT_PINNED_TOOLS
17
17
  from .helpers import exception_to_structured_error, log_tool_usage
18
18
  from .util_helpers import (
19
19
  add_timezone_metadata,
20
+ build_pagination_metadata,
20
21
  coerce_bool_param,
21
22
  coerce_int_param,
22
23
  parse_string_list_param,
@@ -28,15 +29,22 @@ logger = logging.getLogger(__name__)
28
29
  def _build_pagination_metadata(
29
30
  total_matches: int, offset: int, limit: int, results: list[dict[str, Any]]
30
31
  ) -> dict[str, Any]:
31
- """Build standardized pagination metadata for search responses."""
32
- has_more = (offset + len(results)) < total_matches
32
+ """Build standardized pagination metadata for search responses.
33
+
34
+ Thin wrapper around the shared ``build_pagination_metadata`` helper that
35
+ keeps the existing call-site signature (accepts a *results* list and uses
36
+ ``total_matches`` as the key name expected by search tools).
37
+ """
38
+ meta = build_pagination_metadata(total_matches, offset, limit, len(results))
39
+ # Search tools use "total_matches" instead of "total_count" —
40
+ # construct explicitly to avoid fragile dependency on shared helper's key names
33
41
  return {
34
- "total_matches": total_matches,
35
- "offset": offset,
36
- "limit": limit,
37
- "count": len(results),
38
- "has_more": has_more,
39
- "next_offset": offset + limit if has_more else None,
42
+ "total_matches": meta["total_count"],
43
+ "offset": meta["offset"],
44
+ "limit": meta["limit"],
45
+ "count": meta["count"],
46
+ "has_more": meta["has_more"],
47
+ "next_offset": meta["next_offset"],
40
48
  }
41
49
 
42
50
 
@@ -144,8 +152,8 @@ def register_search_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
144
152
  annotations={
145
153
  "idempotentHint": True,
146
154
  "readOnlyHint": True,
147
- "title": "Search Entities"
148
- }
155
+ "title": "Search Entities",
156
+ },
149
157
  )
150
158
  @log_tool_usage
151
159
  async def ha_search_entities(
@@ -498,8 +506,8 @@ def register_search_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
498
506
  annotations={
499
507
  "idempotentHint": True,
500
508
  "readOnlyHint": True,
501
- "title": "Get System Overview"
502
- }
509
+ "title": "Get System Overview",
510
+ },
503
511
  )
504
512
  @log_tool_usage
505
513
  async def ha_get_overview(
@@ -721,8 +729,8 @@ def register_search_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
721
729
  annotations={
722
730
  "idempotentHint": True,
723
731
  "readOnlyHint": True,
724
- "title": "Deep Search"
725
- }
732
+ "title": "Deep Search",
733
+ },
726
734
  )
727
735
  @log_tool_usage
728
736
  async def ha_deep_search(
@@ -829,8 +837,8 @@ def register_search_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
829
837
  annotations={
830
838
  "idempotentHint": True,
831
839
  "readOnlyHint": True,
832
- "title": "Get Entity State"
833
- }
840
+ "title": "Get Entity State",
841
+ },
834
842
  )
835
843
  @log_tool_usage
836
844
  async def ha_get_state(entity_id: str) -> dict[str, Any]:
@@ -856,8 +864,8 @@ def register_search_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
856
864
  annotations={
857
865
  "idempotentHint": True,
858
866
  "readOnlyHint": True,
859
- "title": "Get Multiple Entity States"
860
- }
867
+ "title": "Get Multiple Entity States",
868
+ },
861
869
  )
862
870
  @log_tool_usage
863
871
  async def ha_get_states(