Skip to content

Commit 87fc533

Browse files
julienldclaude
andauthored
feat: add ha_bug_report tool for collecting diagnostic info (#233)
* feat: add ha_bug_report tool for collecting diagnostic info Implements GitHub issue #204. Adds a new tool that outputs: - Home Assistant version - ha-mcp version - Connection status - Entity count - Instructions for creating bug reports Also updates the bug report issue template to reference the new tool. Closes #204 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * feat: enhance ha_bug_report with logs, template, and anonymization guide - Add tool_call_count parameter to control how many logs to include - Use formula: avg_log_entries_per_tool * 2 * tool_call_count for log retrieval - Add bug report template with "I want to file a bug for:" format - Add comprehensive anonymization guide for privacy-conscious reporting - Add get_recent_logs() function to usage_logger.py - Update tool description with usage guidance for AI agents - Include instructions for presenting template to users - Add new unit tests for all enhanced features 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * refactor: use in-memory ring buffer for recent logs Replace fragile file-reading approach with a proper in-memory ring buffer using collections.deque for O(1) access to recent log entries. - Add ring buffer with configurable size (default 200 entries) - Thread-safe implementation with lock for concurrent access - Logs are added to buffer immediately on log_tool_usage() - No file I/O required for get_recent_logs() - Add comprehensive unit tests for ring buffer functionality - Test thread safety, overflow, and edge cases 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 92bece9 commit 87fc533

6 files changed

Lines changed: 876 additions & 7 deletions

File tree

.github/ISSUE_TEMPLATE/bug_report.md

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,22 @@ A clear and concise description of what you expected to happen.
2424
A clear and concise description of what actually happened.
2525

2626
## 📊 Environment
27+
28+
> **Tip:** Run the `ha_bug_report` tool in your AI assistant to automatically collect this information!
29+
> Just ask your AI to call `ha_bug_report()` and paste the output below.
30+
31+
<details>
32+
<summary>Diagnostic info from ha_bug_report (paste here)</summary>
33+
34+
```
35+
Paste the output from ha_bug_report() here
36+
```
37+
38+
</details>
39+
2740
- **Python version**: (e.g., 3.13)
28-
- **Home Assistant version**: (e.g., 2025.9.1)
29-
- **MCP Server version**: (commit hash or version)
41+
- **Home Assistant version**: (from ha_bug_report or manually: e.g., 2025.9.1)
42+
- **MCP Server version**: (from ha_bug_report or manually: e.g., 4.7.7)
3043
- **Operating System**: (e.g., Ubuntu 22.04, Windows 11, macOS)
3144

3245
## 📝 Error Logs
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
"""
2+
Bug report tool for Home Assistant MCP Server.
3+
4+
This module provides a tool to collect diagnostic information and guide users
5+
on how to create effective bug reports.
6+
"""
7+
8+
import logging
9+
from typing import Annotated, Any
10+
11+
from pydantic import Field
12+
13+
from ha_mcp import __version__
14+
15+
from ..utils.usage_logger import AVG_LOG_ENTRIES_PER_TOOL, get_recent_logs
16+
from .helpers import log_tool_usage
17+
18+
logger = logging.getLogger(__name__)
19+
20+
21+
def register_bug_report_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
22+
"""Register bug report tools with the MCP server."""
23+
24+
@mcp.tool(
25+
annotations={
26+
"idempotentHint": True,
27+
"readOnlyHint": True,
28+
"tags": ["system", "diagnostics"],
29+
"title": "Bug Report Info",
30+
}
31+
)
32+
@log_tool_usage
33+
async def ha_bug_report(
34+
tool_call_count: Annotated[
35+
int,
36+
Field(
37+
default=3,
38+
ge=1,
39+
le=50,
40+
description=(
41+
"Number of tool calls made since the bug started. "
42+
"This determines how many log entries to include. "
43+
"The AI agent should count how many ha_* tools it called "
44+
"from when the issue began. Default: 3"
45+
),
46+
),
47+
] = 3,
48+
) -> dict[str, Any]:
49+
"""
50+
Collect diagnostic information for filing bug reports against ha-mcp.
51+
52+
**WHEN TO USE THIS TOOL:**
53+
Use this tool when the user says something like:
54+
- "I want to file a bug for: <reason>"
55+
- "This isn't working, I need to report this"
56+
- "How do I report this issue?"
57+
58+
**BEFORE CALLING THIS TOOL:**
59+
If the bug details are unclear, guide the user to provide:
60+
1. What they were trying to do (the goal)
61+
2. What actually happened (the result)
62+
3. What they expected to happen instead
63+
4. Any error messages they saw
64+
5. Steps to reproduce the issue
65+
66+
**PARAMETERS:**
67+
- tool_call_count: Count how many ha_* tools you called since the bug started.
68+
This helps include the right amount of logs. Default is 3.
69+
70+
**OUTPUT:**
71+
Returns diagnostic info, recent logs, and a bug report template.
72+
The template guides privacy-conscious reporting while preserving
73+
enough detail to diagnose the issue.
74+
"""
75+
diagnostic_info: dict[str, Any] = {
76+
"ha_mcp_version": __version__,
77+
"connection_status": "Unknown",
78+
"home_assistant_version": "Unknown",
79+
"entity_count": 0,
80+
}
81+
82+
# Try to get Home Assistant config and connection status
83+
try:
84+
config = await client.get_config()
85+
diagnostic_info["connection_status"] = "Connected"
86+
diagnostic_info["home_assistant_version"] = config.get(
87+
"version", "Unknown"
88+
)
89+
diagnostic_info["location_name"] = config.get("location_name", "Unknown")
90+
diagnostic_info["time_zone"] = config.get("time_zone", "Unknown")
91+
except Exception as e:
92+
logger.warning(f"Failed to get Home Assistant config: {e}")
93+
diagnostic_info["connection_status"] = f"Connection Error: {str(e)}"
94+
95+
# Try to get entity count
96+
try:
97+
states = await client.get_states()
98+
if states:
99+
diagnostic_info["entity_count"] = len(states)
100+
except Exception as e:
101+
logger.warning(f"Failed to get entity count: {e}")
102+
103+
# Calculate how many log entries to retrieve
104+
# Formula: AVG_LOG_ENTRIES_PER_TOOL * 2 * tool_call_count
105+
max_log_entries = AVG_LOG_ENTRIES_PER_TOOL * 2 * tool_call_count
106+
recent_logs = get_recent_logs(max_entries=max_log_entries)
107+
108+
# Format logs for inclusion (sanitized summary)
109+
log_summary = _format_logs_for_report(recent_logs)
110+
111+
# Build the formatted report
112+
report_lines = [
113+
"=== ha-mcp Bug Report Info ===",
114+
"",
115+
f"Home Assistant Version: {diagnostic_info['home_assistant_version']}",
116+
f"ha-mcp Version: {diagnostic_info['ha_mcp_version']}",
117+
f"Connection Status: {diagnostic_info['connection_status']}",
118+
f"Entity Count: {diagnostic_info['entity_count']}",
119+
]
120+
121+
# Add optional fields if available
122+
if "location_name" in diagnostic_info:
123+
report_lines.append(f"Location Name: {diagnostic_info['location_name']}")
124+
if "time_zone" in diagnostic_info:
125+
report_lines.append(f"Time Zone: {diagnostic_info['time_zone']}")
126+
127+
if recent_logs:
128+
report_lines.extend([
129+
"",
130+
f"=== Recent Tool Calls ({len(recent_logs)} entries) ===",
131+
log_summary,
132+
])
133+
134+
formatted_report = "\n".join(report_lines)
135+
136+
# Bug report template for the AI to present to the user
137+
bug_report_template = _generate_bug_report_template(
138+
diagnostic_info, log_summary
139+
)
140+
141+
# Anonymization instructions
142+
anonymization_guide = _generate_anonymization_guide()
143+
144+
return {
145+
"success": True,
146+
"diagnostic_info": diagnostic_info,
147+
"recent_logs": recent_logs,
148+
"log_count": len(recent_logs),
149+
"formatted_report": formatted_report,
150+
"bug_report_template": bug_report_template,
151+
"anonymization_guide": anonymization_guide,
152+
"issue_url": "https://github.com/homeassistant-ai/ha-mcp/issues/new",
153+
"instructions": (
154+
"Present the bug_report_template to the user. "
155+
"Ask them to review and fill in the [DESCRIBE...] sections. "
156+
"Remind them to follow the anonymization_guide to protect their privacy. "
157+
"The user should copy the completed template and submit it at the issue_url."
158+
),
159+
}
160+
161+
162+
def _format_logs_for_report(logs: list[dict[str, Any]]) -> str:
163+
"""Format log entries for inclusion in a bug report."""
164+
if not logs:
165+
return "(No recent logs available)"
166+
167+
lines = []
168+
for log in logs:
169+
timestamp = log.get("timestamp", "?")[:19] # Trim to seconds
170+
tool_name = log.get("tool_name", "unknown")
171+
success = "OK" if log.get("success") else "FAIL"
172+
exec_time = log.get("execution_time_ms", 0)
173+
error = log.get("error_message", "")
174+
175+
line = f" {timestamp} | {tool_name} | {success} | {exec_time:.0f}ms"
176+
if error:
177+
# Truncate error to avoid leaking sensitive info
178+
error_short = str(error)[:100]
179+
line += f" | Error: {error_short}"
180+
lines.append(line)
181+
182+
return "\n".join(lines)
183+
184+
185+
def _generate_bug_report_template(
186+
diagnostic_info: dict[str, Any], log_summary: str
187+
) -> str:
188+
"""Generate a bug report template for users to fill out."""
189+
return f"""## Bug Report Template
190+
191+
**Copy this template, fill in the sections, and submit at:**
192+
https://github.com/homeassistant-ai/ha-mcp/issues/new
193+
194+
---
195+
196+
### I want to file a bug for: [DESCRIBE THE BUG IN ONE SENTENCE]
197+
198+
### What I was trying to do
199+
[DESCRIBE YOUR GOAL - What were you asking the AI to do?]
200+
201+
### What happened
202+
[DESCRIBE THE RESULT - What did the AI do or say?]
203+
204+
### What I expected
205+
[DESCRIBE EXPECTED BEHAVIOR - What should have happened instead?]
206+
207+
### Steps to reproduce
208+
1. [First step]
209+
2. [Second step]
210+
3. [...]
211+
212+
### Error messages (if any)
213+
```
214+
[PASTE ANY ERROR MESSAGES HERE]
215+
```
216+
217+
### Environment
218+
- Home Assistant Version: {diagnostic_info.get('home_assistant_version', 'Unknown')}
219+
- ha-mcp Version: {diagnostic_info.get('ha_mcp_version', 'Unknown')}
220+
- Connection Status: {diagnostic_info.get('connection_status', 'Unknown')}
221+
- Entity Count: {diagnostic_info.get('entity_count', 0)}
222+
- Time Zone: {diagnostic_info.get('time_zone', 'Unknown')}
223+
224+
### Recent tool calls
225+
```
226+
{log_summary}
227+
```
228+
229+
### Additional context
230+
[ADD ANY OTHER RELEVANT INFORMATION]
231+
232+
---
233+
**Privacy note:** Please review and anonymize any sensitive information before submitting.
234+
See the anonymization guide in the tool output for details.
235+
"""
236+
237+
238+
def _generate_anonymization_guide() -> str:
239+
"""Generate privacy/anonymization instructions."""
240+
return """## Anonymization Guide
241+
242+
Before submitting your bug report, please review and anonymize:
243+
244+
### MUST ANONYMIZE (security-sensitive):
245+
- API tokens, passwords, secrets -> Replace with "[REDACTED]"
246+
- IP addresses (internal/external) -> Replace with "192.168.x.x" or "[IP]"
247+
- MAC addresses -> Replace with "[MAC]"
248+
- Email addresses -> Replace with "user@example.com"
249+
- Phone numbers -> Replace with "[PHONE]"
250+
251+
### CONSIDER ANONYMIZING (privacy-sensitive):
252+
- Location names (city, address) -> Replace with generic names like "Home" or "[LOCATION]"
253+
- Device names that reveal personal info -> Replace with "Device 1", "Light 1", etc.
254+
- Person names in entity IDs -> Replace with "person.user1"
255+
- Calendar/todo items with personal details -> Summarize without specifics
256+
257+
### KEEP AS-IS (helpful for debugging):
258+
- Entity domains (light, switch, sensor, etc.)
259+
- Device types and capabilities
260+
- Automation/script structure (triggers, conditions, actions)
261+
- Error messages (but check for secrets in them)
262+
- Timestamps and durations
263+
- State values (on/off, numeric values, etc.)
264+
- Home Assistant and ha-mcp versions
265+
266+
### Example anonymization:
267+
BEFORE: "light.juliens_bedroom" with token "eyJhbG..."
268+
AFTER: "light.bedroom_1" with token "[REDACTED]"
269+
270+
The goal is to preserve enough detail to reproduce and fix the bug
271+
while protecting your personal information and security.
272+
"""

0 commit comments

Comments
 (0)