Skip to content

Issue 518 part5 flip default (final part) - #571

Merged
kingpanther13 merged 9 commits into
homeassistant-ai:masterfrom
kingpanther13:claude/issue-518-part5-flip-default-U70YN
Feb 15, 2026
Merged

Issue 518 part5 flip default (final part)#571
kingpanther13 merged 9 commits into
homeassistant-ai:masterfrom
kingpanther13:claude/issue-518-part5-flip-default-U70YN

Conversation

@kingpanther13

@kingpanther13 kingpanther13 commented Feb 7, 2026

Copy link
Copy Markdown
Member

What does this PR do?

Part 5 of 5 for issue #518 — Tool errors not signaled via isError in MCP protocol responses.

Flips the exception_to_structured_error() default from raise_error=False to raise_error=True, completing the ToolError migration.

Changes:

  • Change exception_to_structured_error() default to raise_error=True
  • Update @overload type signatures to reflect new default
  • Update docstring to document the new default behavior
  • Update unit tests to reflect new default

Why this is safe:
All tool modules have been migrated in PRs 1-4 to either:

  • Explicitly pass raise_error=False when they need to modify the error before raising
  • Use raise_tool_error() directly

This means changing the default has no effect on any existing code paths.

Will completely close #518

Type of change

  • 🐛 Bug fix
  • ✨ New feature
  • 📚 Documentation
  • 🔧 Maintenance/refactor
  • 💥 Breaking change

Testing

  • I have tested these changes with a LLM agent
  • All automated tests pass (uv run pytest)
  • Code follows style guidelines (uv run ruff check)

Checklist

  • I have updated documentation if needed

PR Stack

This is PR 5 of 5 for #518:

  1. PR Issue #518 part1  #551 — Core infrastructure + tools with existing E2E tests ✅
  2. PR Issue #518 part2  #568 — Update remaining 8 E2E test files ✅
  3. PR Issue #518 part3  #569 — Remaining 9 tool modules ✅
  4. PR Issue 518 part4  #570tools_mcp_component.py
  5. This PR — Flip default to raise_error=True

Depends on: PR 3 #569 must be merged first (all tools must be migrated before flipping the default)

