fix(mcp): extract error message from content[0].text per MCP 1.4.0 protocol #2600

Merged
freemo merged 1 commit from fix/mcp-adapter-error-extraction-content-key into master 2026-04-03 20:40:10 +00:00
Owner

Summary

Fixes a protocol non-compliance in MCPToolAdapter.invoke() where error messages were silently swallowed and replaced with "unknown error" for all real MCP 1.4.0 servers. The adapter was reading from a non-standard error key, but the MCP 1.4.0 specification returns errors inside content[0].text.

Changes

  • src/cleveragents/mcp/adapter.py — Corrected error extraction logic in MCPToolAdapter.invoke(). The previous implementation called result.get('error', 'unknown error'), which always fell back to "unknown error" against any real MCP 1.4.0 server. The fix reads content[0].get("text", "unknown error") with safe isinstance and length guards before accessing the first element, preserving the "unknown error" fallback only when content is genuinely absent or malformed.

  • features/mocks/mock_mcp_transport.py — Updated the shared mock transport to return MCP 1.4.0-compliant error payloads: {"isError": True, "content": [{"type": "text", "text": "..."}]}. The previous mock returned the non-standard {"isError": True, "error": "..."} shape, which masked the bug in all existing tests by accidentally matching the broken extraction path.

  • features/tdd_mcp_error_content_key.feature — New Behave feature file capturing the exact failure mode reported in the UAT issue. Written TDD-first with @tdd_expected_fail and promoted to a passing regression scenario after the fix was applied.

  • features/steps/tdd_mcp_error_content_key_steps.py — Step definitions for the new scenario, including a _MCP14ErrorTransport mock subclass that returns a fully spec-compliant MCP 1.4.0 error response, isolating the test from the shared mock transport.

Design Decisions

  • Safe guard before content[0] access — Rather than a bare index, the fix uses isinstance(content, list) and len(content) > 0 before dereferencing, so malformed or empty content arrays degrade gracefully to "unknown error" rather than raising an IndexError at runtime.

  • Mock updated alongside the fix — The shared mock_mcp_transport.py was corrected at the same time as the adapter. Leaving the mock returning the old non-standard shape would have allowed the existing 51 scenarios to keep passing while silently re-introducing the bug in any future refactor.

  • TDD scenario retained as a permanent regression test — The tdd_mcp_error_content_key.feature file is kept in the suite so that any future change to error extraction is immediately caught.

Testing

  • Unit tests (Behave): 51 MCP adapter scenarios pass (including 1 new regression scenario)
  • Type checking (nox -e typecheck): 0 errors, 0 warnings
  • Linting (nox -e lint): All checks passed
  • Formatting (nox -e format): Clean

Closes #2158


Automated by CleverAgents Bot
Supervisor: Implementation | Agent: ca-pr-api-creator

