Skip to content

Python: Harden MCP catalog discovery behavior - #10042

Open
Jose Alvarez (jpalvarezl) wants to merge 14 commits into
mainfrom
jpalvarezl/fix/mcp_pagination_hardening
Open

Jose Alvarez (jpalvarezl) wants to merge 14 commits into
mainfrom
jpalvarezl/fix/mcp_pagination_hardening

Conversation

@jpalvarezl

Copy link
Copy Markdown
Member

Motivation & Context

MCP servers can expose paginated tool and prompt catalogs whose size and response time vary significantly. Catalog discovery should remain predictable, preserve the previous complete snapshot when refresh fails, and make framework-owned versus caller-owned request policy explicit.

Description & Review Guide

  • What are the major changes? Added shared catalog page fetching with connection recovery, configurable page and item limits for tool/prompt discovery, a finite default request timeout for framework-owned clients, and actionable limit errors. Tool annotations are now retained with the atomic catalog snapshot so security labeling no longer performs an independent catalog listing. Tests and MCP ownership guidance cover the new behavior.
  • What is the impact of these changes? Framework-owned clients use bounded defaults while caller-supplied clients retain their own transport and timeout policy. Catalog refresh continues to publish atomically, supports explicit trusted-server opt-outs, and reports which setting should be adjusted when a configured limit is reached.
  • What do you want reviewers to focus on? Please focus on the ownership boundary for framework-created versus caller-supplied clients, the page/item limit semantics at exact boundaries, and preservation of atomic refresh and security-label behavior.

Related Issue

N/A - maintainer-directed behavior hardening with no public tracking issue.

AI Assistance

  • No material AI assistance was used.
  • This is an AI-assisted contribution. I reviewed, understood, and verified all submitted content and accept responsibility for it.

AI assistance details: Copilot assisted with design review, implementation guidance, tests, documentation, and this PR description.