…stry tools (homeassistant-ai#518)

Part 1 of the isError signaling fix. Adds core infrastructure and applies
to tools that already have comprehensive E2E test coverage.

Core changes:
- Add raise_tool_error() helper to convert error dicts to ToolError exceptions
- Update exception_to_structured_error() to raise by default (raise_error=True)
- Add @overload type signatures for raise_error parameter
- Add safe_call_tool() and tool_error_to_result() test utilities

Tool changes (3 tools with existing E2E tests):
- tools_config_automations: 5 error paths now raise ToolError
- tools_service: 4 error paths now raise ToolError
- tools_registry: 1 error path now raises ToolError

Test updates:
- Update 11 E2E test files to use safe_call_tool for error path testing
- Add unit tests for raise_tool_error and exception_to_structured_error
- Add retry logic for entity rename tests (timing robustness)

Closes part of homeassistant-ai#518

https://claude.ai/code/session_01MvDDV6qmWosBfGhmTLYzo8
Per code review feedback:
1. Use match statement for HTTP status code dispatch (Python 3.13)
2. Keep raise_error=False as default to avoid regression during PR stack
   - Tools explicitly pass raise_error=True or use raise_error=False + raise_tool_error()
   - Part 5 will flip the default after all tools are migrated
3. Update unit tests to reflect new default behavior

https://claude.ai/code/session_01MvDDV6qmWosBfGhmTLYzo8
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @kingpanther13, 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 finalizes the migration to a new error signaling mechanism within the system's tool modules. It primarily involves changing the default behavior of an error handling utility to proactively raise structured 'ToolError' exceptions, ensuring that tool execution failures are consistently communicated at the protocol level. This change, along with the introduction of a dedicated error-raising helper and updates to test infrastructure, streamlines error management and improves the reliability of tool interactions.

Highlights

  • ToolError Migration Completion: This pull request is the fifth and final part of issue [BUG] Tool errors not signaled via isError in MCP protocol responses #518, which aims to signal tool errors via the 'isError' flag in MCP protocol responses. It flips the default 'raise_error' parameter in 'exception_to_structured_error()' from 'False' to 'True'.
  • Default Error Handling Behavior Change: The 'exception_to_structured_error()' function now defaults to 'raise_error=True', meaning it will raise a 'ToolError' by default instead of returning an error dictionary. This change is safe because all tool modules were previously updated to explicitly handle error raising or return.
  • New 'raise_tool_error()' Helper: A new helper function 'raise_tool_error()' has been introduced to explicitly raise a 'ToolError' with structured error information, ensuring consistent error signaling across tool modules.
  • Enhanced Test Utilities: E2E test utilities have been updated to correctly parse 'ToolError' exceptions and provide a 'safe_call_tool()' wrapper for robust error handling in tests, improving the reliability of test assertions.
Changelog
  • src/ha_mcp/tools/helpers.py
    • Imported 'json', 'Literal', 'NoReturn', 'overload', and 'ToolError'.
    • Added 'raise_tool_error()' function to raise 'ToolError' with JSON-serialized structured error.
    • Modified 'exception_to_structured_error()' to include '@overload' decorators for type hinting, changed its signature to include a 'raise_error: bool = True' parameter, and updated its docstring to reflect the new default behavior. The function's logic was refactored to conditionally raise 'ToolError' or return the error dictionary based on the 'raise_error' parameter.
  • src/ha_mcp/tools/tools_areas.py
    • Imported 'ToolError' and 'raise_tool_error'.
    • Replaced direct 'return create_error_response(...)' with 'raise_tool_error(create_error_response(...))' for validation errors.
    • Added 'except ToolError: raise' blocks to 'ha_config_set_area' and 'ha_config_set_floor' to re-raise existing 'ToolError' exceptions.
  • src/ha_mcp/tools/tools_config_automations.py
    • Imported 'raise_tool_error'.
    • Replaced direct 'return create_error_response(...)' with 'raise_tool_error(...)' for validation errors and other structured errors.
    • Added 'raise_error=False' to 'exception_to_structured_error' calls where the error dictionary is still processed before being raised.
  • src/ha_mcp/tools/tools_config_entry_flow.py
    • Replaced 'return exception_to_structured_error(...)' with 'exception_to_structured_error(...)' (which now raises by default).
  • src/ha_mcp/tools/tools_config_helpers.py
    • Imported 'ToolError' and 'raise_tool_error'.
    • Replaced direct 'return create_error_response(...)' with 'raise_tool_error(create_error_response(...))' for validation errors.
    • Added 'except ToolError: raise' block to 'ha_config_set_helper'.
  • src/ha_mcp/tools/tools_entities.py
    • Imported 'ToolError' and 'raise_tool_error'.
    • Replaced direct 'return create_error_response(...)' with 'raise_tool_error(create_error_response(...))' for validation errors.
    • Replaced 'return exception_to_structured_error(...)' with 'exception_to_structured_error(...)' (which now raises by default).
    • Added 'except ToolError: raise' block to 'ha_set_entity'.
  • src/ha_mcp/tools/tools_filesystem.py
    • Replaced 'return await add_timezone_metadata(client, error_response)' with 'exception_to_structured_error(...)' (which now raises by default), removing the 'add_timezone_metadata' call.
  • src/ha_mcp/tools/tools_hacs.py
    • Imported 'raise_tool_error'.
    • Replaced 'return await add_timezone_metadata(client, error_response)' with 'exception_to_structured_error(...)' or 'raise_tool_error(error_response)'.
    • Added 'raise_error=False' to 'exception_to_structured_error' calls where the error dictionary is still processed before being raised.
  • src/ha_mcp/tools/tools_integrations.py
    • Replaced 'return exception_to_structured_error(...)' with 'exception_to_structured_error(...)' (which now raises by default).
  • src/ha_mcp/tools/tools_registry.py
    • Imported 'raise_tool_error'.
    • Replaced direct 'return create_error_response(...)' with 'raise_tool_error(create_error_response(...))' for validation errors.
  • src/ha_mcp/tools/tools_search.py
    • Imported 'raise_tool_error'.
    • Replaced 'return await add_timezone_metadata(client, error_response)' with 'raise_tool_error(error_response)'.
    • Added 'raise_error=False' to 'exception_to_structured_error' calls where the error dictionary is still processed before being raised.
  • src/ha_mcp/tools/tools_service.py
    • Imported 'raise_tool_error'.
    • Replaced direct 'return create_validation_error(...)' with 'raise_tool_error(create_validation_error(...))' for validation errors.
    • Replaced 'return error_response' with 'raise_tool_error(error_response)' for structured errors.
    • Added 'raise_error=False' to 'exception_to_structured_error' calls where the error dictionary is still processed before being raised.
  • src/ha_mcp/tools/tools_voice_assistant.py
    • Imported 'ToolError' and 'raise_tool_error'.
    • Replaced direct 'return {...}' with 'raise_tool_error({...})' for validation errors and other structured errors.
    • Added 'except ToolError: raise' block to 'ha_get_entity_exposure'.
  • tests/src/e2e/utilities/assertions.py
    • Imported 'ToolError' and 'json'.
    • Modified 'parse_mcp_result()' to handle 'ToolError' responses by parsing their JSON content.
    • Added 'tool_error_to_result()' to convert 'ToolError' exceptions back to a result dictionary for testing.
    • Added 'safe_call_tool()' to wrap 'mcp_client.call_tool()' and catch 'ToolError' exceptions, returning a parsed result dictionary.
    • Updated 'assert_mcp_success()' and 'assert_mcp_failure()' to work with 'ToolError' exceptions caught by 'safe_call_tool()'.
    • Updated 'assert_mcp_failure()' to correctly parse error messages that might be nested in a dictionary.
  • tests/src/e2e/workflows/automation/test_helpers.py
    • Updated various E2E tests to use the new 'safe_call_tool()' utility.
    • Adjusted 'max_retries' in 'wait_for_entity_state' to 10 for CI robustness.
    • Changed 'initial' values for input helpers from string representations to native boolean/integer types.
  • tests/src/e2e/workflows/automation/test_lifecycle.py
    • Updated various E2E tests to use the new 'safe_call_tool()' utility.
  • tests/src/e2e/workflows/blueprints/test_blueprints.py
    • Updated various E2E tests to use the new 'safe_call_tool()' utility.
  • tests/src/e2e/workflows/calendar/test_calendar.py
    • Updated various E2E tests to use the new 'safe_call_tool()' utility.
  • tests/src/e2e/workflows/config/test_config_entry_flow.py
    • Updated various E2E tests to use the new 'safe_call_tool()' utility.
  • tests/src/e2e/workflows/config/test_helper_crud.py
    • Updated various E2E tests to use the new 'safe_call_tool()' utility.
    • Changed 'initial' values for input helpers from string representations to native boolean/integer types.
  • tests/src/e2e/workflows/core/test_history.py
    • Updated various E2E tests to use the new 'safe_call_tool()' utility.
  • tests/src/e2e/workflows/core/test_service.py
    • Updated various E2E tests to use the new 'safe_call_tool()' utility.
  • tests/src/e2e/workflows/core/test_state.py
    • Updated various E2E tests to use the new 'safe_call_tool()' utility.
  • tests/src/e2e/workflows/core/test_templates.py
    • Updated various E2E tests to use the new 'safe_call_tool()' utility.
  • tests/src/e2e/workflows/device_control/test_lights.py
    • Updated various E2E tests to use the new 'safe_call_tool()' utility.
    • Added a 'pytest.skip' condition if cover services are unavailable.
  • tests/src/e2e/workflows/entities/test_entity_management.py
    • Updated various E2E tests to use the new 'safe_call_tool()' utility.
  • tests/src/e2e/workflows/filesystem/test_file_operations.py
    • Updated various E2E tests to use the new 'safe_call_tool()' utility.
  • tests/src/e2e/workflows/hacs/test_hacs.py
    • Updated various E2E tests to use the new 'safe_call_tool()' utility.
  • tests/src/e2e/workflows/integrations/test_integration_management.py
    • Updated various E2E tests to use the new 'safe_call_tool()' utility.
  • tests/src/e2e/workflows/registry/test_device_registry.py
    • Updated various E2E tests to use the new 'safe_call_tool()' utility.
  • tests/src/e2e/workflows/registry/test_entity_rename.py
    • Imported 'asyncio' and updated various E2E tests to use the new 'safe_call_tool()' utility.
    • Added 'asyncio.sleep' calls in rename tests to allow time for entity registration and propagation.
  • tests/src/e2e/workflows/registry/test_voice_assistant.py
    • Updated various E2E tests to use the new 'safe_call_tool()' utility.
  • tests/src/unit/test_tool_error_signaling.py
    • Added new unit test file to verify the behavior of 'raise_tool_error()' and 'exception_to_structured_error()'.
    • Tests that 'raise_tool_error()' raises 'ToolError' with JSON-serialized structured error.
    • Tests that 'exception_to_structured_error()' raises 'ToolError' by default and returns a dictionary when 'raise_error=False'.
    • Verifies correct error code mapping for various exception types.
    • Includes an integration test simulating MCP client error detection.
  • tests/src/unit/test_tools_entities.py
    • Imported 'ToolError'.
    • Updated tests for 'set_entity_tool' to expect 'ToolError' to be raised for invalid parameters instead of returning an error dictionary.
  • tests/src/unit/test_tools_voice_assistant.py
    • Imported 'json' and 'ToolError'.
    • Updated tests for 'ha_get_entity_exposure' to expect 'ToolError' to be raised for invalid assistants or websocket errors instead of returning an error dictionary.
Activity
  • This pull request is the final part of a series of 5 PRs addressing issue [BUG] Tool errors not signaled via isError in MCP protocol responses #518, focusing on robust tool error signaling.
  • The changes introduce a new feature by modifying the default error handling behavior to raise structured exceptions.
  • All automated tests pass, ensuring the stability and correctness of the changes.
  • The code adheres to established style guidelines.
  • Documentation has been updated to reflect the new default behavior and helper functions.
  • The PR depends on PR Issue #518 part3  #569, which has already been merged, ensuring the necessary prerequisites are met.
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.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

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.

This was referenced Feb 7, 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 successfully completes the migration to using ToolError for signaling errors, which is a significant improvement for error handling consistency and client-side error detection. The changes are applied consistently across all tool modules, and the test suite has been thoroughly updated to reflect this new mechanism, including the addition of robust test helpers like safe_call_tool. The included suggestions for improving context propagation in error messages and enhancing test stability remain valid and are recommended for further improvement.

Comment thread src/ha_mcp/tools/helpers.py Outdated
Comment thread src/ha_mcp/tools/helpers.py Outdated
Comment thread src/ha_mcp/tools/helpers.py Outdated
Comment thread tests/src/e2e/workflows/registry/test_entity_rename.py Outdated
@kingpanther13 kingpanther13 mentioned this pull request Feb 7, 2026
9 tasks
Fixes for HIGH priority issues:
- H1: Add `except ToolError: raise` before `except Exception` handlers in
  tools_config_automations.py and tools_service.py to prevent double-wrapping
- H2: Change return type annotation to `dict[str, Any]` (NoReturn in union is meaningless)
- H3: Add case 403 to match statement for auth/permission errors

Fixes for MEDIUM priority issues:
- M2: Add `default=str` to json.dumps in raise_tool_error for non-serializable fallback
- M3: Change pytest.skip to pytest.xfail in test_lights.py to not mask regressions

https://claude.ai/code/session_01MvDDV6qmWosBfGhmTLYzo8
…omeassistant-ai#518)

Part 2 of the isError signaling fix. Updates all remaining E2E test
files that expect error responses to use safe_call_tool, which handles
both legacy dict returns and new ToolError exceptions.

Updated test files:
- test_config_entry_flow: 2 error-expecting tests
- test_helper_crud: 3 error tests + wait_for_entity_registration helper
- test_entity_management: 2 error-expecting tests
- test_file_operations: 2 helper functions using parse_mcp_result
- test_hacs: 3 error-expecting tests
- test_integration_management: 1 error-expecting test
- test_device_registry: 4 error-expecting tests
- test_voice_assistant: 2 error-expecting tests

Total: ~28 error-expecting assertions across 8 files

Depends on PR 1 which provides safe_call_tool infrastructure.

https://claude.ai/code/session_01MvDDV6qmWosBfGhmTLYzo8
…ant-ai#518)

Migrate remaining tool modules to use ToolError/raise_tool_error for
MCP protocol-level error signaling. Includes updated unit tests for
voice assistant and entity tools.

https://claude.ai/code/session_01MvDDV6qmWosBfGhmTLYzo8
…omeassistant-ai#518)

Now that all tools are migrated to use ToolError, flip the default
behavior of exception_to_structured_error to raise by default.

This completes the ToolError migration - all error paths now signal
errors at the MCP protocol level with isError=true.

https://claude.ai/code/session_01MvDDV6qmWosBfGhmTLYzo8
@kingpanther13
kingpanther13 force-pushed the claude/issue-518-part5-flip-default-U70YN branch from 4842dfc to c9d0493 Compare February 7, 2026 19:18
kingpanther13 and others added 2 commits February 14, 2026 15:56
Resolve all merge conflicts from upstream/master (Parts 1-4 merged).
Address Gemini review comments:
- Context propagation now handled by upstream's context=context params
  (fixes comments #1, #2, #3 on helpers.py)
- Replace asyncio.sleep(1.0) with polling loop in test_entity_rename.py
  (fixes comment #4)

Fix callers that assign exception_to_structured_error() result to add
explicit raise_error=False (tools_config_dashboards, tools_search) since
the default is now True.

Fix duplicate ToolError imports in tools_config_helpers and tools_entities.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…_entities

Merge artifact from upstream integration — duplicate
`from fastmcp.exceptions import ToolError` lines.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@kingpanther13
kingpanther13 marked this pull request as ready for review February 14, 2026 21:09
@kingpanther13
kingpanther13 requested a review from a team February 14, 2026 21:09
@kingpanther13
kingpanther13 enabled auto-merge (squash) February 14, 2026 21:10

@sergeykad sergeykad left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Consider adding a suggestions parameter to exception_to_structured_error to simplify the ~18 call sites that follow this pattern:

# Current: 3 lines repeated across 18 call sites
error_response = exception_to_structured_error(e, context=ctx, raise_error=False)
error_response["error"]["suggestions"] = ["Check connection", ...]
raise_tool_error(error_response)

# Proposed: 1 line
exception_to_structured_error(e, context=ctx, suggestions=["Check connection", ...])

The function embeds the suggestions in the structured error dict before raising. Each of these call sites collapses to one line.

raise_error=False still has one legitimate use: _fetch_state inside ha_get_states needs a classified error dict back (not a raise) because it runs inside asyncio.gather and collects per-entity errors into a bulk response. That case cannot be simplified away.

Address review feedback from @sergeykad — collapse the repeated 3-line
pattern (get error dict, insert suggestions, raise) into a single call
by adding an optional `suggestions` parameter to
`exception_to_structured_error()`. This simplifies ~15 call sites across
7 tool modules while preserving behavior.

The `raise_error=False` path remains for `_fetch_state` in ha_get_states
which collects per-entity errors inside asyncio.gather.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@kingpanther13

kingpanther13 commented Feb 14, 2026

Copy link
Copy Markdown
Member Author

@sergeykad Addressed in 6fcea2e — added suggestions parameter to exception_to_structured_error().

What changed:

  • exception_to_structured_error() now accepts suggestions: list[str] | None = None — embeds them in error_response["error"]["suggestions"] before raising/returning
  • ~15 call sites simplified across tools_config_automations, tools_config_dashboards, tools_hacs, tools_mcp_component, tools_search, and tools_service
  • Sites that only needed suggestions + raise collapsed from 5-7 lines → 1 call
  • Sites that also need add_timezone_metadata collapsed from 6-7 lines → 3 lines (suggestions embedded inline, still raise_error=False for the tz step)
  • 3 new unit tests: test_suggestions_embedded_when_raising, test_suggestions_embedded_when_returning, test_no_suggestions_when_none

Left unchanged (as noted in your review):

  • _fetch_state inside ha_get_states — legitimate raise_error=False for asyncio.gather bulk collection
  • 2 branching sites (ha_config_remove_automation, ha_get_state) where create_resource_not_found_error / create_entity_not_found_error shares the same suggestions block — refactoring these would make the code less clear

All 610 unit tests pass, ruff check clean.

@kingpanther13
kingpanther13 merged commit 907c176 into homeassistant-ai:master Feb 15, 2026
12 checks passed
@github-actions

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 Tuesday release:
📖 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.

[BUG] Tool errors not signaled via isError in MCP protocol responses

3 participants