Replace MCP resource URI templates¶
The Model Context Protocol addresses
resources by URI — custom schemes like weather://, db://, notion://,
screen://. The official Python SDK and FastMCP both ship URI-template
matchers, and both of them are hand-rolled URLPattern lookalikes.
The awkward way¶
Official MCP Python SDK,
src/mcp/server/mcpserver/resources/templates.py:
def matches(self, uri: str) -> dict[str, Any] | None:
"""Check if URI matches template and extract parameters."""
pattern = self.uri_template.replace("{", "(?P<").replace("}", ">[^/]+)")
match = re.match(f"^{pattern}$", uri)
if match:
return {key: unquote(value) for key, value in match.groupdict().items()}
return None
Two str.replace calls plus re.match. No scheme isolation, no
wildcards, no segment regex constraints, no way to express an optional
trailing segment. A template like weather://{city}/current matches
db://anything/current too, because the scheme isn't part of the
"pattern" — it's part of the URI string the regex happens to see.
FastMCP rebuilt the same thing with more features (build_regex() in
fastmcp_slim/fastmcp/resources/template.py),
still as a regex-string-concat with try/except re.error to detect bad
templates.
With URLPattern¶
from yarlpattern import URLPattern
WEATHER = URLPattern({"protocol": "weather", "hostname": ":city", "pathname": "/current"})
WEATHER.test("weather://nyc/current") # True
WEATHER.test("weather://sf/current") # True
WEATHER.test("weather://nyc/forecast") # False — wrong path
WEATHER.test("db://nyc/current") # False — wrong scheme
WEATHER.exec("weather://nyc/current").hostname["groups"]["city"] # 'nyc'
Need an integer-only resource id? Constrain the named group:
DB_USER = URLPattern({"protocol": "db", "hostname": ":table", "pathname": "/:id(\\d+)"})
DB_USER.test("db://users/42") # True
DB_USER.test("db://users/abc") # False — id must be digits
A whole resource registry becomes a small table:
RESOURCES = [
(WEATHER, get_weather),
(URLPattern({"protocol": "db", "hostname": ":table", "pathname": "/:id(\\d+)"}),
fetch_record),
(URLPattern({"protocol": "file", "pathname": "/:path+"}),
read_file),
(URLPattern({"protocol": "notion", "hostname": ":workspace", "pathname": "/:page_id"}),
load_notion_page),
]
def dispatch(uri: str):
for pattern, handler in RESOURCES:
result = pattern.exec(uri)
if result is not None:
return handler(result)
What you get for free¶
- Scheme isolation by construction.
protocol: "weather"means exactly that scheme; the MCP SDK'sstr.replacetrick gives you no such isolation — any URI shape matches as long as the path lines up. - Per-group regex constraints.
:id(\\d+)rejects non-numeric ids at the pattern level. FastMCP's regex-string-concat can express this but only if you remember to escape every other character correctly. - Spec-conformant URI parsing. Custom schemes, percent-encoding, case folding all behave consistently with the rest of the URL parsing in your application (yarlpattern delegates to yarl, which is WHATWG-conformant).
- The pattern is the documentation. A reader sees
URLPattern({"protocol": "weather", "hostname": ":city", "pathname": "/current"})and knows the resource shape immediately. The MCP SDK's{"city/current"}template lives inside a class with amatches()method that has to be read to understand what it accepts.