ha-mcp-dev 6.7.2.dev250__tar.gz → 6.7.2.dev251__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 (94) hide show
  1. {ha_mcp_dev-6.7.2.dev250/src/ha_mcp_dev.egg-info → ha_mcp_dev-6.7.2.dev251}/PKG-INFO +1 -1
  2. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/pyproject.toml +1 -1
  3. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_updates.py +230 -2
  4. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  5. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/LICENSE +0 -0
  6. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/MANIFEST.in +0 -0
  7. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/README.md +0 -0
  8. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/setup.cfg +0 -0
  9. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/__init__.py +0 -0
  10. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/__main__.py +0 -0
  11. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/_pypi_marker +0 -0
  12. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/auth/__init__.py +0 -0
  13. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/auth/consent_form.py +0 -0
  14. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/auth/provider.py +0 -0
  15. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/client/__init__.py +0 -0
  16. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/client/rest_client.py +0 -0
  17. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/client/websocket_client.py +0 -0
  18. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/client/websocket_listener.py +0 -0
  19. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/config.py +0 -0
  20. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/errors.py +0 -0
  21. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/py.typed +0 -0
  22. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/resources/card_types.json +0 -0
  23. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/resources/dashboard_guide.md +0 -0
  24. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  25. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  26. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  27. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  28. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  29. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  30. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  31. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  32. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  33. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  34. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  35. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  36. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  37. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  38. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  39. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  40. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/server.py +0 -0
  41. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/smoke_test.py +0 -0
  42. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/__init__.py +0 -0
  43. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/backup.py +0 -0
  44. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/device_control.py +0 -0
  45. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/enhanced.py +0 -0
  46. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/helpers.py +0 -0
  47. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/registry.py +0 -0
  48. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/smart_search.py +0 -0
  49. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_addons.py +0 -0
  50. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_areas.py +0 -0
  51. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  52. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  53. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_calendar.py +0 -0
  54. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_camera.py +0 -0
  55. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  56. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  57. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
  58. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  59. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_config_info.py +0 -0
  60. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  61. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_entities.py +0 -0
  62. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  63. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_groups.py +0 -0
  64. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_hacs.py +0 -0
  65. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_history.py +0 -0
  66. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_integrations.py +0 -0
  67. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_labels.py +0 -0
  68. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  69. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_registry.py +0 -0
  70. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_resources.py +0 -0
  71. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_search.py +0 -0
  72. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_service.py +0 -0
  73. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_services.py +0 -0
  74. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_system.py +0 -0
  75. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_todo.py +0 -0
  76. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_traces.py +0 -0
  77. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_utility.py +0 -0
  78. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  79. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_zones.py +0 -0
  80. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/util_helpers.py +0 -0
  81. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/utils/__init__.py +0 -0
  82. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/utils/domain_handlers.py +0 -0
  83. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  84. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/utils/operation_manager.py +0 -0
  85. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/utils/python_sandbox.py +0 -0
  86. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/utils/usage_logger.py +0 -0
  87. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  88. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  89. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  90. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  91. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  92. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/tests/__init__.py +0 -0
  93. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/tests/test_constants.py +0 -0
  94. {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/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: 6.7.2.dev250
3
+ Version: 6.7.2.dev251
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 = "6.7.2.dev250"
7
+ version = "6.7.2.dev251"
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"
@@ -5,6 +5,7 @@ This module provides tools for listing available updates, getting release notes,
5
5
  and retrieving system version information.
6
6
  """
7
7
 
8
+ import asyncio
8
9
  import logging
9
10
  import re
10
11
  from typing import Annotated, Any
@@ -19,6 +20,188 @@ from .util_helpers import coerce_bool_param
19
20
 
20
21
  logger = logging.getLogger(__name__)
21
22
 
23
+ _GITHUB_CORE_RELEASE_URL = (
24
+ "https://api.github.com/repos/home-assistant/core/releases/tags/{version}"
25
+ )
26
+ _MAX_MONTHLY_VERSIONS = 12
27
+
28
+
29
+ def _parse_version(version_str: str) -> tuple[int, ...] | None:
30
+ """Parse '2025.11.3' into a comparable tuple, or None on failure."""
31
+ if not version_str:
32
+ return None
33
+ try:
34
+ return tuple(int(x) for x in version_str.split("."))
35
+ except (ValueError, AttributeError):
36
+ return None
37
+
38
+
39
+ def _get_monthly_versions_between(current: str, target: str) -> list[str]:
40
+ """Return .0 monthly versions between current (exclusive) and target (inclusive)."""
41
+ current_parts = _parse_version(current)
42
+ target_parts = _parse_version(target)
43
+ if not current_parts or not target_parts or len(current_parts) < 2 or len(target_parts) < 2:
44
+ if target_parts and len(target_parts) >= 2:
45
+ return [f"{target_parts[0]}.{target_parts[1]}.0"]
46
+ return []
47
+
48
+ versions: list[str] = []
49
+ year, month = current_parts[0], current_parts[1] + 1
50
+ while (year, month) <= (target_parts[0], target_parts[1]):
51
+ versions.append(f"{year}.{month}.0")
52
+ month += 1
53
+ if month > 12:
54
+ month, year = 1, year + 1
55
+ if len(versions) >= _MAX_MONTHLY_VERSIONS:
56
+ break
57
+ return versions
58
+
59
+
60
+ def _strip_html(html: str) -> str:
61
+ """Strip HTML tags and normalise whitespace for readable plain text."""
62
+ text = re.sub(r"<br\s*/?>", "\n", html)
63
+ text = re.sub(r"<p[^>]*>", "\n", text)
64
+ text = re.sub(r"</p>", "\n", text)
65
+ text = re.sub(r"<li[^>]*>", "\n- ", text)
66
+ text = re.sub(r"<[^>]+>", "", text)
67
+ text = re.sub(r"[ \t]+", " ", text)
68
+ text = re.sub(r"\n{3,}", "\n\n", text)
69
+ return text.strip()
70
+
71
+
72
+ def _extract_blog_content(html: str) -> str:
73
+ """Extract article body from an HA blog post as plain text."""
74
+ article = re.search(r"<article[^>]*>(.*?)</article>", html, re.DOTALL)
75
+ if article:
76
+ return _strip_html(article.group(1))
77
+ content = re.search(
78
+ r"(<h[12][^>]*>.*?)(?=<div[^>]*id=\"discourse|<footer[^>]*>|</body>)",
79
+ html, re.DOTALL | re.IGNORECASE,
80
+ )
81
+ if content:
82
+ return _strip_html(content.group(1))
83
+ return _strip_html(html)
84
+
85
+
86
+ def _parse_breaking_changes_html(html: str, source_url: str) -> dict[str, Any] | None:
87
+ """Extract 'Backward-incompatible changes' section entries from blog HTML."""
88
+ section_match = re.search(
89
+ r'id="backward-incompatible-changes"[^>]*>.*?</h2>(.*?)(?=<h2[ >]|</article>|</main>)',
90
+ html, re.DOTALL | re.IGNORECASE,
91
+ )
92
+ if not section_match:
93
+ return None
94
+
95
+ entries: list[dict[str, str]] = []
96
+ for match in re.finditer(
97
+ r"<h3[^>]*>(.*?)</h3>(.*?)(?=<h3[^>]*>|$)", section_match.group(1), re.DOTALL
98
+ ):
99
+ name = _strip_html(match.group(1)).strip()
100
+ desc = _strip_html(match.group(2)).strip()
101
+ if name:
102
+ entries.append({"integration": name, "description": desc})
103
+
104
+ if not entries:
105
+ return None
106
+ return {"entries": entries, "count": len(entries), "source_url": source_url}
107
+
108
+
109
+ def _parse_patch_breaking_changes(body: str, version: str) -> dict[str, Any] | None:
110
+ """Parse (breaking-change) tagged items from a GitHub patch-release body."""
111
+ entries: list[dict[str, str]] = []
112
+ for line in body.split("\n"):
113
+ if "(breaking-change)" not in line.lower():
114
+ continue
115
+ clean = re.sub(r"\(breaking-change\)", "", line, flags=re.IGNORECASE).lstrip("-*").strip()
116
+ if not clean:
117
+ continue
118
+ doc_match = re.search(r"\[([^\]]+?)\s+(?:docs|documentation)\]", clean, re.IGNORECASE)
119
+ integration = doc_match.group(1).strip() if doc_match else "unknown"
120
+ entries.append({"integration": integration, "description": clean})
121
+
122
+ if not entries:
123
+ return None
124
+ return {
125
+ "entries": entries,
126
+ "count": len(entries),
127
+ "source_url": f"https://github.com/home-assistant/core/releases/tag/{version}",
128
+ }
129
+
130
+
131
+ async def _fetch_release_data_for_version(
132
+ http_client: httpx.AsyncClient, version: str
133
+ ) -> dict[str, Any] | None:
134
+ """Fetch release notes and breaking changes for a single HA Core version."""
135
+ try:
136
+ resp = await http_client.get(_GITHUB_CORE_RELEASE_URL.format(version=version))
137
+ if resp.status_code != 200:
138
+ return None
139
+ body = resp.json().get("body", "").strip()
140
+
141
+ if body.startswith("https://www.home-assistant.io/blog/"):
142
+ blog_resp = await http_client.get(body)
143
+ if blog_resp.status_code == 200:
144
+ bc = _parse_breaking_changes_html(blog_resp.text, body)
145
+ return {
146
+ "entries": bc["entries"] if bc else [],
147
+ "count": bc["count"] if bc else 0,
148
+ "source_url": body,
149
+ "release_notes": _extract_blog_content(blog_resp.text),
150
+ }
151
+
152
+ if "(breaking-change)" in body.lower():
153
+ return _parse_patch_breaking_changes(body, version)
154
+ return None
155
+ except (httpx.RequestError, ValueError, KeyError) as e:
156
+ logger.debug(f"Failed to fetch release data for {version}: {e}")
157
+ return None
158
+
159
+
160
+ async def _fetch_release_data(current_version: str, target_version: str) -> dict[str, Any]:
161
+ """Fetch release notes and breaking changes for all monthly versions in range."""
162
+ monthly = _get_monthly_versions_between(current_version, target_version)
163
+ if not monthly:
164
+ return {"entries": [], "count": 0, "versions_checked": [], "release_notes": []}
165
+
166
+ async with httpx.AsyncClient(
167
+ timeout=20.0, follow_redirects=True,
168
+ headers={"User-Agent": "HomeAssistant-MCP-Server", "Accept": "application/vnd.github+json"},
169
+ ) as http_client:
170
+ results = await asyncio.gather(
171
+ *[_fetch_release_data_for_version(http_client, v) for v in monthly],
172
+ return_exceptions=True,
173
+ )
174
+
175
+ all_entries: list[dict[str, Any]] = []
176
+ versions_checked: list[str] = []
177
+ release_notes: list[dict[str, str]] = []
178
+
179
+ for version, result in zip(monthly, results, strict=True):
180
+ if not isinstance(result, dict):
181
+ continue
182
+ versions_checked.append(version)
183
+ src = result.get("source_url", "")
184
+ all_entries.extend({**entry, "version": version} for entry in result.get("entries", []))
185
+ notes = result.get("release_notes", "")
186
+ if notes:
187
+ release_notes.append({"version": version, "content": notes, "source_url": src})
188
+
189
+ return {
190
+ "entries": all_entries, "count": len(all_entries),
191
+ "versions_checked": versions_checked, "release_notes": release_notes,
192
+ }
193
+
194
+
195
+ async def _get_installed_integration_domains(client: Any) -> set[str]:
196
+ """Get installed integration domains from config entries."""
197
+ try:
198
+ entries = await client._request("GET", "/config/config_entries/entry")
199
+ if isinstance(entries, list):
200
+ return {e.get("domain", "") for e in entries} - {""}
201
+ return set()
202
+ except (httpx.RequestError, ValueError, KeyError):
203
+ return set()
204
+
22
205
 
23
206
  def register_update_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
24
207
  """Register Home Assistant update management tools."""
@@ -104,7 +287,9 @@ def register_update_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
104
287
  "include_skipped": include_skipped,
105
288
  }
106
289
 
107
- async def _get_update_details(entity_id: str) -> dict[str, Any]:
290
+ async def _get_update_details(
291
+ entity_id: str, include_release_notes: bool = False
292
+ ) -> dict[str, Any]:
108
293
  """Internal helper to get details for a specific update entity."""
109
294
  # Validate entity_id format
110
295
  if not entity_id.startswith("update."):
@@ -187,6 +372,26 @@ def register_update_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
187
372
  f"View them at: {release_url}"
188
373
  )
189
374
 
375
+ # Include multi-version breaking change analysis for Core updates
376
+ if include_release_notes and result.get("category") == "core":
377
+ installed = result.get("installed_version")
378
+ target = result.get("latest_version")
379
+ if installed and target:
380
+ rd_result, domains_result = await asyncio.gather(
381
+ _fetch_release_data(installed, target),
382
+ _get_installed_integration_domains(client),
383
+ return_exceptions=True,
384
+ )
385
+ rd = rd_result if isinstance(rd_result, dict) else {}
386
+ domains = domains_result if isinstance(domains_result, set) else set()
387
+ result["installed_integrations"] = sorted(domains)
388
+ result["multi_version_release_notes"] = rd.get("release_notes", [])
389
+ result["breaking_changes"] = {
390
+ "entries": rd.get("entries", []),
391
+ "count": rd.get("count", 0),
392
+ "versions_checked": rd.get("versions_checked", []),
393
+ }
394
+
190
395
  return result
191
396
 
192
397
  @mcp.tool(annotations={"idempotentHint": True, "openWorldHint": True, "readOnlyHint": True, "tags": ["update"], "title": "Get Updates"})
@@ -207,6 +412,15 @@ def register_update_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
207
412
  default=False,
208
413
  ),
209
414
  ] = False,
415
+ include_release_notes: Annotated[
416
+ bool | str,
417
+ Field(
418
+ description="When getting a Core update entity, fetch multi-version release notes "
419
+ "and breaking changes for all versions between installed and latest (default: False). "
420
+ "Adds breaking_changes, multi_version_release_notes, and installed_integrations to the response.",
421
+ default=False,
422
+ ),
423
+ ] = False,
210
424
  ) -> dict[str, Any]:
211
425
  """
212
426
  Get update information - list all updates or get details for a specific one.
@@ -217,10 +431,15 @@ def register_update_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
217
431
  With an entity_id: Returns detailed information about a specific update including
218
432
  version info, category, and release notes (if available).
219
433
 
434
+ With include_release_notes=True (Core updates only): Also fetches HA release
435
+ blog posts for every monthly version between installed and latest. Returns
436
+ structured breaking changes and installed integration domains for cross-referencing.
437
+
220
438
  EXAMPLES:
221
439
  - List all updates: ha_get_updates()
222
440
  - List including skipped: ha_get_updates(include_skipped=True)
223
441
  - Get specific update: ha_get_updates(entity_id="update.home_assistant_core_update")
442
+ - Pre-update analysis: ha_get_updates(entity_id="update.home_assistant_core_update", include_release_notes=True)
224
443
 
225
444
  RETURNS (when listing):
226
445
  - updates_available: Count of available updates
@@ -231,6 +450,11 @@ def register_update_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
231
450
  - Update details including installed/latest versions
232
451
  - Release notes (fetched from WebSocket API or GitHub)
233
452
  - Category and installation status
453
+
454
+ RETURNS (with include_release_notes=True, Core only):
455
+ - breaking_changes.entries[]: Each has integration, description, version
456
+ - multi_version_release_notes[]: Full text per version {version, content, source_url}
457
+ - installed_integrations: Your integration domains for cross-referencing
234
458
  """
235
459
  try:
236
460
  if entity_id is None:
@@ -241,7 +465,10 @@ def register_update_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
241
465
  return await _list_updates(include_skipped_bool)
242
466
  else:
243
467
  # Get mode: return details for specific update
244
- return await _get_update_details(entity_id)
468
+ include_rn_bool = coerce_bool_param(
469
+ include_release_notes, "include_release_notes", default=False
470
+ ) or False
471
+ return await _get_update_details(entity_id, include_rn_bool)
245
472
 
246
473
  except ToolError:
247
474
  raise
@@ -260,6 +487,7 @@ def register_update_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
260
487
  "Verify API access permissions",
261
488
  ])
262
489
 
490
+
263
491
  def _supports_release_notes(entity_id: str, attributes: dict[str, Any]) -> bool:
264
492
  """
265
493
  Determine if an update entity supports fetching release notes.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 6.7.2.dev250
3
+ Version: 6.7.2.dev251
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