Skip to content

fix: drop configurable dedupe from AggregateProvider, always warn#3877

Merged
jlowin merged 3 commits intomainfrom
fix/remove-aggregate-dedupe-logic
Apr 12, 2026
Merged

fix: drop configurable dedupe from AggregateProvider, always warn#3877
jlowin merged 3 commits intomainfrom
fix/remove-aggregate-dedupe-logic

Conversation

@jlowin
Copy link
Copy Markdown
Member

@jlowin jlowin commented Apr 12, 2026

Follow-up to #3827.

#3827 wired FastMCP's on_duplicate kwarg into AggregateProvider so that cross-provider collisions could raise ValueError on list_* calls. That overloaded the kwarg: on_duplicate was designed for LocalProvider's registration-time behavior, where the author writes both decorators and can meaningfully react to an error at code time. AggregateProvider composition is different — mounted providers can be dynamic (file-system watchers, remote mirrors, etc.), so a strict-mode runtime error would crash production with no actionable recovery path for the author.

This change makes the cross-provider signal warn-only and non-configurable. on_duplicate still controls LocalProvider registration as before; AggregateProvider just emits a warning when two mounted providers return the same MCP component. Collision detection now uses FastMCPComponent.key, which is the canonical MCP identity — it encodes type (tool: / resource: / template: / prompt:), identifier (name for tools/prompts, uri for resources, uri_template for templates), and version. That replaces the ad-hoc name or uri or uri_template fallback from #3827 and gives version-awareness for free: [email protected] from provider A and [email protected] from provider B are variants, not duplicates — matching what _get_highest_version_result already does on the get_* side.

# Before — FastMCP(on_duplicate="error") crashed list_tools() on collision.
# After  — collision always logs a warning, never raises.

main = FastMCP("main")
main.mount(sub1, "ns")
main.mount(sub2, "ns")  # both expose "greet"
await main.list_tools()  # logs: Duplicate list_tools component 'tool:ns_greet@' from ...

#3827 let FastMCP's on_duplicate kwarg control cross-provider duplicate
behavior in AggregateProvider, but that setting was designed for
LocalProvider registration: the author wrote both decorators and can react
to an error at code time. AggregateProvider composition is different —
mounted providers can be dynamic, so list_tools() raising at runtime based
on a static kwarg would crash production with no actionable recovery.

Collisions across mounted providers now always log a warning and never
raise. The identity key used for detection is MCP-native per component
type (uri for resources, uri_template for templates, name for tools and
prompts), so two providers exposing the same resource URI under different
display names get flagged while two providers exposing different URIs
under the same display name no longer false-positive.

LocalProvider's registration-time on_duplicate behavior is unchanged.
@marvin-context-protocol marvin-context-protocol Bot added bug Something isn't working. Reports of errors, unexpected behavior, or broken functionality. server Related to FastMCP server implementation or server-side functionality. labels Apr 12, 2026
Replace the ad-hoc name-or-uri-or-uri_template fallback with the
canonical FastMCPComponent.key property, which encodes type,
identifier, and version. Version variants across providers are no
longer flagged as duplicates — consistent with
_get_highest_version_result on the get_* side.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 05c08c4dbd

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +130 to +133
key = getattr(item, "key", None)
if key is not None:
first = seen_keys.setdefault(key, i)
if first != i:
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Normalize versions before duplicate-key comparison

Duplicate detection now compares raw item.key strings, but keys embed the version text without normalization, so semantically equivalent versions like 1 and 1.0 are treated as different and won't warn. In the same class, _get_highest_version_result uses version_sort_key (PEP 440 parsing), which treats those versions as equal and then silently picks the first provider. That creates a false negative in the collision warning path for mounted providers and hides ambiguous cross-provider resolution in production.

Useful? React with 👍 / 👎.

@marvin-context-protocol
Copy link
Copy Markdown
Contributor

Test Failure Analysis

Summary: One test failed on Windows only — a tight performance benchmark missed its 100ms threshold by ~0.2ms (got 0.1002s). This is unrelated to the PR changes.

Root Cause: uses a hard-coded 100ms threshold with zero tolerance. The test averaged 0.1002s on the Windows CI runner — barely over the limit. This PR only touches src/fastmcp/server/providers/aggregate.py and tests/server/mount/test_mount.py; the failing test is not in the diff and was pre-existing. The same test passed on Python 3.10 and 3.13 on Ubuntu in this same run.

Suggested Solution: The test at tests/server/providers/openapi/test_performance_comparison.py:217 sets max_acceptable_time = 0.1. Windows runners consistently run slower than Linux, and a 100ms hard ceiling with no margin will flap. The fix is to either:

  1. Loosen the threshold on Windows — use pytest.mark.skipif(sys.platform == 'win32', ...) or set a platform-specific limit (e.g. 200ms on Windows).
  2. Add a tolerance margin — e.g. max_acceptable_time = 0.15 to account for CI variance.

The test is a pre-existing issue and not caused by this PR.

Detailed Analysis

Failing job: Tests: Python 3.10 on windows-latest (job ID 70993692507)

Failure log excerpt:

AssertionError: Server should initialize in under 100ms, got 0.1002s
assert 0.10021982192993165 < 0.1
tests\server\providers\openapi\test_performance_comparison.py:221: AssertionError

Test logic (lines 198–223):

def test_server_initialization_performance(self, comprehensive_spec):
    num_iterations = 5
    times = []
    for _ in range(num_iterations):
        client = httpx.AsyncClient(base_url="https://api.example.com")
        start_time = time.time()
        server = create_openapi_server(...)
        end_time = time.time()
        times.append(end_time - start_time)

    avg_time = sum(times) / len(times)
    max_acceptable_time = 0.1  # 100ms  ← no platform tolerance

    assert avg_time < max_acceptable_time, ...

The margin of failure is ~0.2ms — well within normal CI timing jitter on Windows. All other jobs (Linux Python 3.10, Linux Python 3.13, integration tests, MCP conformance) passed.

PR diff summary: Only aggregate.py and test_mount.py were changed — no OpenAPI provider or performance-related code touched.

Related Files
  • tests/server/providers/openapi/test_performance_comparison.py — contains the flaky performance test (lines 198–223); needs a platform-aware threshold or wider margin
  • src/fastmcp/server/providers/aggregate.py — modified by this PR (unrelated to the failure)
  • tests/server/mount/test_mount.py — modified by this PR (unrelated to the failure)

CLAUDE.md now tells contributors (and LLM agents) to read the base
component class before writing cross-component logic, and to prefer
.key over name/uri fallbacks. Expanded the .key docstring with
when-to-use guidance so the same hint surfaces in LSP hover.
@jlowin jlowin merged commit 57f1b1b into main Apr 12, 2026
8 checks passed
@jlowin jlowin deleted the fix/remove-aggregate-dedupe-logic branch April 12, 2026 21:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working. Reports of errors, unexpected behavior, or broken functionality. server Related to FastMCP server implementation or server-side functionality.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant