You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Bug] validate_tool_use_result_messages raises false positive when tool_use is not followed by tool_result #2960
validate_tool_use_result_messages() in src/mcp/server/validation.py:83-87 raises a false ValueError when a tool_use block in the previous message is not followed by a tool_result block in the last message.
Spec Context
Per SEP-1577:
tool_result blocks MUST be preceded by a tool_use block ✅ (already checked)
There is no requirement that tool_use must be followed by tool_result
A plain text response after a tool_use is valid.
Reproduction
frommcp.typesimportSamplingMessage, TextContent, ToolUseContentfrommcp.server.validationimportvalidate_tool_use_result_messagesmsg1=SamplingMessage(role="assistant", content=[
TextContent(type="text", text="Hold on..."),
ToolUseContent(type="tool_use", id="abc", name="search", input={"q": "test"}),
])
msg2=SamplingMessage(role="user", content=[
TextContent(type="text", text="Thanks, no results needed"),
])
validate_tool_use_result_messages([msg1, msg2])
# ValueError: ids of tool_result blocks and tool_use blocks from previous message do not match# This is a false positive — msg2 has NO tool_result at all!
Root Cause
Line 83 runs the ID-matching check outside the if has_tool_results: block:
# L83 — runs unconditionally after the has_tool_results blockifhas_previous_tool_useandprevious_content:
tool_use_ids= {c.idforcinprevious_contentifc.type=="tool_use"}
tool_result_ids= {c.tool_use_idforcinlast_contentifc.type=="tool_result"}
iftool_use_ids!=tool_result_ids: # tool_result_ids is empty → mismatchraiseValueError(...)
I checked this against the current draft sampling spec and SEP-1577 before opening a PR. It looks like the current validator is enforcing the documented tool-use/result balance rule rather than a false positive.
The draft spec's “Tool Use and Result Balance” section says an assistant message with ToolUseContent must be followed by a user message made of matching ToolResultContent blocks before any other message. SEP-1577 has the same balance requirement for CreateMessageRequest.messages.
I can see one possible small improvement: if maintainers want a clearer diagnostic, the SDK could distinguish “previous assistant tool_use was not followed by any tool_result message” from the current generic id-mismatch error. But accepting plain text after an unresolved tool_use seems contrary to the current sampling spec.
validate_tool_use_result_messages() now returns early when the final sampling message has no tool_result content, so a prior tool_use is not treated as requiring the next message to be a result.
Added a regression test for the reported case: assistant text + tool_use, followed by ordinary user text.
Validation run locally:
UV_CACHE_DIR=/private/tmp/uv-cache uv run pytest tests/server/test_validation.py -q
UV_CACHE_DIR=/private/tmp/uv-cache uv run ruff check src/mcp/server/validation.py tests/server/test_validation.py
UV_CACHE_DIR=/private/tmp/uv-cache uv run ruff format --check src/mcp/server/validation.py tests/server/test_validation.py
I tried opening the upstream PR from the fork, but GitHub returned a repository permission error for the PR-create mutation/API from this account. Happy to reopen if permissions change; the branch is ready for review.
Reviewed all three open PRs against validate_tool_use_result_messages — worth flagging that they are not three implementations of the same fix. They represent two mutually exclusive policy answers to the question @Sehlani042 raised:
#3053 takes the position that the current behavior is spec-correct (tool_use must always be followed by a matching tool_result) and just gives it a clearer, dedicated error message ("tool_use blocks must be followed by matching tool_result blocks") instead of the confusing generic id-mismatch message — it does not change what gets rejected.
#2981 and #2962 take the opposite position — both gate the id-matching check on has_tool_results, so a plain-text message following an unresolved tool_use is now accepted rather than rejected. I traced through both diffs: despite looking different (#2981 removes the inner if has_previous_tool_use and previous_content: guard and de-indents its body into the outer has_tool_results block, #2962 adds an explicit if has_tool_results and has_previous_tool_use and previous_content:), they are semantically identical in effect once you account for the earlier if not has_previous_tool_use: raise check that already guarantees both conditions are true by that point. #2962 is the more minimal/readable diff of the two if this is the direction taken.
Given the docstring for this function documents its own rules (2) and (3) as being about "tool_result messages" specifically (i.e. conditioned on has_tool_results), the current code is inconsistent with its own stated contract regardless of which spec interpretation is correct — that part is an objective bug either way. But whether the fix should be "relax the check" (#2981/#2962) or "keep enforcing it, just with a clearer error" (#3053) depends entirely on resolving whether SEP-1577 actually mandates the balance requirement unconditionally, which is a maintainer/spec-owner call, not something the PRs themselves can settle. Might be worth resolving that question explicitly before merging any of the three, since merging the wrong one is a real spec-compliance regression in either direction.
The check here is enforcing the spec rather than misfiring: the sampling spec's Tool Use and Result Balance section, and SEP-1577 itself, say every assistant message containing ToolUseContent MUST be followed by a user message consisting entirely of matching ToolResultContent blocks before any other message, so a text-only turn after an unresolved tool_use is invalid input to create_message and the validator is right to reject it. I agree the generic id-mismatch error is a confusing way to say that, and #3053 tracks giving this case a dedicated message. Feel free to reopen if the spec wording changes or if there's a case here I've misread.
Activity
Sehlani042 commentedon Jul 2, 2026
I checked this against the current draft sampling spec and SEP-1577 before opening a PR. It looks like the current validator is enforcing the documented tool-use/result balance rule rather than a false positive.
The draft spec's “Tool Use and Result Balance” section says an assistant message with
ToolUseContentmust be followed by a user message made of matchingToolResultContentblocks before any other message. SEP-1577 has the same balance requirement forCreateMessageRequest.messages.Links:
I can see one possible small improvement: if maintainers want a clearer diagnostic, the SDK could distinguish “previous assistant tool_use was not followed by any tool_result message” from the current generic id-mismatch error. But accepting plain text after an unresolved
tool_useseems contrary to the current sampling spec.tarunag10 commentedon Jul 3, 2026
I prepared a focused fix branch for this:
https://github.com/tarunag10/python-sdk/tree/fix-tool-use-result-validation
What it changes:
validate_tool_use_result_messages()now returns early when the final sampling message has notool_resultcontent, so a priortool_useis not treated as requiring the next message to be a result.tool_use, followed by ordinary user text.Validation run locally:
UV_CACHE_DIR=/private/tmp/uv-cache uv run pytest tests/server/test_validation.py -qUV_CACHE_DIR=/private/tmp/uv-cache uv run ruff check src/mcp/server/validation.py tests/server/test_validation.pyUV_CACHE_DIR=/private/tmp/uv-cache uv run ruff format --check src/mcp/server/validation.py tests/server/test_validation.pyI tried opening the upstream PR from the fork, but GitHub returned a repository permission error for the PR-create mutation/API from this account. Happy to reopen if permissions change; the branch is ready for review.
STiFLeR7 commentedon Jul 10, 2026
Reviewed all three open PRs against
validate_tool_use_result_messages— worth flagging that they are not three implementations of the same fix. They represent two mutually exclusive policy answers to the question @Sehlani042 raised:#3053 takes the position that the current behavior is spec-correct (tool_use must always be followed by a matching tool_result) and just gives it a clearer, dedicated error message (
"tool_use blocks must be followed by matching tool_result blocks") instead of the confusing generic id-mismatch message — it does not change what gets rejected.#2981 and #2962 take the opposite position — both gate the id-matching check on
has_tool_results, so a plain-text message following an unresolvedtool_useis now accepted rather than rejected. I traced through both diffs: despite looking different (#2981 removes the innerif has_previous_tool_use and previous_content:guard and de-indents its body into the outerhas_tool_resultsblock, #2962 adds an explicitif has_tool_results and has_previous_tool_use and previous_content:), they are semantically identical in effect once you account for the earlierif not has_previous_tool_use: raisecheck that already guarantees both conditions are true by that point. #2962 is the more minimal/readable diff of the two if this is the direction taken.Given the docstring for this function documents its own rules (2) and (3) as being about "tool_result messages" specifically (i.e. conditioned on
has_tool_results), the current code is inconsistent with its own stated contract regardless of which spec interpretation is correct — that part is an objective bug either way. But whether the fix should be "relax the check" (#2981/#2962) or "keep enforcing it, just with a clearer error" (#3053) depends entirely on resolving whether SEP-1577 actually mandates the balance requirement unconditionally, which is a maintainer/spec-owner call, not something the PRs themselves can settle. Might be worth resolving that question explicitly before merging any of the three, since merging the wrong one is a real spec-compliance regression in either direction.maxisbey commentedon Aug 14, 2026
The check here is enforcing the spec rather than misfiring: the sampling spec's Tool Use and Result Balance section, and SEP-1577 itself, say every assistant message containing
ToolUseContentMUST be followed by a user message consisting entirely of matchingToolResultContentblocks before any other message, so a text-only turn after an unresolvedtool_useis invalid input tocreate_messageand the validator is right to reject it. I agree the generic id-mismatch error is a confusing way to say that, and #3053 tracks giving this case a dedicated message. Feel free to reopen if the spec wording changes or if there's a case here I've misread.AI Disclaimer