Repository navigation
One placeholder-less resource template makes the whole server unserviceable #476
Description
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
ResourceTemplate::__construct()requires at least one placeholder and throws (src/Schema/ResourceTemplate.php#L59-L61).ReflectedElementLoaderwraps that into aConfigurationException(ReflectedElementLoader.php#L214-L219), which abortsRegistry::load()before any element is registered.Builder::$lazyLoadingdefaults totrue, so that load runs on the first read during request handling.Registry::load()setsloadedonly 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:
ReflectedElementLoaderthrowsConfigurationException, 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
[-]Resource template configuration errors are swallowed by the streamed HTTP response[/-][+]One placeholder-less resource template makes the whole server unserviceable[/+]wachterjohannes commentedon Aug 23, 2026
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/listandtools/callfail too.[Server] Keep one unregistrable element from taking the whole registr…
[Server] Keep one unregistrable element from taking the whole registr…
[Server] Keep one unregistrable element from taking the whole registr…
chr-hertel commentedon Aug 24, 2026
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
wachterjohannes commentedon Aug 25, 2026
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, soProtocolcatches it in the\InvalidArgumentExceptionarm and answers-32602with 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. catchingConfigurationExceptionbefore that arm and mapping it toforInternalErroris a few lines and doesn't touch the loading design at all.for the symfony side i'm relaxed anyway,
debug:mcpreads the registry so it triggers the load, and the developer gets the message on the cli before any client connects.