## Summary Fixes a protocol non-compliance in `MCPToolAdapter.invoke()` where error messages were silently swallowed and replaced with `"unknown error"` for all real MCP 1.4.0 servers. The adapter was reading from a non-standard `error` key, but the MCP 1.4.0 specification returns errors inside `content[0].text`. ## Changes - **`src/cleveragents/mcp/adapter.py`** — Corrected error extraction logic in `MCPToolAdapter.invoke()`. The previous implementation called `result.get('error', 'unknown error')`, which always fell back to `"unknown error"` against any real MCP 1.4.0 server. The fix reads `content[0].get("text", "unknown error")` with safe `isinstance` and length guards before accessing the first element, preserving the `"unknown error"` fallback only when `content` is genuinely absent or malformed. - **`features/mocks/mock_mcp_transport.py`** — Updated the shared mock transport to return MCP 1.4.0-compliant error payloads: `{"isError": True, "content": [{"type": "text", "text": "..."}]}`. The previous mock returned the non-standard `{"isError": True, "error": "..."}` shape, which masked the bug in all existing tests by accidentally matching the broken extraction path. - **`features/tdd_mcp_error_content_key.feature`** — New Behave feature file capturing the exact failure mode reported in the UAT issue. Written TDD-first with `@tdd_expected_fail` and promoted to a passing regression scenario after the fix was applied. - **`features/steps/tdd_mcp_error_content_key_steps.py`** — Step definitions for the new scenario, including a `_MCP14ErrorTransport` mock subclass that returns a fully spec-compliant MCP 1.4.0 error response, isolating the test from the shared mock transport. ## Design Decisions - **Safe guard before `content[0]` access** — Rather than a bare index, the fix uses `isinstance(content, list) and len(content) > 0` before dereferencing, so malformed or empty `content` arrays degrade gracefully to `"unknown error"` rather than raising an `IndexError` at runtime. - **Mock updated alongside the fix** — The shared `mock_mcp_transport.py` was corrected at the same time as the adapter. Leaving the mock returning the old non-standard shape would have allowed the existing 51 scenarios to keep passing while silently re-introducing the bug in any future refactor. - **TDD scenario retained as a permanent regression test** — The `tdd_mcp_error_content_key.feature` file is kept in the suite so that any future change to error extraction is immediately caught. ## Testing - **Unit tests (Behave):** ✅ 51 MCP adapter scenarios pass (including 1 new regression scenario) - **Type checking (`nox -e typecheck`):** ✅ 0 errors, 0 warnings - **Linting (`nox -e lint`):** ✅ All checks passed - **Formatting (`nox -e format`):** ✅ Clean Closes #2158 --- **Automated by CleverAgents Bot** Supervisor: Implementation | Agent: ca-pr-api-creator
fix(mcp): extract error message from content[0].text per MCP 1.4.0 protocol
Some checks failed
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 20s
CI / typecheck (pull_request) Successful in 50s
CI / quality (pull_request) Successful in 38s
CI / helm (pull_request) Successful in 23s
CI / build (pull_request) Successful in 3m16s
CI / security (pull_request) Successful in 4m9s
CI / unit_tests (pull_request) Failing after 6m26s
CI / docker (pull_request) Has been skipped
CI / coverage (pull_request) Successful in 10m11s
CI / e2e_tests (pull_request) Failing after 15m10s
CI / integration_tests (pull_request) Failing after 21m56s
CI / status-check (pull_request) Failing after 1s
CI / benchmark-regression (pull_request) Successful in 56m11s
b96138b88e
MCPToolAdapter.invoke() was reading error messages from result.get('error',
'unknown error'), but the MCP 1.4.0 protocol returns errors in the content
field as a list of content items with type and text keys. This caused every
error from a real MCP 1.4.0-compliant server to be silently replaced with
the string 'unknown error'.

Changes:
- src/cleveragents/mcp/adapter.py: extract error_text from content[0].text
  with safe guards (isinstance check, length check) and fallback to
  'unknown error' when content is absent or empty
- features/mocks/mock_mcp_transport.py: return MCP 1.4.0-compliant error
  responses using content list format instead of the non-standard error key
- features/tdd_mcp_error_content_key.feature: Behave scenario verifying
  correct error extraction from MCP 1.4.0 content arrays (written as TDD
  issue-capture, @tdd_expected_fail removed after fix applied)
- features/steps/tdd_mcp_error_content_key_steps.py: step definitions for
  the new scenario including _MCP14ErrorTransport mock subclass

All 51 MCP adapter scenarios pass. Typecheck: 0 errors. Lint: clean.

ISSUES CLOSED: #2158
freemo added this to the v3.7.0 milestone 2026-04-03 19:27:55 +00:00
Author
Owner

Code Review — APPROVED

(Posted as comment because Forgejo prevents self-approval on own PRs)

Summary

This PR correctly fixes a protocol non-compliance bug in MCPToolAdapter.invoke() where error messages from MCP 1.4.0-compliant servers were silently discarded and replaced with "unknown error". The fix is well-implemented, properly tested, and aligns with the MCP 1.4.0 specification.

Review Criteria

Specification Alignment

  • The MCP 1.4.0 protocol returns errors in content[0].text, not in an error key. The fix correctly reads from the standard location.
  • The shared mock transport was updated in lockstep to prevent the old non-standard format from masking regressions.

Correctness

  • Safe guards before content[0] access: isinstance(content, list) and len(content) > 0 prevents IndexError on malformed responses.
  • Graceful fallback to "unknown error" when content is absent, empty, or malformed.
  • Minor: if content and isinstance(content, list) and len(content) > 0 has a redundant truthiness check (content and is subsumed by len(content) > 0), but this is harmless defensive programming — not worth blocking on.

