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.
- {ha_mcp_dev-7.2.0.dev320/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.2.0.dev322}/PKG-INFO +1 -1
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/pyproject.toml +2 -1
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_integrations.py +37 -32
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_registry.py +106 -58
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_search.py +26 -18
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_services.py +105 -44
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_yaml_config.py +2 -2
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/util_helpers.py +67 -24
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/LICENSE +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/README.md +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/setup.cfg +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/py.typed +0 -0
- {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
- {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
- {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
- {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
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/smart_search.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {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
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {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
- {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
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {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
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.2.0.dev320 → ha_mcp_dev-7.2.0.dev322}/tests/test_env_manager.py +0 -0
|
@@ -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.
|
|
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
|
-
-
|
|
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
|
|
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'
|
|
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
|
-
|
|
278
|
-
|
|
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
|
|
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(
|
|
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
|
-
**(
|
|
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
|
|
616
|
-
With device_id or entity_id: Returns
|
|
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
|
|
619
|
-
-
|
|
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
|
-
-
|
|
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
|
|
630
|
-
-
|
|
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
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
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 (
|
|
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(
|
|
848
|
-
"
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
974
|
+
**build_pagination_metadata(
|
|
975
|
+
total_matched, offset_int, limit_int, len(paginated_devices)
|
|
976
|
+
),
|
|
927
977
|
"total_devices": len(all_devices),
|
|
928
|
-
"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
|
-
|
|
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":
|
|
35
|
-
"offset": offset,
|
|
36
|
-
"limit": limit,
|
|
37
|
-
"count":
|
|
38
|
-
"has_more": has_more,
|
|
39
|
-
"next_offset":
|
|
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(
|