Skip to content

fix: drop non-object structuredContent after a jmespath filter - #873

Merged
zereight merged 2 commits into
mainfrom
fix/structured-content-array-guard
Oct 19, 2026
Merged

zereight merged 2 commits into
mainfrom
fix/structured-content-array-guard

Conversation

@zereight

@zereight zereight commented Oct 19, 2026 •

Copy link
Copy Markdown
Owner

Why

applyJmespathToToolResult filters structuredContent with the same expression as the text content and returns whatever JMESPath produces. A filter such as items[].title, keys(@), length(items), or a missing path turns it into an array, a number, or null. The MCP spec says structuredContent is a JSON object, and the SDK's server checks every tools/call result against CallToolResultSchema (@modelcontextprotocol/sdk 1.31, Server.setRequestHandler). That check runs whether or not the tool has an outputSchema. A non-object value therefore fails the whole call with Invalid tools/call result, the same expected record, received array error that #675 reports, and the model gets no result to work with.

No handler sets structuredContent today, so this path is not reachable on main. This is a guard for the first tool that does, not a fix for the report in #675.

What changed

  • After JMESPath runs, structuredContent is kept only when the result is a JSON object.
  • An array, a scalar, or null drops the structuredContent key. The filtered JSON is still in the text content block.
  • A regression test filters a result with object structuredContent through items[].title, missing, and length(items), and checks each result with the SDK's CallToolResultSchema. It fails on main and passes here.

Tradeoffs

Dropping keeps the text block and structuredContent saying the same thing whenever both are present. Wrapping the value as { "result": [...] } would make up a shape that no outputSchema describes. Keeping the unfiltered object would bring back fields the caller filtered out. structuredContent is optional when a tool has no outputSchema, and no tool here declares one.

Blast Radius

None for current tools, because none of them return structuredContent. Results without structuredContent, error results, and filters that return an object are unchanged.

Verification

  • npm run build, npx tsc --noEmit, npm run lint (0 errors), and prettier --check on the changed files passed.
  • node --import tsx/esm --test test/utils/jmespath-tool-result.test.ts passed. Without the fix, the new test fails because items[].title leaves structuredContent as ["a","b"].
  • npm run test:mock passed locally (1780 pass, 0 fail) and in CI.

Refs #675

MCP structuredContent must be a JSON object, and the SDK server checks
every tools/call result against CallToolResultSchema. A filter such as
items[].title, a missing path, or length(items) turned it into an array,
null, or a number, which would fail the whole call with "expected record,
received array". Keep structuredContent only when the filtered value is an
object; the filtered JSON is still in the text content.

No handler sets structuredContent today, so this guards a latent path.

Refs #675
@coderabbitai

coderabbitai Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 276080b6-805c-46c1-b3de-81f3ef6788a4
📥 Commits

Reviewing files that changed from the base of the PR and between 3cbff1c and 4219d07.

📒 Files selected for processing: 3
  • CHANGELOG.md
  • test/utils/jmespath-tool-result.test.ts
  • utils/jmespath-tool-result.ts

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 6 remain after this review.

📜 Recent review details
⏰ Context from checks skipped due to timeout: 3
  • GitHub Check: docs-build
  • GitHub Check: test
  • GitHub Check: integration-test
📝 Summary

Summary by CodeRabbit

  • Bug Fixes
    • JMESPath-filtered results that are arrays, scalars, or null are no longer returned as structured content. The filtered JSON remains available in the text content.
    • Filtered object results continue to be returned as structured content.

JMESPath filtering now retains structuredContent only when the filtered value is a non-null, non-array object. Other filtered values remain in text content. Tests cover these result shapes and validate the results against the tool result schema.

Changes: Structured tool result filtering

Layer / File(s) Summary
Filter structured tool results
utils/jmespath-tool-result.ts, test/utils/jmespath-tool-result.test.ts, CHANGELOG.md
The transformation retains filtered structuredContent for object results and omits it for array, null, and numeric results. Tests check the filtered content and validate each result against CallToolResultSchema. The changelog records the behavior.

Priority: ⬇️ Low

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix

Merge Risk: ⚪ Minimal · up to 4219d

This fix prevents schema-validation failures for filtered non-object results. No actionable merge-blocking risk remains.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 4 functions across 2 files. (1 skipped: 1… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Description check Passed The description clearly explains the JMESPath structuredContent issue, the implemented fix, tests, and verification results.
Title check Passed The title clearly and concisely describes the main change: dropping non-object structuredContent after a JMESPath filter.
Full details: Docstring Coverage

Explanation

Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 4 functions across 2 files. (1 skipped: 1 unsupported.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

  • Fix all pre-merge checks with AI
✨ Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
✨ Simplify code
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@zereight zereight left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Reviewed at 4219d07. No P0/P1 findings. The guard only drops structuredContent when the filtered value is not a JSON object, the text block still carries the filtered JSON, and masking and compaction already handle a missing key. No current tool sets structuredContent, so there is no visible change today.

@zereight
zereight merged commit 8c7a8ab into main Oct 19, 2026
12 checks passed
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.

1 participant