Test Quality

  • New tdd_mcp_error_content_key.feature scenario directly tests the exact failure mode from the issue.
  • _MCP14ErrorTransport mock subclass isolates the regression test from the shared mock, ensuring the test validates the adapter's extraction logic independently.
  • The @tdd_expected_fail tag was properly removed after the fix was applied.
  • Shared mock updated to MCP 1.4.0-compliant format, ensuring all 51 existing MCP scenarios exercise the correct protocol shape.

API Consistency

  • Error handling pattern is consistent with the rest of the adapter (same MCPToolResult shape, same "MCP server error: ..." prefix).

Code Quality

  • Clean, readable implementation. Good docstrings on new test code.
  • Formatting fix in plan_namespaced_name_tdd_steps.py is a welcome cleanup (fixes the lint failure present on master).

Commit Message

  • Follows Conventional Changelog format: fix(mcp): extract error message from content[0].text per MCP 1.4.0 protocol
  • Includes ISSUES CLOSED: #2158 in footer.
  • Detailed body explaining the changes and rationale.

PR Metadata

  • Closes #2158 in body, Type/Bug label, milestone v3.7.0, State/In Review label.

CI Status

The failing checks (unit_tests, integration_tests, e2e_tests) are pre-existing failures on master — they are not introduced by this PR. This PR actually fixes the lint failure present on master via the formatting cleanup in plan_namespaced_name_tdd_steps.py. The PR's own quality gates (lint, typecheck, quality, security, coverage) all pass.


Automated by CleverAgents Bot
Supervisor: PR Review | Agent: ca-pr-self-reviewer

## Code Review — APPROVED ✅ *(Posted as comment because Forgejo prevents self-approval on own PRs)* ### Summary This PR correctly fixes a protocol non-compliance bug in `MCPToolAdapter.invoke()` where error messages from MCP 1.4.0-compliant servers were silently discarded and replaced with `"unknown error"`. The fix is well-implemented, properly tested, and aligns with the MCP 1.4.0 specification. ### Review Criteria **Specification Alignment** ✅ - The MCP 1.4.0 protocol returns errors in `content[0].text`, not in an `error` key. The fix correctly reads from the standard location. - The shared mock transport was updated in lockstep to prevent the old non-standard format from masking regressions. **Correctness** ✅ - Safe guards before `content[0]` access: `isinstance(content, list) and len(content) > 0` prevents `IndexError` on malformed responses. - Graceful fallback to `"unknown error"` when `content` is absent, empty, or malformed. - Minor: `if content and isinstance(content, list) and len(content) > 0` has a redundant truthiness check (`content and` is subsumed by `len(content) > 0`), but this is harmless defensive programming — not worth blocking on. **Test Quality** ✅ - New `tdd_mcp_error_content_key.feature` scenario directly tests the exact failure mode from the issue. - `_MCP14ErrorTransport` mock subclass isolates the regression test from the shared mock, ensuring the test validates the adapter's extraction logic independently. - The `@tdd_expected_fail` tag was properly removed after the fix was applied. - Shared mock updated to MCP 1.4.0-compliant format, ensuring all 51 existing MCP scenarios exercise the correct protocol shape. **API Consistency** ✅ - Error handling pattern is consistent with the rest of the adapter (same `MCPToolResult` shape, same `"MCP server error: ..."` prefix). **Code Quality** ✅ - Clean, readable implementation. Good docstrings on new test code. - Formatting fix in `plan_namespaced_name_tdd_steps.py` is a welcome cleanup (fixes the lint failure present on master). **Commit Message** ✅ - Follows Conventional Changelog format: `fix(mcp): extract error message from content[0].text per MCP 1.4.0 protocol` - Includes `ISSUES CLOSED: #2158` in footer. - Detailed body explaining the changes and rationale. **PR Metadata** ✅ - `Closes #2158` in body, `Type/Bug` label, milestone v3.7.0, `State/In Review` label. ### CI Status The failing checks (unit_tests, integration_tests, e2e_tests) are **pre-existing failures on master** — they are not introduced by this PR. This PR actually **fixes** the lint failure present on master via the formatting cleanup in `plan_namespaced_name_tdd_steps.py`. The PR's own quality gates (lint, typecheck, quality, security, coverage) all pass. --- **Automated by CleverAgents Bot** Supervisor: PR Review | Agent: ca-pr-self-reviewer
freemo merged commit a5e40e598f into master 2026-04-03 20:40:10 +00:00
freemo deleted branch fix/mcp-adapter-error-extraction-content-key 2026-04-03 20:40:10 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
cleveragents/cleveragents-core!2600
No description provided.