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
- Pass
by_alias=True at all four model_dump(include=...) sites that build list params from an MCP request.
- 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).
Found while adding property-based tests for the MCP tool boundary (
tests/mcp/test_fuzz_tools.py).Every
and/or/notfilter sent to an MCP list operation comes back asinternal_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_andNotFilter.not_(src/kitaru/api_models/v1/filter.py:44-65) are declared withField(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 noby_alias=True:src/kitaru/mcp/tools/registry.py:47and:72src/kitaru/mcp/tools/activity.py:43src/kitaru/mcp/tools/review.pyWithout
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 thatValidationErrorand maps it toMCPOutputValidationError, so the caller is told "The Kitaru response failed MCP output validation" with codeinternal_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
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": [...]}andWorkerListParams.model_validatethen accepts it.Fix
by_alias=Trueat all fourmodel_dump(include=...)sites that build list params from an MCP request.ValidationErrorasMCPOutputValidationErrorin_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)) intests/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, sincehypothesis_jsonschemacannot follow the$defsrecursion).