Skip to content

fix: add ast-grep rule and fix hand-built error dicts - #895

Merged
sergeykad merged 4 commits into
masterfrom
fix/return-success-false-violations
Apr 6, 2026
Merged

fix: add ast-grep rule and fix hand-built error dicts#895
sergeykad merged 4 commits into
masterfrom
fix/return-success-false-violations

Conversation

@sergeykad

@sergeykad sergeykad commented Apr 6, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Adds a new ast-grep rule (no-return-success-false) and fixes all pre-existing violations where tool functions return {"success": False, ...} dicts instead of raising ToolError. Returning error dicts doesn't set isError=true on the MCP response (per MCP spec), so LLM agents may not recognize these as errors.

The 3 tools_hacs.py violations were already fixed by #871 (now merged).

New ast-grep rule

The existing no-return-error-response rule catches return create_error_response(...) but misses hand-built error dicts like return await add_timezone_metadata(client, {"success": False, ...}). The new rule uses AST-level matching to catch any dict with "success": False inside a return statement, regardless of key ordering. Correctly excludes batch item appends (.append(...)) and dict assignments.

Fixes

  • device_control.py: failed/timeout operation status in get_device_operation_status now raises ToolError with SERVICE_CALL_FAILED/TIMEOUT_OPERATION
  • tools_entities.py: ha_set_entity exposure failures (both partial and expose-only) now raise ToolError, preserving exposure_succeeded/exposure_failed/partial context
  • tools_entities.py: _fetch_entity inner function raises ValueError on failure instead of returning error dict (callers handle via return_exceptions=True)
  • backup.py: _get_backup_password raises ToolError directly instead of returning (None, error_dict) tuples; callers updated accordingly
  • test_tools_entities.py: updated 4 unit tests to expect ToolError

Type of change

  • Bug fix
  • New feature
  • Documentation
  • Maintenance/refactor
  • Breaking change

Testing

  • All automated tests pass (uv run pytest) — 952 unit tests, E2E tests
  • Code follows style guidelines (uv run ruff check)
  • mypy passes with no errors
  • ast-grep rule tested against positive and negative patterns (0 remaining violations)

Checklist

  • I have updated documentation if needed

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses several pre-existing violations where tool functions returned error dictionaries instead of raising ToolError. By standardizing error handling, the changes ensure that the MCP response correctly sets isError=true, allowing LLM agents to accurately identify and process tool execution failures.

Highlights

  • Error Handling Refactor: Replaced manual error dictionary returns with raise_tool_error across multiple tool functions to ensure proper MCP error reporting.
  • Tool Reliability: Updated device_control.py, tools_entities.py, and backup.py to correctly signal failures to LLM agents by adhering to the MCP error specification.
  • Test Updates: Updated unit tests in test_tools_entities.py to verify that ToolError is correctly raised instead of returning error dictionaries.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@sergeykad sergeykad changed the title fix: replace hand-built error dicts with raise_tool_error fix: add ast-grep rule and fix hand-built error dicts Apr 6, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors error handling across several tool modules—including backup, device control, and entity management—by replacing manual error dictionary returns with structured ToolError exceptions. The reviewer identified an improvement opportunity in tools_entities.py where a generic ValueError is raised; this should be replaced with a structured ToolError using the INTERNAL_ERROR code to comply with repository standards for consistent error reporting.

Comment thread src/ha_mcp/tools/tools_entities.py
@sergeykad
sergeykad marked this pull request as ready for review April 6, 2026 13:32
@sergeykad
sergeykad requested a review from a team April 6, 2026 13:32

@kingpanther13 kingpanther13 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

Good work identifying and fixing the {"success": False} dict-return pattern — this is a real MCP spec issue where isError=true doesn't get set. The ast-grep rule is a nice addition for regression prevention.

Merge order conflict: tools_hacs.py changes duplicate #871

The tools_hacs.py changes in this PR overlap almost entirely with #871. The same 3 error paths (ha_hacs_repository_info not-found, ha_hacs_add_repository invalid-format, ha_hacs_download not-found) were already converted from return await add_timezone_metadata(client, {"success": False, ...}) to raise_tool_error(create_error_response(...)) in #871 — at your request on that PR. Those changes are now being duplicated here.

Requested change: #871 should merge first since it restructures the file significantly (consolidating 4 HACS tools → 2). Please rebase this PR onto #871 once it merges and drop the tools_hacs.py hunks to avoid conflicts and duplicated work.

Test should still assert exposure_succeeded

In test_expose_only_entity_not_found_raises_tool_error, the old assertion assert "exposure_succeeded" in result was removed. Since create_error_response merges context into the top-level response via response.update(context), and the production code passes context={"entity_id": entity_id, "exposure_succeeded": exposure_result}, this data is still present at result["exposure_succeeded"].

Requested change: Re-add the assertion to verify the exposure context is preserved in the error:

assert result["exposure_succeeded"] == {"conversation": True}

Remaining {"success": False} dict return in _update_single_entity

