Skip to content

One placeholder-less resource template makes the whole server unserviceable #476

Description

@wachterjohannes

Reported by Mariano Damian Ferro Villanueva via email, opening it here on their behalf.

Problem

A #[McpResourceTemplate] whose uriTemplate contains no placeholder takes the whole server down, not just that one template.

#[McpResourceTemplate(
    uriTemplate: 'data://tags',
    name: 'all_tags',
    title: 'All Tags',
    description: 'All Tags',
    mimeType: 'application/json'
)]
public function tag_all(?int $paged = 1): array
{
    // ...
}

The handshake still succeeds, and then every request fails, including ones that have nothing to do with resources:

tools/list -> {"jsonrpc":"2.0","id":2,"error":{"code":-32602,"message":"Error registering manual resource template 'data://tags': Invalid URI template : \"data://tags\" must be a valid URI template with at least one placeholder."}}
tools/call -> {"jsonrpc":"2.0","id":3,"error":{"code":-32602,"message":"Error registering manual resource template 'data://tags': Invalid URI template : \"data://tags\" must be a valid URI template with at least one placeholder."}}

Reproduced on main against Server::builder() with the Streamable HTTP transport, on both the handshake and the 2026-07-28 lifecycle.

Cause

  1. ResourceTemplate::__construct() requires at least one placeholder and throws (src/Schema/ResourceTemplate.php#L59-L61).
  2. ReflectedElementLoader wraps that into a ConfigurationException (ReflectedElementLoader.php#L214-L219), which aborts Registry::load() before any element is registered.
  3. Builder::$lazyLoading defaults to true, so that load runs on the first read during request handling. Registry::load() sets loaded only on success, so every subsequent request retries and fails the same way.

So one misconfigured element is enough to make a server serve nothing, and the client is told -32602 (Invalid params) for what is a server-side configuration error it cannot do anything about.

The original report saw it as a 500 with Cannot modify header information - headers already sent (output started at /vendor/symfony/http-foundation/Response.php:393), which is how it surfaces once the response is already being written. I could not reproduce that part on main, so treat it as a symptom of the surrounding stack rather than part of this issue.

Secondary issue: inconsistent handling

The same mistake behaves differently depending on the registration path:

  • ReflectedElementLoader throws ConfigurationException, a hard failure taking down the registry.
  • Discoverer::processFile() catches \Throwable, logs it and continues, so an attribute-discovered template with the same mistake is silently dropped.

Suggested fix

Validate the URI template in Builder::addResourceTemplate(), where the developer wrote it, instead of at the first read of the registry. PR follows.

Worth considering separately: a ConfigurationException reaching a client as -32602 is misleading (it is caught by catch (\InvalidArgumentException) in Protocol), and a single failing element aborting the whole registry load is a large blast radius for any other configuration mistake.

Line references are against main.

Activity

  1. changed the title [-]Resource template configuration errors are swallowed by the streamed HTTP response[/-] [+]One placeholder-less resource template makes the whole server unserviceable[/+] on Aug 23, 2026
  2. wachterjohannes commented on Aug 23, 2026

    @wachterjohannes
    ContributorAuthor

    Updated the description after reproducing it: the SDK does turn this into a JSON-RPC error rather than letting it escape, so the original "swallowed by the streamed response" framing was wrong. The real problem is the blast radius, one bad template makes tools/list and tools/call fail too.

  3. added 3 commits that reference this issue on Aug 24, 2026
    737ec3b
    76fb3ec
    12856e0
  4. chr-hertel commented on Aug 24, 2026

    @chr-hertel
    Member

    i'm a bit torn here if this really needs a code fix - mostly because it is not that easy.
    in the end it feels fair: user land code is wrong, execution fails - error message give a hint even.

    i understand that it is inconvenient to be late, but elevating the logic into the builder (solution #1) or silently dropping all of those issues (solution #2) is too big of a trade off to me ... unfortunately i don't have a good counter idea at this point, sorry

  5. added
    ServerIssues & PRs related to the Server component
    on Aug 24, 2026
  6. wachterjohannes commented on Aug 25, 2026

    @wachterjohannes
    ContributorAuthor

    yeah, fine to leave the blast radius open for now, i don't have a better idea either and i agree that both options cost more than they fix.

    one piece i'd split off though, independent of that decision: ConfigurationException extends InvalidArgumentException, so Protocol catches it in the \InvalidArgumentException arm and answers -32602 with the full message. every other unexpected throwable is masked as "Internal server error." right below, so this is the one case where the client both gets blamed for something it cannot fix and gets handed the handler and uri template. catching ConfigurationException before that arm and mapping it to forInternalError is a few lines and doesn't touch the loading design at all.

    for the symfony side i'm relaxed anyway, debug:mcp reads the registry so it triggers the load, and the developer gets the message on the cli before any client connects.

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

    ServerIssues & PRs related to the Server component

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions