Skip to content

[Bug] ResourceManager.add_template() ignores warn_on_duplicate_resources and silently overwrites existing templates #3813

Open

Description

@Tanmay9223

Initial Checks

Release line

2.x (current stable)

Description

In mcp.server.mcpserver.resources.resource_manager.ResourceManager, the add_resource() method properly respects self.warn_on_duplicate_resources (configured via MCPServer(warn_on_duplicate_resources=True), which is True by default): when a duplicate URI is added, it logs a warning (Resource already exists: ...) and preserves the existing resource handler (return existing).

However, add_template() completely ignores warn_on_duplicate_resources:

  1. It does not check self.warn_on_duplicate_resources.
  2. It emits no log warning when an identical URI template is re-registered.
  3. It silently overwrites the existing template in self._templates[template.uri_template] = template.

This creates divergent registration semantics between static resources and template resources registered via @server.resource(...), and leads to silent replacement of template handlers when multiple modules or extensions declare the same URI template.

Example Code

import logging
from mcp.server.mcpserver import MCPServer

logging.basicConfig(level=logging.WARNING)

server = MCPServer(warn_on_duplicate_resources=True)

# 1. Static resources: logs warning and preserves existing (doc1)
@server.resource("file://doc")
def doc1() -> str:
    return "v1"

@server.resource("file://doc")
def doc2() -> str:
    return "v2"

# 2. Template resources: emits no warning and silently overwrites template1 with template2
@server.resource("file://doc/{id}")
def template1(id: str) -> str:
    return "template_v1"

@server.resource("file://doc/{id}")
def template2(id: str) -> str:
    return "template_v2"

# Static resource warned and kept doc1:
assert server._resource_manager._resources["file://doc"].fn() == "v1"

# Template resource silently overwritten without warning:
assert server._resource_manager._templates["file://doc/{id}"].name == "template2"

Expected output:
When template2 is registered for "file://doc/{id}", ResourceManager should emit:
WARNING:mcp.server.mcpserver.resources.resource_manager:Template already exists: file://doc/{id}
and retain or handle the duplicate consistently with add_resource / add_tool / add_prompt.

Python & MCP Python SDK

  • Python 3.11 / 3.12 / 3.14
  • MCP Python SDK: main branch (v2.x)