Contribution Checklist

  • The code builds clean without any errors or warnings
  • All unit tests pass, and I have added new tests where possible
  • The PR follows the Contribution Guidelines
  • This PR links to an agreed issue with no competing open PR, or the Related Issue section documents a trivial-change or repository-automation exception.
  • This is not a breaking change. If it is a breaking change, add the breaking change label (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and the title prefix in sync automatically.

Copilot AI 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.

🟢 Approval recommended

No active review issues are recorded for the MCP catalog discovery hardening.

No open issues 🎉

What changed in this PR

This PR hardens Python MCP catalog discovery with bounded requests and pagination while preserving complete catalog snapshots and caller-owned connection policy.

Changes:

  • Adds configurable page and item limits, shared page-fetch recovery, and a 30-second default request timeout for framework-owned clients.
  • Retains tool annotations with catalog snapshots so security labeling no longer lists tools independently.
  • Adds tests and ownership guidance for discovery limits, recovery, and annotation refresh behavior.
File Description
python/​packages/​core/​tests/​test_security.py Updates security tests to use retained annotation snapshots and explicit refreshes.
python/​packages/​core/​tests/​core/​test_mcp.py Tests limits, exact boundaries, opaque cursors, timeout defaults, and connection recovery.
python/​packages/​core/​AGENTS.md Documents request bounds, ownership policy, and atomic annotation refresh.
python/​packages/​core/​agent_framework/​security.py Uses cached annotations and refreshes tools before reapplying proxy labels.
python/​packages/​core/​agent_framework/​_mcp.py Implements bounded discovery, shared recovery, timeout defaults, and annotation retention.
docs/​decisions/​0045-python-mcp-v2-client-lifecycle.md Clarifies timeout ownership, discovery bounds, and catalog publication responsibilities.

Review effort: Balanced

…gination_hardening

# Conflicts:
#	python/packages/core/agent_framework/_mcp.py
@github-code-quality

github-code-quality Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Code coverage overview

Languages: Python

Python / code-coverage/python

main d615566 jpalvarezl/fix/mcp_p... 9f20a93 +/-
92% 92% 0%

ℹ️ Calculations represent line coverage. Results may change as calculations are updated.


Updated November 03, 2026 14:21 UTC

@github-actions github-actions 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.

MAF Automated Review — Iteration 1

Result: Findings reported
Scope: full PR (12 commit(s)): 5d32c1bd3d23, 37bd4437fd3e, 4b344e12a986, 9edeb705d1b2, 455f290dcd65, bd4e9f2ac202, 41eb445c870d, 4efc33245cb8, f358e659882a, 99ae067f1291, 863cb18fc4cd, 907d87eee6e3
Model: gpt-5.6-sol

Overview

The change adds per-fetch page and item limits, exact-boundary validation, atomic catalog publication, and retained annotation snapshots with conservative security-label defaults. The staged-state design and rollback tests provide strong protection against partial publication. Three compatibility gaps remain: the new timeout affects all MCP requests rather than discovery alone, label refresh can populate tools that were explicitly disabled, and wrapper entry points do not expose the new trusted-server opt-outs.

Reviewed the supplied pull-request change set across correctness, security/reliability, architecture, and failure behavior.
3 verified findings remained after source verification (3 medium) across 2 files. Details are attached to the affected lines below.

Affected areas: python/packages/core/agent_framework/_mcp.py, python/packages/core/agent_framework/security.py

read_timeout_seconds=(
self.request_timeout
if self.request_timeout is not None
else _MCP_DEFAULT_READ_TIMEOUT_SECONDS

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.

request_timeout=None previously passed no SDK timeout, but this fallback gives every RPC on framework-owned clients, including initialization and tools/call, a 30-second ceiling. Long-running tools that previously succeeded now fail, and None no longer provides an opt-out. Please scope the finite default to catalog discovery or provide a separate explicit way to retain unlimited tool-call timing.

raise RuntimeError("MCPTool is not connected. Connect before refreshing labels.")
callback_bound = self._bind_function_load_callback()
try:
await self._mcp_tool.load_tools()

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.

When the wrapped tool was constructed with load_tools=False, this call still populates _functions because public load_tools() does not honor that flag, while invocation continues to reject every loaded tool. refresh_labels() therefore changes the exposed catalog and creates unusable functions instead of only refreshing current labels. Please preserve the opt-out, either by gating this reload or by refreshing annotations without publishing tools.

use_progressive_disclosure: bool = False,
always_load: Collection[str] | None = None,
*,
max_discovery_pages: int | None = _DEFAULT_MCP_DISCOVERY_MAX_PAGES,

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.

These non-None defaults are inherited by adapters that do not expose either option. In particular, SecureMCPToolProxy(url=...) and DefaultMCPToolHandler construct MCPStreamableHTTPTool without forwarding these settings, so a trusted catalog over 100 pages or 1,000 items now fails to connect through those APIs with no configuration-level opt-out. Please plumb the limits through those entry points and their schemas, or preserve the previous behavior where callers cannot configure them.

Comment on lines +3171 to +3176
return await self._try_list_page_with_recovery(
params,
kind="prompt",
is_supported=lambda: self._supports_prompts,
request_page=lambda page_params: self._require_connection().list_prompts_page(page_params),
)

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.

do we need this wrapper?

self._tool_annotations_by_name = tool_annotations_by_name
self._progressive_loaded_tool_names.difference_update(existing_tools.keys() - reused_tool_names)

async def _try_list_tools_page_with_recovery(

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.

same with this one

async def request_page_with_retry() -> _MCPListResultT | None:
await self._ensure_connected()

if not is_supported():

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.

why are we checking this per page, can't we just skip this loading altogether?

params,
kind="prompt",
is_supported=lambda: self._supports_prompts,
request_page=lambda page_params: self._require_connection().list_prompts_page(page_params),

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.

wouldn't this be equivalent?

Suggested change
request_page=lambda page_params: self._require_connection().list_prompts_page(page_params),
request_page=self._require_connection().list_prompts_page,

Comment on lines +1240 to +1241
self._max_discovery_pages = max_discovery_pages
self._max_discovery_items = max_discovery_items

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.

Suggested change
self._max_discovery_pages = max_discovery_pages
self._max_discovery_items = max_discovery_items
self.max_discovery_pages = max_discovery_pages
self.max_discovery_items = max_discovery_items

task_options: Deprecated compatibility parameter. Non-``None`` values are ignored with a warning.
max_discovery_pages: Maximum number of pages accepted while discovering tools or prompts.
``None`` disables the limit.
max_discovery_items: Maximum number of tools or prompts accepted across all discovery pages.

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.

it is not clear whether this is a single limit for tools+prompts or a limit for each one?

This branch was previously deployed

1 inactive deployment
github-app-auth — 9f20a93e Deployed Nov 3, 2026 by jpalvarezl via add_label #26966
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Usage: [Issues, PRs], Target: documentation in the code base and learn docs python Usage: [Issues, PRs], Target: Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants