Skip to content

fix(mcp): load all pages of the tool list - #10717

Open
Ars1027 wants to merge 4 commits into
AstrBotDevs:masterfrom
Ars1027:fix/mcp-tool-pagination
Open

Ars1027 wants to merge 4 commits into
AstrBotDevs:masterfrom
Ars1027:fix/mcp-tool-pagination

Conversation

@Ars1027

@Ars1027 Ars1027 commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Problem

When an MCP server paginates tools/list, AstrBot reads only the first page. Tools on later pages are missing from the tool list and cannot be called by the model.

Solution

Follow nextCursor until all pages have been read, then replace the cached tool list. Use the SDK's ClientSession.list_tools(cursor=...) so request serialization and tool output schema caching stay with the SDK.

Changes

  • Collect all tool pages in MCPClient.list_tools_and_save(), preserving the first page's metadata.
  • Reject repeated cursors and escape cursor values in error messages.
  • Reject duplicate tool names and provider-facing name collisions before replacing the cache.
  • Check names against other registered MCP servers before publishing the new server's tools. On conflict, preserve existing tools and clean up the new connection.
  • Reserve inactive MCP tool names until their server is disabled.
  • Raise the MCP SDK minimum version to 1.10.0 in both dependency files.
  • Add cross-server registration coverage to the existing tool-manager tests.

Testing

uv run pytest tests/unit/test_mcp_client_schema.py tests/unit/test_func_tool_manager.py -q
ruff format .
ruff check .
  • 34 passed, 20 skipped on Windows. The skipped shell-execution tests require POSIX. Ruff formatting and lint checks passed.
  • Cover concurrent server startup with colliding or distinct names, rejected-client cleanup, no partial registration, inactive-name reservation, and reuse after disabling a server.
  • Earlier integration checks verified second-page tool calls and reconnect behavior against a separate MCP server process, including MCP SDK 1.10.0.
  • An earlier isolated Linux/QQ check loaded both pages and let the model call page_2, returning page-two-ok.

The standalone pagination test file has been removed as requested in review.

Summary by Sourcery

Support reliable MCP tool discovery and registration across paginated listings and conflicting provider-facing names.

Bug Fixes:

  • Load the complete paginated MCP tool list so tools on later pages are available to models.
  • Reject repeated pagination cursors and conflicting normalized tool names while preserving the previous cache when listing fails or validation fails.
  • Prevent MCP tool registration from replacing existing tools when provider-facing names conflict across servers.

Enhancements:

  • Return a combined tool-list result while preserving first-page metadata and maintain atomic MCP tool publication during reconnects.

Build:

  • Raise the minimum MCP SDK dependency to 1.10.0 in project dependency files.

Tests:

  • Add regression coverage for multi-page listings, cursor failures, cancellation, normalized-name collisions, cache preservation, cross-server conflicts, and reconnect behavior.

@chatgpt-codex-connector

Copy link
Copy Markdown
Contributor

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review 🔄 Running since 2026-10-24T03:31:42.393984Z 192a00b PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="astrbot/core/agent/mcp_client.py" line_range="674" />
<code_context>
+                raise RuntimeError(f"MCP server repeated tools/list cursor: {cursor}")
+            seen_cursors.add(cursor)
+            page = await self.session.list_tools(cursor=cursor)
+            tools.extend(page.tools)
+            cursor = page.nextCursor
+
</code_context>
<issue_to_address>
**Normalized tools overwrite each other**

When tools on different pages have distinct MCP names that normalize to the same provider-facing name, `MCPTool` maps both names to the same provider-facing name, and `ToolSet.add_tool()` overwrites one entry; `FunctionToolManager.get_func()` resolves the shared name to the later wrapper, so the model sees an ambiguous name and the earlier tool cannot be called.

Detect colliding provider-facing names and reject or disambiguate them before registering the tools.

Also at `astrbot/core/agent/mcp_client.py:675`.
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 1 finding to address first, and the agent can now discover and potentially invoke tools returned on later MCP pages, whose actions may affect external systems. Reverting stops future discovery, but it cannot undo side effects from calls already made.

Blocking findings: astrbot/core/agent/mcp_client.py:674


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread astrbot/core/agent/mcp_client.py
@Ars1027

Ars1027 commented Oct 24, 2026

Copy link
Copy Markdown
Contributor Author

@sourcery-ai review

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey - I've found 3 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="astrbot/core/agent/mcp_client.py" line_range="678-679" />
<code_context>
+            tools.extend(page.tools)
+            cursor = page.nextCursor
+
+        normalized_names: dict[str, str] = {}
+        for tool in tools:
+            # Match MCPTool's provider-facing name before publishing the cache.
+            normalized_name = re.sub(r"[^A-Za-z0-9_-]+", "_", tool.name)
</code_context>
<issue_to_address>
**Tools from different servers collide**

When two active MCP servers expose distinct tool names that normalize to the same provider-facing name, each `MCPClient` checks only its own list, so `FunctionToolManager` publishes colliding names into one tool set. The tool set keeps or resolves only one matching tool, making the other unavailable or routing a call to the wrong server.

Check normalized names across all active MCP clients before publishing tools to the shared provider-facing tool set.
</issue_to_address>

### Comment 2
<location path="astrbot/core/agent/mcp_client.py" line_range="683" />
<code_context>
+            # Match MCPTool's provider-facing name before publishing the cache.
+            normalized_name = re.sub(r"[^A-Za-z0-9_-]+", "_", tool.name)
+            previous_name = normalized_names.get(normalized_name)
+            if previous_name is not None and previous_name != tool.name:
+                raise ValueError(
+                    f"MCP tool names {previous_name!r} and {tool.name!r} "
</code_context>
<issue_to_address>
**Duplicate tool definitions remain**

When an MCP server returns the same tool name more than once across its pages, the collision check in `list_tools_and_save()` allows repeated identical names, so duplicate wrappers are registered under one provider-facing name. The manager or tool set keeps one definition, hiding the other schema or behavior from callers.

Reject any already-seen normalized name, including when the original tool names are identical.

Also at `astrbot/core/agent/mcp_client.py:675`.
</issue_to_address>

### Comment 3
<location path="astrbot/core/agent/mcp_client.py" line_range="672" />
<code_context>
+        seen_cursors = set()
+        while cursor is not None:
+            if cursor in seen_cursors:
+                raise RuntimeError(f"MCP server repeated tools/list cursor: {cursor}")
+            seen_cursors.add(cursor)
+            page = await self.session.list_tools(cursor=cursor)
</code_context>
<issue_to_address>
**Cursor text forges log entries**

When a remote MCP server returns the same cursor containing CR/LF characters on consecutive pages during a dashboard connection test, `list_tools_and_save()` interpolates the server-controlled cursor verbatim into the `RuntimeError`; the dashboard connection-test path logs its traceback, allowing the server to inject forged lines into AstrBot’s logs.

Escape or sanitize the cursor before including it in exception messages that are logged.
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 3 findings to address first, and tools returned on later pages will now be available to the agent and may be invoked, including tools that make changes outside this client. Reverting restores the old tool list, but it cannot undo side effects of calls already made.

Blocking findings: astrbot/core/agent/mcp_client.py:679, astrbot/core/agent/mcp_client.py:683, astrbot/core/agent/mcp_client.py:672


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread astrbot/core/agent/mcp_client.py
Comment thread astrbot/core/agent/mcp_client.py Outdated
Comment thread astrbot/core/agent/mcp_client.py Outdated

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sourcery assessment

Approved.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Hi, this test is unnecessary, it can be removed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed as suggested. Thanks :)

This branch has not been deployed

No deployments
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.

2 participants