_update_single_entity (~line 237) still returns {"success": False, ...} for partial exposure failures via a variable (return response), so the new ast-grep rule won't catch it. This is the same pattern from the LLM's perspective — isError won't be set.

Requested change: Either convert this to raise_tool_error(create_error_response(...)) with the partial-failure context in context= (so isError=true is set and the LLM can still read exposure_succeeded/exposure_failed from the structured error), or document explicitly in a code comment why this case intentionally stays as a dict return.

Everything else looks clean — the ValueError choice in _fetch_entity is well-justified for the asyncio.gather(return_exceptions=True) pattern, the backup.py refactor simplifies both callers nicely, and the error codes are all appropriate.

@sergeykad
sergeykad marked this pull request as draft April 6, 2026 14:48
sergeykad pushed a commit that referenced this pull request Apr 6, 2026
- Revert tools_hacs.py changes (will be handled by #871)
- Fix _update_single_entity exposure failure to raise ToolError
  instead of returning {"success": False} via variable assignment
- Re-add exposure_succeeded assertion in test
- Update 3 exposure failure tests to expect ToolError
Sergey added 4 commits April 6, 2026 17:49
Fixes 6 of 9 violations caught by the new `no-return-success-false`
ast-grep rule (remaining 3 are in tools_hacs.py, addressed by #871).

Returning `{"success": False, ...}` from tool functions doesn't set
`isError=true` on the MCP response, so LLM agents may not recognize
these as errors.

Changes:
- device_control.py: failed/timeout operation status now raises ToolError
- tools_entities.py: ha_set_entity exposure failure now raises ToolError
- tools_entities.py: _fetch_entity raises ValueError instead of returning
  error dict (callers already handle exceptions via return_exceptions)
- backup.py: _get_backup_password raises ToolError directly instead of
  returning (None, error_dict) tuples; callers updated accordingly
- test_tools_entities.py: updated test to expect ToolError
The existing `no-return-error-response` rule catches `return create_error_response(...)`
but misses hand-built `return {"success": False, ...}` dicts that bypass `raise_tool_error`.
These return `isError=false` in MCP responses, so LLM agents may not recognize them as errors.

The new rule catches any dictionary with `"success": False` inside a return statement,
regardless of key ordering. It correctly excludes batch item appends (`.append(...)`) and
dict assignments which are legitimate uses.

Currently flags 9 pre-existing violations across 4 files:
- tools_hacs.py (3), device_control.py (2), tools_entities.py (2), backup.py (2)
The previous commit dropped partial-success data when converting
to raise_tool_error. Restore it via the context dict so LLM agents
can see which exposure changes succeeded before the failure.
- Revert tools_hacs.py changes (will be handled by #871)
- Fix _update_single_entity exposure failure to raise ToolError
  instead of returning {"success": False} via variable assignment
- Re-add exposure_succeeded assertion in test
- Update 3 exposure failure tests to expect ToolError
@sergeykad
sergeykad force-pushed the fix/return-success-false-violations branch from b7c9a49 to 7c3eea6 Compare April 6, 2026 14:50
@sergeykad
sergeykad marked this pull request as ready for review April 6, 2026 14:52
@sergeykad

Copy link
Copy Markdown
Collaborator Author

All three items addressed:

  1. tools_hacs.py — Dropped from this PR. Already fixed by refactor: consolidate HACS read tools from 4 to 2 #871 (now merged); rebased on top of it.
  2. exposure_succeeded assertion — Re-added. Also added assert result["exposure_succeeded"] == {"conversation": True} to verify the value.
  3. _update_single_entity dict return — Converted to raise_tool_error(create_error_response(...)) with exposure_succeeded/exposure_failed/partial/entity_entry in context. Updated 3 additional unit tests that tested this path.

@sergeykad
sergeykad enabled auto-merge (squash) April 6, 2026 14:56

@kingpanther13 kingpanther13 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All three requested changes addressed:

  1. tools_hacs.py hunks dropped — no more overlap with #871
  2. exposure_succeeded assertion restored (and strengthened to check value, not just presence)
  3. _update_single_entity partial failure path converted to raise_tool_error with full context, plus 4 related tests updated

CI all green. LGTM.

@sergeykad
sergeykad merged commit db9b84f into master Apr 6, 2026
19 checks passed
@sergeykad
sergeykad deleted the fix/return-success-false-violations branch April 6, 2026 14:58
@github-actions

github-actions Bot commented Apr 6, 2026

Copy link
Copy Markdown
Contributor

🧪 Your changes are now in the dev channel!

Your PR has been merged to master and is available for testing in the dev channel.

Test your changes before the next stable release (biweekly Wednesday):
📖 Dev Channel Documentation

Quick start

# Run dev version
uvx ha-mcp-dev

# Check version
uvx ha-mcp-dev --version

Docker:

docker pull ghcr.io/homeassistant-ai/ha-mcp:dev
docker run --rm -i \
  -e HOMEASSISTANT_URL=http://your-ha:8123 \
  -e HOMEASSISTANT_TOKEN=your_token \
  ghcr.io/homeassistant-ai/ha-mcp:dev

Found an issue? Please open a new bug report and mention this PR for context.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants