Skip to content

fix(mcp): deliver a successful call's filter output as-is - #6116

Open
BlueX888 wants to merge 1 commit into
pipecat-ai:mainfrom
BlueX888:fix/prep-mcp-output-filter-clobbers-result
Open

BlueX888 wants to merge 1 commit into
pipecat-ai:mainfrom
BlueX888:fix/prep-mcp-output-filter-clobbers-result

Conversation

@BlueX888

Copy link
Copy Markdown

Please describe the changes in your PR. If it is addressing an issue, please reference that as well.

Fixes #6111

Summary

MCPClient.tools_output_filters is documented as dict[str, Callable[[Any], Any]], with each filter returning "the processed output (any type)" (src/pipecat/services/mcp_service.py:216, :225-226). In practice, a filter that returned a non-string value (e.g. a parsed dict) or the empty string had its output discarded, and the model received Sorry, could not call the mcp tool even though session.call_tool() had succeeded.

Why

After the filter is applied, the result was gated by if isinstance(response, str) and response:. Any non-string value and the empty string fell into the else, which replaces the response with error_msg or "Sorry, could not call the mcp tool" — and error_msg is None for a successful call. That check was aimed at filter failures (the adjacent except-handler sets response = "" when a filter raises), but it also caught every successful call whose legitimate output was empty or non-string, making a successful tool call indistinguishable from a failed one for the model. The framework already supports non-string tool results end-to-end: the result callback takes result: Any (src/pipecat/services/llm_service.py:105-106) and the LLM context aggregator serializes non-string results with json.dumps (src/pipecat/processors/aggregators/llm_response_universal.py:2119).

Changes

  • src/pipecat/services/mcp_service.py: track whether a filter ran without raising (filter_applied). On a successful call (error_msg is None), a filter that ran without error owns the response and its output is delivered as-is, whatever the type. A failed call, or a filter that raises, still reports error_msg or "Sorry, could not call the mcp tool". Behavior for calls without a filter and for failed calls is unchanged.
  • tests/test_mcp_service.py: tests for a non-string filter output (a dict), an empty-string filter output (fully redacted), and a filter that raises still reading as the stock failure line. The first two fail on main without the fix.
  • changelog/6111.fixed.md: changelog fragment.

Testing

On the unpatched tree, the two new regression tests fail:

FAILED tests/test_mcp_service.py::TestTools::test_handler_delivers_a_non_string_filter_output
FAILED tests/test_mcp_service.py::TestTools::test_handler_delivers_an_empty_filter_output
E           AssertionError: expected await not found.
E           Expected: mock({'result': 'tool_a-RESULT'})
E             Actual: mock('Sorry, could not call the mcp tool')
========================= 2 failed, 1 passed in 0.54s ==========================

With the fix:

  • uv run pytest tests/test_mcp_service.py — 57 passed, 5 subtests passed
  • uv run pytest tests/test_keenable_web_search.py — 11 passed (the only other test file exercising MCPClient)
  • uv run pytest — 7187 passed, 64 failed, 63 skipped; the 64 failures are in 5 network-dependent files unrelated to MCP and reproduce identically (same files, same count) on a pristine origin/main checkout
  • uv run ruff check — All checks passed!
  • uv run ruff format --check — 1596 files already formatted
  • uv run towncrier build --draft --version Unreleased — the new fragment renders under Fixed

This branch has not been deployed

No deployments
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.

MCPClient tools_output_filters: non-string or empty-string filter results are discarded and the model is told the tool call failed

1 participant