Activity

  1. added
    bugSomething isn't working
    v1Affects the v1.x maintenance line
    v2Affects the v2 line (2.x on main)
    on Nov 3, 2026
  2. Shanmukh-l33t commented on Nov 3, 2026

    @Shanmukh-l33t

    Confirmed independently and prototyped a fix (not opening a PR — I understand unassigned-contributor PRs get auto-closed here, and this is @Tanmay9223's report to own).

    Root cause (src/mcp/server/mcpserver/resources/resource_manager.py): add_template() builds the ResourceTemplate and then does self._templates[template.uri_template] = template unconditionally — there is no existing lookup at all, so warn_on_duplicate_resources is never consulted. add_resource() (same file) and add_tool()/add_prompt() (their managers) all do the lookup, warn, and return existing.

    Fix shape (mirror add_resource() exactly):

    existing = self._templates.get(template.uri_template)
    if existing:
        if self.warn_on_duplicate_resources:
            logger.warning(f"Template already exists: {template.uri_template}")
        return existing
    self._templates[template.uri_template] = template
    return template

    Tested branch, if useful to whoever lands this: fix/3813-duplicate-template-registration on Shanmukh-l33t/python-sdk (compare), commit 02456738 (+37/-0). Three regression tests mirroring the existing test_add_duplicate_resource / test_warn_on_duplicate_resources / test_disable_warn_on_duplicate_resources trio: duplicate registration keeps the first template (second is first), warns with the URI template when enabled, and stays silent but still non-overwriting when warn_on_duplicate_resources=False.

    Evidence: the 3 new tests fail on main @ af4c6f4 (3 failed) and pass with the change (test file 14 passed); full suite 6228 passed / 8 skipped / 1 xfailed with one failure, test_file_resources.py::test_permission_error, which fails identically on pristine main in this environment (test runs as root); ruff check + ruff format --check clean; pyright clean on both touched files.

    One design note: the duplicate check runs after ResourceTemplate.from_function(), so a re-registration with a malformed function still raises from from_function before the duplicate path — same failure surface as today; happy to move the lookup earlier if you'd prefer duplicates short-circuit before validation.

    (AI disclosure: this analysis, patch and tests were drafted with AI assistance and reviewed by me.)

  3. media-ctrl commented on Nov 4, 2026

    @media-ctrl

    Reproduced by reading src/mcp/server/mcpserver/resources/resource_manager.py on main: add_template writes self._templates[template.uri_template] = template at line 86 with no read of self.warn_on_duplicate_resources, while add_resource at lines 52-58 checks the flag, logs Resource already exists: ... and returns the existing object.

    One thing to settle before copying add_resource's behaviour: the return value is discarded. MCPServer.resource() calls self._resource_manager.add_template(...) at server.py:905 as a bare statement and the decorator hands the author their own function back, so "warn and return the existing template" produces a warning plus a registration that silently did nothing, with the author still holding a handler that will never fire. That is better than a silent overwrite, but the two reasonable end states are log-and-keep-first (with the author's second handler dead) or raise on duplicate. For a decorator surface I would raise, because the author is present at import time and a dead handler is exactly the failure mode this issue is about.

    Worth noting while you are in there: templates are keyed by the uri_template string, so file://doc/{id} and file://doc/{name} are two keys and both survive, and get_resource will only ever reach one of them for a given URI. String equality dedupe (the cheap fix, matching add_resource) catches the literal duplicate but not the equivalent pattern, so the warn is a partial answer to the divergence the report describes.

    Minimal patch in the shape of add_resource:

    existing = self._templates.get(template.uri_template)
    if existing is not None:
        if self.warn_on_duplicate_resources:
            logger.warning(f"Template already exists: {template.uri_template}")
        return existing
    self._templates[template.uri_template] = template
  4. Shanmukh-l33t commented on Nov 4, 2026

    @Shanmukh-l33t

    @media-ctrl — good catch on the discarded return. I verified it against main, and the dead-handler shape you're worried about already exists verbatim on the sibling path:

    • server.py:905: self._resource_manager.add_template(...) is a bare statement and the decorator then return fn — confirmed, as you say.
    • The literal-URI path behaves identically today: MCPServer.add_resource (server.py:786, annotated -> None) discards the manager's return, and @mcp.resource() calls it as a bare statement at server.py:944 before return fn. Registering the same literal URI twice already produces exactly "warning + first registration wins + the author's second function never fires".

    So add_template mirroring add_resource (my branch fix/3813-duplicate-template-registration, commit 02456738) makes the two surfaces consistent, which is what #3813 reports: the flag is never consulted for templates.

    On raising instead: I agree it's the better end state for a decorator surface — but it's a behavior change spanning both surfaces (otherwise one raises and one warns), which is a design decision for the maintainers, not something to fold into a bug fix for "the flag is ignored". Happy to file that as its own issue if nobody has.

    On equivalent patterns: agreed, and no disagreement — file://doc/{id} vs file://doc/{name} are distinct keys and string-equality dedupe can't catch them. Pattern-level equivalence needs real pattern matching plus a tie-break rule for overlapping templates, which is a separate change; I'd keep #3813 to the literal duplicate the report describes.

    (AI disclosure: this analysis was drafted with AI assistance and reviewed by me.)

  5. media-ctrl commented on Nov 4, 2026

    @media-ctrl

    Agreed on scoping #3813 to the literal duplicate. The flag never being consulted is the defect, and add_template mirroring add_resource is the minimal fix that makes the two surfaces tell the same story.

    On the raise path, two things that would help whoever opens it:

    • Cover add_resource and add_template in one issue. If only the template decorator raises, we trade one inconsistency for another.
    • Name the exception. Authors writing resources today get a warning and a working handler, so a bare ValueError from a decorator will look like a bug in their own code. A dedicated error type is easier to document and to catch.

    I can supply the trace from main I posted above plus the shape of a focused test (register twice, assert the exception, assert the first handler still resolves) so the change lands with coverage rather than needing it later. I would rather be a co-reporter than the opener on that one, since the design call belongs to the maintainers.

  6. Shanmukh-l33t commented on Nov 4, 2026

    @Shanmukh-l33t

    The minimal patch you sketched is the shape I had already built and tested before this thread picked up: branch fix/3813-duplicate-template-registration at 0245673802afd6cac3c8e938cd26eec89f862399 (compare: main...Shanmukh-l33t:python-sdk:fix/3813-duplicate-template-registration). It adds the existing is not None check mirroring add_resource (resource_manager.py:52-58): warn only when warn_on_duplicate_resources is set, keep the first registration. 3 tests were red at main and green with the fix.

    Your point about the discarded return is exactly why I stopped at warn-and-keep-first instead of raising: MCPServer.resource() calls add_template(...) as a bare statement (server.py:905) and hands the author their own function back, so both today's silent overwrite and a warn-only fix leave a dead handler registered — the decorator has no channel to the author at import time except raising.

    So on the raise path I'd take your two scoping calls as-is: one issue covering add_resource and add_template together (otherwise the two surfaces just diverge the other way), and a named exception instead of bare ValueError — authors today get a warning and a working handler, so an undifferentiated ValueError out of their own decorator call reads like a bug in their code. The register-twice → assert-exception → assert-first-handler-still-resolves test shape ports straight onto what the warn-path tests already do.

    The uri_template equivalence gap you flagged (file://doc/{id} and file://doc/{name} both surviving) is a deeper question — pattern-equivalence dedupe isn't string equality — and I'd keep it out of the literal-duplicate fix entirely rather than block it.

    Either way the design call is the maintainers'; the branch is there as the minimal fix or a starting point.

    (Written with AI assistance; line references re-verified against main today.)

  7. Tanmay9223 commented on Nov 4, 2026

    @Tanmay9223
    Author

    Thank you @Shanmukh-l33t and @media-ctrl for the thorough reproduction, analysis, and discussion!

    I completely agree with the consensus on scoping:

    1. Keep [Bug] ResourceManager.add_template() ignores warn_on_duplicate_resources and silently overwrites existing templates #3813 strictly focused on the literal duplicate bug:
      Aligning ResourceManager.add_template() to mirror add_resource() (consulting warn_on_duplicate_resources, logging the warning when enabled, and returning the existing template without overwriting) resolves the exact inconsistency reported here with zero collateral risk.

    2. Track broader architectural discussions separately:

      • Raising on duplicate registrations: Agree that moving from warning to raising (e.g. introducing a dedicated DuplicateResourceError) is a cross-cutting design call that should span both add_resource and add_template equally so the surfaces don't diverge.
      • Pattern-level template equivalence: Differentiating or deduplicating URI template patterns (like {id} vs {name}) requires formal pattern parsing and precedence rules, which definitely belongs in its own feature/SEP discussion.

    As the reporter, I'm happy to open the minimal PR for this (and ensure @Shanmukh-l33t is co-authored for the test cases and prototype) if maintainers would like an outside contribution, or happy if the maintainer team prefers to pick it up directly.

    (AI disclosure: drafted with AI assistance and reviewed by me.)

  8. Shanmukh-l33t commented on Nov 4, 2026

    @Shanmukh-l33t

    Agreed on all three scoping points — your summary matches exactly where the thread landed: literal-duplicate fix now, raise-on-duplicate as one cross-cutting call spanning both adders (named exception included), pattern equivalence in its own SEP.

    The reporter opening the minimal PR is the right call — first call on your own issue — so I'll stay out of the way. The minimal fix is on my fork as fix/3813-duplicate-template-registration (commit 0245673802afd6cac3c8e938cd26eec89f862399): a 5-line add_resource mirror in add_template plus 3 warn-flag tests that were red at main and green with it. Cherry-pick it, re-derive it, whatever is easier — and co-author credit is generous of you but not owed; the PR should land under your name either way.

    For the raise-path follow-up, the test shape from the branch ports directly: register twice → assert the named exception → assert the first handler still resolves. Happy to review either follow-up when it lands.

    (Written with AI assistance.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingv1Affects the v1.x maintenance linev2Affects the v2 line (2.x on main)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions