Skip to content

MCP list tools answer every boolean filter with internal_error #908

Description

@strickvl

Found while adding property-based tests for the MCP tool boundary (tests/mcp/test_fuzz_tools.py).

Every and/or/not filter sent to an MCP list operation comes back as internal_error, so boolean filtering through the MCP tools is completely non-functional and the error message points the user away from the real cause.

What happens

AndFilter.and_, OrFilter.or_ and NotFilter.not_ (src/kitaru/api_models/v1/filter.py:44-65) are declared with Field(alias="and"/"or"/"not"), because the Python names are keywords.

The four handlers that build SDK list params do so with request.model_dump(include={"cursor", "size", "sort", "filter"}) and no by_alias=True:

  • src/kitaru/mcp/tools/registry.py:47 and :72
  • src/kitaru/mcp/tools/activity.py:43
  • the matching line in src/kitaru/mcp/tools/review.py

Without by_alias=True, model_dump() emits the Python field name, so the dumped filter is {"and_": [...]}. WorkerListParams.model_validate (and its siblings) accept only the alias, so validation fails with nine errors. _invoke (src/kitaru/mcp/registry.py:216) catches that ValidationError and maps it to MCPOutputValidationError, so the caller is told "The Kitaru response failed MCP output validation" with code internal_error — a server-side fault report for a perfectly valid client request.

Affected tools: kitaru_registry_read, kitaru_activity_read, kitaru_review_read, at any nesting depth.

Reproduction

import asyncio, sys
sys.path.insert(0, "tests/mcp")
from mcp_fakes import NullClient, build_server_context
from kitaru.mcp.settings import CapabilityMode

request = {"kind": "worker", "operation": "list",
           "filter": {"and": [{"field": "a", "op": "eq", "value": 1}]}}
server, context = build_server_context(NullClient(), mode=CapabilityMode.DESTRUCTIVE)
result = asyncio.run(server.call_tool("kitaru_registry_read", {"request": request}, context))
print(result.structured_content["error"]["code"])  # internal_error

The same request with a bare {"field": "a", "op": "eq", "value": 1} filter reaches the SDK call normally. model_dump(include=..., by_alias=True) produces {"and": [...]} and WorkerListParams.model_validate then accepts it.

Fix

  1. Pass by_alias=True at all four model_dump(include=...) sites that build list params from an MCP request.
  2. Stop labelling an input-side ValidationError as MCPOutputValidationError in _invoke: a request the handler itself cannot marshal is not an output-schema fault.

Tests

The property-based testing PR pins this with test_boolean_filter_reaches_the_sdk (xfail(strict=True)) in tests/mcp/test_fuzz_tools.py, and _drop_boolean_filters() in the same file excludes boolean filter nodes from the generated requests so the property keeps hunting for other faults. Fixing this flips the xfail; remove the marker and the generator exclusion in the same PR (bounded to a small depth, since hypothesis_jsonschema cannot follow the $defs recursion).

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions