Skip to content

uri_template

RFC 6570 URI Templates with bidirectional support.

Provides both expansion (template + variables → URI) and matching (URI → variables). RFC 6570 only specifies expansion; matching is the inverse operation needed by MCP servers to route resources/read requests to handlers.

Supports Levels 1-3 fully, plus Level 4 explode modifier for path-like operators ({/var*}, {.var*}, {;var*}). The Level 4 prefix modifier ({var:N}) and query-explode ({?var*}) are not supported.

Matching semantics

Matching is not specified by RFC 6570 (§1.4 explicitly defers to regex languages). This implementation uses a two-ended scan that never backtracks: match time is O(n·v) where n is URI length and v is the number of template variables. Realistic templates have v < 10, making this effectively linear; there is no input that produces superpolynomial time.

A template may contain at most one multi-segment variable{+var}, {#var}, or an explode-modified variable ({/var*}, {.var*}, {;var*}). This variable greedily consumes whatever the surrounding bounded variables and literals do not. Two such variables in one template are inherently ambiguous (which one gets the extra segment?) and are rejected at parse time. So are any two variables adjacent with no literal between them — including a variable adjacent to the multi-segment variable: the scan has nothing to anchor the boundary on. Operators that emit their own lead character supply that literal themselves, so {+path}{.ext} and {a}{.b} are fine while {+path}{ext} and {a}{b} are not.

Bounded variables before the multi-segment variable match lazily (first occurrence of the following literal); those after match greedily (last occurrence of the preceding literal). Templates without a multi-segment variable match greedily throughout, identical to regex semantics.

Reserved expansion {+var} leaves ? and # unencoded, but the scan stops at those characters so {+path}{?q} can separate path from query. A value containing a literal ? or # expands fine but will not round-trip through match().

InvalidUriTemplate

Bases: ValueError

Raised when a URI template string is malformed or unsupported.

Attributes:

Name Type Description
template

The template string that failed to parse.

position

Character offset where the error was detected, or None if the error is not tied to a specific position.

Source code in src/mcp/shared/uri_template.py
124
125
126
127
128
129
130
131
132
133
134
135
136
class InvalidUriTemplate(ValueError):
    """Raised when a URI template string is malformed or unsupported.

    Attributes:
        template: The template string that failed to parse.
        position: Character offset where the error was detected, or None
            if the error is not tied to a specific position.
    """

    def __init__(self, message: str, *, template: str, position: int | None = None) -> None:
        super().__init__(message)
        self.template = template
        self.position = position

Variable dataclass

A single variable within a URI template expression.

Source code in src/mcp/shared/uri_template.py
139
140
141
142
143
144
145
@dataclass(frozen=True)
class Variable:
    """A single variable within a URI template expression."""

    name: str
    operator: Operator
    explode: bool = False

UriTemplate dataclass

A parsed RFC 6570 URI template.

Construct via :meth:parse. Instances are immutable and hashable; equality is based on the template string alone.

Source code in src/mcp/shared/uri_template.py
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
@dataclass(frozen=True)
class UriTemplate:
    """A parsed RFC 6570 URI template.

    Construct via :meth:`parse`. Instances are immutable and hashable;
    equality is based on the template string alone.
    """

    template: str
    _parts: list[_Part] = field(repr=False, compare=False)
    _variables: list[Variable] = field(repr=False, compare=False)
    _prefix: list[_Atom] = field(repr=False, compare=False)
    _greedy: Variable | None = field(repr=False, compare=False)
    _suffix: list[_Atom] = field(repr=False, compare=False)
    _query_variables: list[Variable] = field(repr=False, compare=False)

    @staticmethod
    def is_template(value: str) -> bool:
        """Check whether a string contains URI template expressions.

        A cheap heuristic for distinguishing concrete URIs from templates
        without the cost of full parsing. Returns ``True`` if the string
        contains at least one ``{...}`` pair.

        Example::

            >>> UriTemplate.is_template("file://docs/{name}")
            True
            >>> UriTemplate.is_template("file://docs/readme.txt")
            False

        Note:
            This does not validate the template. A ``True`` result does
            not guarantee :meth:`parse` will succeed.
        """
        open_i = value.find("{")
        return open_i != -1 and value.find("}", open_i) != -1

    @classmethod
    def parse(
        cls,
        template: str,
        *,
        max_length: int = DEFAULT_MAX_TEMPLATE_LENGTH,
        max_variables: int = DEFAULT_MAX_VARIABLES,
    ) -> UriTemplate:
        """Parse a URI template string.

        Args:
            template: An RFC 6570 URI template.
            max_length: Maximum permitted length of the template string.
                Guards against resource exhaustion.
            max_variables: Maximum number of variables permitted across
                all expressions. Counting variables rather than
                ``{...}`` expressions closes the gap where a single
                ``{v0,v1,...,vN}`` expression packs arbitrarily many
                variables under one expression count.

        Raises:
            InvalidUriTemplate: If the template is malformed, exceeds the
                size limits, or uses unsupported RFC 6570 features.
        """
        if len(template) > max_length:
            raise InvalidUriTemplate(
                f"Template exceeds maximum length of {max_length}",
                template=template,
            )

        parts, variables = _parse(template, max_variables=max_variables)

        # Trailing {?...}/{&...} expressions are split off and matched as
        # a query string (order-agnostic, partial, extras ignored) rather
        # than via the linear scan.
        path_parts, query_vars = _split_query_tail(parts)
        atoms = _flatten(path_parts)
        prefix, greedy, suffix = _partition_greedy(atoms, template)

        return cls(
            template=template,
            _parts=parts,
            _variables=variables,
            _prefix=prefix,
            _greedy=greedy,
            _suffix=suffix,
            _query_variables=query_vars,
        )

    @property
    def variables(self) -> list[Variable]:
        """All variables in the template, in order of appearance."""
        return list(self._variables)

    @property
    def variable_names(self) -> list[str]:
        """All variable names in the template, in order of appearance."""
        return [v.name for v in self._variables]

    @property
    def query_variable_names(self) -> frozenset[str]:
        """Names of variables that :meth:`match` treats as optional query parameters.

        These are the variables in a trailing run of ``{?...}``/``{&...}``
        expressions, which are matched leniently: a URI that omits some
        (or all) of them still matches, and the omitted names are simply
        absent from the result. Any value bound to such a name therefore
        needs a fallback for the omitted case.

        Every other variable is bound on every successful :meth:`match`
        (possibly to an empty string) and is *not* in this set. That
        includes a ``{&...}`` expression with no preceding ``{?...}``: it
        never emits the ``?`` the lenient query split keys on, so it is
        matched strictly.
        """
        return frozenset(v.name for v in self._query_variables)

    def expand(self, variables: Mapping[str, str | Sequence[str]]) -> str:
        """Expand the template by substituting variable values.

        String values are percent-encoded according to their operator:
        simple ``{var}`` encodes reserved characters; ``{+var}`` and
        ``{#var}`` leave them intact. Sequence values are joined with
        commas for non-explode variables, or with the operator's
        separator for explode variables.

        Example::

            >>> t = UriTemplate.parse("file://docs/{name}")
            >>> t.expand({"name": "hello world.txt"})
            'file://docs/hello%20world.txt'

            >>> t = UriTemplate.parse("file://docs/{+path}")
            >>> t.expand({"path": "src/main.py"})
            'file://docs/src/main.py'

            >>> t = UriTemplate.parse("/search{?q,lang}")
            >>> t.expand({"q": "mcp", "lang": "en"})
            '/search?q=mcp&lang=en'

            >>> t = UriTemplate.parse("/files{/path*}")
            >>> t.expand({"path": ["a", "b", "c"]})
            '/files/a/b/c'

        Args:
            variables: Values for each template variable. Keys must be
                strings; values must be ``str`` or a sequence of ``str``.

        Returns:
            The expanded URI string.

        Note:
            Per RFC 6570, variables absent from the mapping are
            **silently omitted**. This is the correct behavior for
            optional query parameters (``{?page}`` with no page yields
            no ``?page=``), but for required path segments it produces
            a structurally incomplete URI. If you need all variables
            present, validate before calling::

                missing = set(t.variable_names) - variables.keys()
                if missing:
                    raise ValueError(f"Missing: {missing}")

        Raises:
            TypeError: If a value is neither ``str`` nor an iterable of
                ``str``. Non-string scalars (``int``, ``None``) are not
                coerced.
        """
        out: list[str] = []
        for part in self._parts:
            if isinstance(part, str):
                out.append(part)
            else:
                out.append(_expand_expression(part, variables))
        return "".join(out)

    def match(self, uri: str, *, max_uri_length: int = DEFAULT_MAX_URI_LENGTH) -> dict[str, str | list[str]] | None:
        """Match a concrete URI against this template and extract variables.

        This is the inverse of :meth:`expand`. The URI is matched via a
        linear scan of the template and captured values are
        percent-decoded. The round-trip ``match(expand({k: v})) == {k: v}``
        holds when ``v`` does not contain its operator's separator
        unencoded: ``{.ext}`` with ``ext="tar.gz"`` expands to
        ``.tar.gz`` but does not match — the scan stops ``ext`` at the
        first ``.`` and the trailing ``.gz`` has nothing to consume it.
        RFC 6570 §1.4 notes this is an inherent reversal limitation.

        Matching is structural at the URI level only: a simple ``{name}``
        will not match across a literal ``/`` in the URI (the scan stops
        there), but a percent-encoded ``%2F`` that decodes to ``/`` is
        accepted as part of the value. Path-safety validation belongs at
        a higher layer; see :mod:`mcp.shared.path_security`.

        Example::

            >>> t = UriTemplate.parse("file://docs/{name}")
            >>> t.match("file://docs/readme.txt")
            {'name': 'readme.txt'}
            >>> t.match("file://docs/hello%20world.txt")
            {'name': 'hello world.txt'}

            >>> t = UriTemplate.parse("file://docs/{+path}")
            >>> t.match("file://docs/src/main.py")
            {'path': 'src/main.py'}

            >>> t = UriTemplate.parse("/files{/path*}")
            >>> t.match("/files/a/b/c")
            {'path': ['a', 'b', 'c']}

        **Query parameters** (``{?q,lang}`` at the end of a template)
        are matched leniently: order-agnostic, partial, and unrecognized
        params are ignored. Absent params are omitted from the result so
        downstream function defaults can apply::

            >>> t = UriTemplate.parse("logs://{service}{?since,level}")
            >>> t.match("logs://api")
            {'service': 'api'}
            >>> t.match("logs://api?level=error")
            {'service': 'api', 'level': 'error'}
            >>> t.match("logs://api?level=error&since=5m&utm=x")
            {'service': 'api', 'since': '5m', 'level': 'error'}

        Args:
            uri: A concrete URI string.
            max_uri_length: Maximum permitted length of the input URI.
                Oversized inputs return ``None`` without scanning,
                guarding against resource exhaustion.

        Returns:
            A mapping from variable names to decoded values (``str`` for
            scalar variables, ``list[str]`` for explode variables), or
            ``None`` if the URI does not match the template or exceeds
            ``max_uri_length``.
        """
        if len(uri) > max_uri_length:
            return None

        if self._query_variables:
            # Two-phase: scan matches the path, the query is split and
            # decoded manually. Query params may be partial, reordered,
            # or include extras; absent params stay absent so downstream
            # defaults can apply. Fragment is stripped first since the
            # template's {?...} tail never describes a fragment.
            before_fragment, _, _ = uri.partition("#")
            path, _, query = before_fragment.partition("?")
            result = self._scan(path)
            if result is None:
                return None
            if query:
                parsed = _parse_query(query)
                for var in self._query_variables:
                    if var.name in parsed:
                        result[var.name] = parsed[var.name]
            return result

        return self._scan(uri)

    def _scan(self, uri: str) -> dict[str, str | list[str]] | None:
        """Run the two-ended linear scan against the path portion of a URI."""
        n = len(uri)

        if self._greedy is None:
            # No greedy var: the suffix IS the whole template, scanned
            # right-to-left and anchored so atoms[0] matches at position 0.
            suffix = _scan_suffix(self._suffix, uri, n, anchored=True)
            if suffix is None:
                return None
            suffix_result, suffix_start = suffix
            return suffix_result if suffix_start == 0 else None

        # Greedy var present. The parser rejects a capture adjacent to
        # the greedy slot, so a non-empty suffix begins with a _Lit whose
        # rfind-derived anchor does not depend on how far the prefix
        # scans. Scan the suffix first, then give the prefix that exact
        # position as its ceiling so it cannot consume past the anchor.
        suffix = _scan_suffix(self._suffix, uri, n, anchored=False)
        if suffix is None:
            return None
        suffix_result, suffix_start = suffix
        prefix = _scan_prefix(self._prefix, uri, 0, suffix_start)
        if prefix is None:
            return None
        prefix_result, prefix_end = prefix

        # Prefix consumed [0, prefix_end); suffix consumed [suffix_start, n);
        # the greedy var takes the gap. The prefix scan is bounded by
        # suffix_start, so this holds by construction; guard explicitly
        # rather than asserting so a future regression surfaces as a
        # non-match, not an exception.
        if suffix_start < prefix_end:
            return None  # pragma: no cover - unreachable while bounds hold
        middle = uri[prefix_end:suffix_start]
        greedy_value = _extract_greedy(self._greedy, middle)
        if greedy_value is None:
            return None

        return {**prefix_result, self._greedy.name: greedy_value, **suffix_result}

    def __str__(self) -> str:
        return self.template

is_template staticmethod

is_template(value: str) -> bool

Check whether a string contains URI template expressions.

A cheap heuristic for distinguishing concrete URIs from templates without the cost of full parsing. Returns True if the string contains at least one {...} pair.

Example::

>>> UriTemplate.is_template("file://docs/{name}")
True
>>> UriTemplate.is_template("file://docs/readme.txt")
False
Note

This does not validate the template. A True result does not guarantee :meth:parse will succeed.

Source code in src/mcp/shared/uri_template.py
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
@staticmethod
def is_template(value: str) -> bool:
    """Check whether a string contains URI template expressions.

    A cheap heuristic for distinguishing concrete URIs from templates
    without the cost of full parsing. Returns ``True`` if the string
    contains at least one ``{...}`` pair.

    Example::

        >>> UriTemplate.is_template("file://docs/{name}")
        True
        >>> UriTemplate.is_template("file://docs/readme.txt")
        False

    Note:
        This does not validate the template. A ``True`` result does
        not guarantee :meth:`parse` will succeed.
    """
    open_i = value.find("{")
    return open_i != -1 and value.find("}", open_i) != -1

parse classmethod

parse(
    template: str,
    *,
    max_length: int = DEFAULT_MAX_TEMPLATE_LENGTH,
    max_variables: int = DEFAULT_MAX_VARIABLES
) -> UriTemplate

Parse a URI template string.

Parameters:

Name Type Description Default
template str

An RFC 6570 URI template.

required
max_length int

Maximum permitted length of the template string. Guards against resource exhaustion.

DEFAULT_MAX_TEMPLATE_LENGTH
max_variables int

Maximum number of variables permitted across all expressions. Counting variables rather than {...} expressions closes the gap where a single {v0,v1,...,vN} expression packs arbitrarily many variables under one expression count.

DEFAULT_MAX_VARIABLES

Raises:

Type Description
InvalidUriTemplate

If the template is malformed, exceeds the size limits, or uses unsupported RFC 6570 features.

Source code in src/mcp/shared/uri_template.py
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
@classmethod
def parse(
    cls,
    template: str,
    *,
    max_length: int = DEFAULT_MAX_TEMPLATE_LENGTH,
    max_variables: int = DEFAULT_MAX_VARIABLES,
) -> UriTemplate:
    """Parse a URI template string.

    Args:
        template: An RFC 6570 URI template.
        max_length: Maximum permitted length of the template string.
            Guards against resource exhaustion.
        max_variables: Maximum number of variables permitted across
            all expressions. Counting variables rather than
            ``{...}`` expressions closes the gap where a single
            ``{v0,v1,...,vN}`` expression packs arbitrarily many
            variables under one expression count.

    Raises:
        InvalidUriTemplate: If the template is malformed, exceeds the
            size limits, or uses unsupported RFC 6570 features.
    """
    if len(template) > max_length:
        raise InvalidUriTemplate(
            f"Template exceeds maximum length of {max_length}",
            template=template,
        )

    parts, variables = _parse(template, max_variables=max_variables)

    # Trailing {?...}/{&...} expressions are split off and matched as
    # a query string (order-agnostic, partial, extras ignored) rather
    # than via the linear scan.
    path_parts, query_vars = _split_query_tail(parts)
    atoms = _flatten(path_parts)
    prefix, greedy, suffix = _partition_greedy(atoms, template)

    return cls(
        template=template,
        _parts=parts,
        _variables=variables,
        _prefix=prefix,
        _greedy=greedy,
        _suffix=suffix,
        _query_variables=query_vars,
    )

variables property

variables: list[Variable]

All variables in the template, in order of appearance.

variable_names property

variable_names: list[str]

All variable names in the template, in order of appearance.

query_variable_names property

query_variable_names: frozenset[str]

Names of variables that :meth:match treats as optional query parameters.

These are the variables in a trailing run of {?...}/{&...} expressions, which are matched leniently: a URI that omits some (or all) of them still matches, and the omitted names are simply absent from the result. Any value bound to such a name therefore needs a fallback for the omitted case.

Every other variable is bound on every successful :meth:match (possibly to an empty string) and is not in this set. That includes a {&...} expression with no preceding {?...}: it never emits the ? the lenient query split keys on, so it is matched strictly.

expand

expand(variables: Mapping[str, str | Sequence[str]]) -> str

Expand the template by substituting variable values.

String values are percent-encoded according to their operator: simple {var} encodes reserved characters; {+var} and {#var} leave them intact. Sequence values are joined with commas for non-explode variables, or with the operator's separator for explode variables.

Example::

>>> t = UriTemplate.parse("file://docs/{name}")
>>> t.expand({"name": "hello world.txt"})
'file://docs/hello%20world.txt'

>>> t = UriTemplate.parse("file://docs/{+path}")
>>> t.expand({"path": "src/main.py"})
'file://docs/src/main.py'

>>> t = UriTemplate.parse("/search{?q,lang}")
>>> t.expand({"q": "mcp", "lang": "en"})
'/search?q=mcp&lang=en'

>>> t = UriTemplate.parse("/files{/path*}")
>>> t.expand({"path": ["a", "b", "c"]})
'/files/a/b/c'

Parameters:

Name Type Description Default
variables Mapping[str, str | Sequence[str]]

Values for each template variable. Keys must be strings; values must be str or a sequence of str.

required

Returns:

Type Description
str

The expanded URI string.

Note

Per RFC 6570, variables absent from the mapping are silently omitted. This is the correct behavior for optional query parameters ({?page} with no page yields no ?page=), but for required path segments it produces a structurally incomplete URI. If you need all variables present, validate before calling::

missing = set(t.variable_names) - variables.keys()
if missing:
    raise ValueError(f"Missing: {missing}")

Raises:

Type Description
TypeError

If a value is neither str nor an iterable of str. Non-string scalars (int, None) are not coerced.

Source code in src/mcp/shared/uri_template.py
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
def expand(self, variables: Mapping[str, str | Sequence[str]]) -> str:
    """Expand the template by substituting variable values.

    String values are percent-encoded according to their operator:
    simple ``{var}`` encodes reserved characters; ``{+var}`` and
    ``{#var}`` leave them intact. Sequence values are joined with
    commas for non-explode variables, or with the operator's
    separator for explode variables.

    Example::

        >>> t = UriTemplate.parse("file://docs/{name}")
        >>> t.expand({"name": "hello world.txt"})
        'file://docs/hello%20world.txt'

        >>> t = UriTemplate.parse("file://docs/{+path}")
        >>> t.expand({"path": "src/main.py"})
        'file://docs/src/main.py'

        >>> t = UriTemplate.parse("/search{?q,lang}")
        >>> t.expand({"q": "mcp", "lang": "en"})
        '/search?q=mcp&lang=en'

        >>> t = UriTemplate.parse("/files{/path*}")
        >>> t.expand({"path": ["a", "b", "c"]})
        '/files/a/b/c'

    Args:
        variables: Values for each template variable. Keys must be
            strings; values must be ``str`` or a sequence of ``str``.

    Returns:
        The expanded URI string.

    Note:
        Per RFC 6570, variables absent from the mapping are
        **silently omitted**. This is the correct behavior for
        optional query parameters (``{?page}`` with no page yields
        no ``?page=``), but for required path segments it produces
        a structurally incomplete URI. If you need all variables
        present, validate before calling::

            missing = set(t.variable_names) - variables.keys()
            if missing:
                raise ValueError(f"Missing: {missing}")

    Raises:
        TypeError: If a value is neither ``str`` nor an iterable of
            ``str``. Non-string scalars (``int``, ``None``) are not
            coerced.
    """
    out: list[str] = []
    for part in self._parts:
        if isinstance(part, str):
            out.append(part)
        else:
            out.append(_expand_expression(part, variables))
    return "".join(out)

match

match(
    uri: str,
    *,
    max_uri_length: int = DEFAULT_MAX_URI_LENGTH
) -> dict[str, str | list[str]] | None

Match a concrete URI against this template and extract variables.

This is the inverse of :meth:expand. The URI is matched via a linear scan of the template and captured values are percent-decoded. The round-trip match(expand({k: v})) == {k: v} holds when v does not contain its operator's separator unencoded: {.ext} with ext="tar.gz" expands to .tar.gz but does not match — the scan stops ext at the first . and the trailing .gz has nothing to consume it. RFC 6570 §1.4 notes this is an inherent reversal limitation.

Matching is structural at the URI level only: a simple {name} will not match across a literal / in the URI (the scan stops there), but a percent-encoded %2F that decodes to / is accepted as part of the value. Path-safety validation belongs at a higher layer; see :mod:mcp.shared.path_security.

Example::

>>> t = UriTemplate.parse("file://docs/{name}")
>>> t.match("file://docs/readme.txt")
{'name': 'readme.txt'}
>>> t.match("file://docs/hello%20world.txt")
{'name': 'hello world.txt'}

>>> t = UriTemplate.parse("file://docs/{+path}")
>>> t.match("file://docs/src/main.py")
{'path': 'src/main.py'}

>>> t = UriTemplate.parse("/files{/path*}")
>>> t.match("/files/a/b/c")
{'path': ['a', 'b', 'c']}

Query parameters ({?q,lang} at the end of a template) are matched leniently: order-agnostic, partial, and unrecognized params are ignored. Absent params are omitted from the result so downstream function defaults can apply::

>>> t = UriTemplate.parse("logs://{service}{?since,level}")
>>> t.match("logs://api")
{'service': 'api'}
>>> t.match("logs://api?level=error")
{'service': 'api', 'level': 'error'}
>>> t.match("logs://api?level=error&since=5m&utm=x")
{'service': 'api', 'since': '5m', 'level': 'error'}

Parameters:

Name Type Description Default
uri str

A concrete URI string.

required
max_uri_length int

Maximum permitted length of the input URI. Oversized inputs return None without scanning, guarding against resource exhaustion.

DEFAULT_MAX_URI_LENGTH

Returns:

Type Description
dict[str, str | list[str]] | None

A mapping from variable names to decoded values (str for

dict[str, str | list[str]] | None

scalar variables, list[str] for explode variables), or

dict[str, str | list[str]] | None

None if the URI does not match the template or exceeds

dict[str, str | list[str]] | None

max_uri_length.

Source code in src/mcp/shared/uri_template.py
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
def match(self, uri: str, *, max_uri_length: int = DEFAULT_MAX_URI_LENGTH) -> dict[str, str | list[str]] | None:
    """Match a concrete URI against this template and extract variables.

    This is the inverse of :meth:`expand`. The URI is matched via a
    linear scan of the template and captured values are
    percent-decoded. The round-trip ``match(expand({k: v})) == {k: v}``
    holds when ``v`` does not contain its operator's separator
    unencoded: ``{.ext}`` with ``ext="tar.gz"`` expands to
    ``.tar.gz`` but does not match — the scan stops ``ext`` at the
    first ``.`` and the trailing ``.gz`` has nothing to consume it.
    RFC 6570 §1.4 notes this is an inherent reversal limitation.

    Matching is structural at the URI level only: a simple ``{name}``
    will not match across a literal ``/`` in the URI (the scan stops
    there), but a percent-encoded ``%2F`` that decodes to ``/`` is
    accepted as part of the value. Path-safety validation belongs at
    a higher layer; see :mod:`mcp.shared.path_security`.

    Example::

        >>> t = UriTemplate.parse("file://docs/{name}")
        >>> t.match("file://docs/readme.txt")
        {'name': 'readme.txt'}
        >>> t.match("file://docs/hello%20world.txt")
        {'name': 'hello world.txt'}

        >>> t = UriTemplate.parse("file://docs/{+path}")
        >>> t.match("file://docs/src/main.py")
        {'path': 'src/main.py'}

        >>> t = UriTemplate.parse("/files{/path*}")
        >>> t.match("/files/a/b/c")
        {'path': ['a', 'b', 'c']}

    **Query parameters** (``{?q,lang}`` at the end of a template)
    are matched leniently: order-agnostic, partial, and unrecognized
    params are ignored. Absent params are omitted from the result so
    downstream function defaults can apply::

        >>> t = UriTemplate.parse("logs://{service}{?since,level}")
        >>> t.match("logs://api")
        {'service': 'api'}
        >>> t.match("logs://api?level=error")
        {'service': 'api', 'level': 'error'}
        >>> t.match("logs://api?level=error&since=5m&utm=x")
        {'service': 'api', 'since': '5m', 'level': 'error'}

    Args:
        uri: A concrete URI string.
        max_uri_length: Maximum permitted length of the input URI.
            Oversized inputs return ``None`` without scanning,
            guarding against resource exhaustion.

    Returns:
        A mapping from variable names to decoded values (``str`` for
        scalar variables, ``list[str]`` for explode variables), or
        ``None`` if the URI does not match the template or exceeds
        ``max_uri_length``.
    """
    if len(uri) > max_uri_length:
        return None

    if self._query_variables:
        # Two-phase: scan matches the path, the query is split and
        # decoded manually. Query params may be partial, reordered,
        # or include extras; absent params stay absent so downstream
        # defaults can apply. Fragment is stripped first since the
        # template's {?...} tail never describes a fragment.
        before_fragment, _, _ = uri.partition("#")
        path, _, query = before_fragment.partition("?")
        result = self._scan(path)
        if result is None:
            return None
        if query:
            parsed = _parse_query(query)
            for var in self._query_variables:
                if var.name in parsed:
                    result[var.name] = parsed[var.name]
        return result

    return self._scan(uri)