Repository navigation
Tool handlers returning UserList lose native content blocks despite Sequence return annotation #3692
Description
Initial Checks
- Reproduced on the newest 2.x release, mcp 2.3.0, and current main.
- Searched existing issues and open PRs. Prompt handlers returning UserList are rendered as one text message #3643 reports a similar symptom in prompt rendering; this report concerns tool result conversion.
Release line
2.x (current stable)
Description
When checking native content returns for MCP tools, I found that a tool annotated Sequence[TextContent] behaves differently depending on the concrete sequence returned. A list produces two native text blocks; collections.UserList produces one text block containing a quoted Python representation of the objects. The call reports is_error=False.
Expected: the returned native content blocks should be preserved for a supported non-string sequence. If tools intentionally support only list/tuple, the Sequence[...] annotation handling should be consistent with that limitation.
On current main, _returns_content() recognizes the Sequence annotation and selects content-only output, while _convert_to_content() only unrolls list/tuple. The UserList therefore reaches the JSON fallback instead of being converted block by block. This is a separate path from Prompt.render() in #3643.
Example Code
from collections import UserList
from collections.abc import Sequence
import anyio
from mcp import Client
from mcp.server.mcpserver import MCPServer
from mcp.types import TextContent
server = MCPServer("probe")
@server.tool()
def normal_list() -> Sequence[TextContent]:
return [TextContent(type="text", text="one"), TextContent(type="text", text="two")]
@server.tool()
def user_list() -> Sequence[TextContent]:
return UserList([TextContent(type="text", text="one"), TextContent(type="text", text="two")])
async def main():
async with Client(server) as client:
for name in ["normal_list", "user_list"]:
result = await client.call_tool(name)
print(name, "blocks=", len(result.content), "texts=", [x.text for x in result.content], "is_error=", result.is_error)
anyio.run(main)The list tool returns blocks=2, texts ['one', 'two']. The UserList tool returns blocks=1; its sole text is a quoted representation beginning "[TextContent(type='text', text='one', ...) rather than the original two text blocks. Both report is_error=False.
Python & MCP Python SDK
- macOS, Python 3.14.5.
- Latest stable: mcp 2.3.0.
- Also reproduced from main at
c9356a2a8fb805ac2202e44c05371cf005edc923, usinguv run --frozen --no-default-groupsand an in-memoryClient(server). - No model API or network transport is needed for the reproduction.
Possible scope
If non-string Sequence tool returns are intended to work, align runtime content conversion with the annotation handling. Regression cases should cover UserList, list/tuple, empty sequences, and ensure strings and binary values keep their existing behavior. I would be interested in contributing a focused fix if you accept the issue; no patch has been prepared yet.
AI assistance: Codex helped inspect the source and run the local reproductions. This was found in a focused compatibility probe, not a reported production outage.
Activity
Janicebenita commentedon Oct 12, 2026
I reproduced this on current
mainand prepared a focused local fix with public client/server regression coverage for populated and emptyUserListresults, plus guards for string and binary sequence-like values.A clean full Windows run completed with 6,202 passed, 16 skipped, 1 xfailed, and 100% coverage. After adding one final
memoryviewguard test, two full retries hit unrelated five-second transport/server timeouts; each affected test then passed 5/5 in isolation. The changed files pass Ruff and Pyright.I see that the reporter has offered to contribute, so I do not want to pre-empt them. If maintainers and the reporter prefer, I can share the patch or take an assignment under the contribution policy.
AI assistance was used during implementation and verification; I reviewed the patch and executed the reported checks locally.