Zum Inhalt

URI-Templates und Pfadsicherheit

Maschinelle Übersetzung

Diese Seite wurde automatisch aus der englischen Dokumentation übersetzt, und die englische Seite ist die maßgebliche Fassung. Wenn sich etwas falsch liest, erklärt Übersetzungen, wie du es melden kannst.

Dies ist die Referenz für die URI-Template-Syntax, die @mcp.resource akzeptiert, und für die Pfadsicherheitsrichtlinie, die das SDK auf extrahierte Werte anwendet. Eine Einführung, was Ressourcen sind und wann du sie einsetzt, findest du in Ressourcen; diese Seite setzt voraus, dass du bereits sicher im Deklarieren einer Ressource bist und den vollständigen Operatorsatz, die Sicherheitseinstellungen oder die Low-Level-Verdrahtung suchst.

Die Template-Syntax ist RFC 6570. Das SDK unterstützt eine Teilmenge, die für das Matching eingehender resources/read-URIs ausgewählt wurde, plus eine Sicherheitsschicht, die Werte ablehnt, die außerhalb des Verzeichnisses landen würden, das du bereitstellen willst. Die Details auf Protokollebene (Nachrichtenformate, Lebenszyklus, Paginierung) stehen in der MCP-Ressourcen-Spezifikation.

Der vollständige Operatorsatz

Der einfache Platzhalter {user_id} ist der, den Ressourcen einführt. Es gibt vier weitere Operatorformen; hier stehen sie alle auf einem Server, damit du sie nebeneinander siehst:

server.py
from mcp.server import MCPServer

mcp = MCPServer("Bookshop")

BOOKS = {
    "978-0441172719": {"title": "Dune", "author": "Frank Herbert"},
    "978-0553293357": {"title": "Foundation", "author": "Isaac Asimov"},
}

MANUALS = {
    "printing/setup.md": "# Printer setup\n\nLoad paper, then power on.",
    "returns.md": "# Returns policy\n\nThirty days with a receipt.",
}


@mcp.resource("books://{isbn}")
def get_book(isbn: str) -> dict[str, str]:
    """A single book by ISBN."""
    return BOOKS[isbn]


@mcp.resource("orders://{order_id}")
def get_order(order_id: int) -> dict[str, object]:
    """An order by its numeric id."""
    return {"order_id": order_id, "next_order": order_id + 1, "status": "shipped"}


@mcp.resource("manuals://{+path}")
def read_manual(path: str) -> str:
    """A staff manual page. The path keeps its slashes."""
    return MANUALS[path]


@mcp.resource("reviews://{isbn}{?limit,sort}")
def list_reviews(isbn: str, limit: int = 10, sort: str = "newest") -> str:
    """Reviews of a book, optionally limited and sorted."""
    return f"{limit} {sort} reviews of {BOOKS[isbn]['title']}"


@mcp.resource("shelves://browse{/path*}")
def browse_shelf(path: list[str]) -> str:
    """A shelf in the category tree, addressed by segments."""
    return " > ".join(["catalog", *path])

Jeder hervorgehobene Dekorator zerlegt den URI auf eine andere Weise. Die folgenden Abschnitte gehen sie von oben nach unten durch.

Einfache Expansion: {name}

books://{isbn} ist die schlichte Alltagsform. Der Platzhalter wird auf den Parameter isbn abgebildet, sodass ein Client, der books://978-0441172719 liest, get_book("978-0441172719") aufruft.

Ein einfaches {name} endet am ersten /. books://978/extra passt nicht, weil der Schrägstrich nach 978 die Erfassung beendet und /extra übrig bleibt.

Typkonvertierung

Extrahierte Werte kommen als Strings an, aber du kannst einen genaueren Typ deklarieren, und das SDK konvertiert. orders://{order_id} landet in einer Funktion, deren Parameter order_id: int ist, sodass das Lesen von orders://12345 get_order(12345) aufruft, nicht get_order("12345"). Der Handler rechnet damit (order_id + 1), ohne zu casten.

Mehrteilige Pfade: {+name}

Um einen Wert zu erfassen, der Schrägstriche enthält, verwende {+name}. Mit manuals://{+path}:

  • manuals://returns.md ergibt path = "returns.md"
  • manuals://printing/setup.md ergibt path = "printing/setup.md"

Greif zu {+name}, wann immer der Wert hierarchisch ist: Dateisystempfade, verschachtelte Objektschlüssel, URL-Pfade, die du als Proxy weiterreichst.

Query-Parameter: {?a,b,c}

reviews://{isbn}{?limit,sort} setzt limit und sort hinter das ?. Der Pfad bestimmt, welches Buch; die Query steuert, wie du es liest.

Query-Parameter werden nachsichtig abgeglichen: Die Reihenfolge spielt keine Rolle, zusätzliche werden ignoriert, und weggelassene fallen auf die Standardwerte deiner Funktion zurück. reviews://978-0441172719 verwendet also limit=10, sort="newest", und reviews://978-0441172719?sort=top überschreibt nur sort.

Pfadsegmente als Liste: {/name*}

Wenn du jedes Pfadsegment als eigenes Listenelement haben willst statt als einen String mit Schrägstrichen, verwende {/name*}. Mit shelves://browse{/path*} ruft ein Client, der shelves://browse/fiction/sci-fi liest, browse_shelf(["fiction", "sci-fi"]) auf.

Template-Referenz

Die häufigsten Muster:

Muster Beispieleingabe Du bekommst
{name} alice "alice"
{name} docs/intro.md kein Treffer (endet am /)
{+path} docs/intro.md "docs/intro.md"
{.ext} .json "json"
{/segment} /v2 "v2"
{?key} ?key=value "value"
{?a,b} ?a=1&b=2 "1", "2"
{/path*} /a/b/c ["a", "b", "c"]

Was der Parser ablehnt

Einige Template-Formen werden vorab abgefangen, statt beim ersten Request zu scheitern. @mcp.resource parst das Template, wenn der Dekorator läuft, sodass keine davon je einen laufenden Server erreicht.

UriTemplate.parse() löst InvalidUriTemplate aus bei:

  • Zwei Variablen ohne etwas dazwischen. manuals://{+path}{ext} wird abgelehnt: Das Matching kann nicht erkennen, wo path endet und ext beginnt. Setze ein Literal dazwischen (manuals://{+path}/{ext}) oder verwende einen Operator, der seinen eigenen Trenner mitbringt. manuals://{+path}{.ext} wird akzeptiert, weil {.ext} den . selbst beisteuert.
  • Mehr als eine mehrteilige Variable. Höchstens eines von {+var}, {#var} oder einer explodierten Variable ({/var*}, {.var*}, {;var*}) pro Template. Zwei sind grundsätzlich mehrdeutig: Es gibt keinen begründbaren Weg zu entscheiden, welche ein zusätzliches Segment aufnimmt.
  • Den üblichen Syntaxfehlern: eine nicht geschlossene geschweifte Klammer, ein doppelt verwendeter Variablenname oder ein RFC-6570-Feature, das das SDK nicht unterstützt, etwa der Präfix-Modifikator {var:3} oder die Query-Explosion {?vars*}.

Darüber hinaus löst @mcp.resource einen ValueError aus, wenn ein Handler-Parameter an eine Query-Variable im abschließenden {?...}/{&...}-Lauf des Templates gebunden ist, aber keinen Python-Standardwert hat. Diese Variablen werden nachsichtig abgeglichen (ein Client darf jede davon weglassen), sodass ein Parameter ohne Standardwert erst beim ersten Request, der ihn weglässt, als undurchsichtiger interner Fehler auftauchen würde. reviews://{isbn}{?limit,sort} im Server oben ist die wohlgeformte Variante: limit und sort tragen beide Standardwerte.

Sicherheit

Template-Parameter kommen vom Client. Fließen sie ungeprüft in Dateisystem- oder Datenbankoperationen, können Werte wie ../../etc/passwd außerhalb des Verzeichnisses landen, das du bereitstellen wolltest.

Was das SDK standardmäßig prüft

Bevor dein Handler läuft, lehnt das SDK jeden Parameter ab, der:

  • sein Ausgangsverzeichnis über ..-Komponenten verlassen würde
  • wie ein absoluter Pfad aussieht (/etc/passwd, C:\Windows) oder wie ein laufwerksrelativer Windows-Pfad (C:foo). Ein laufwerksrelativer Wert und ein Bezeichner mit Namensraum wie x:y sind als Strings nicht zu unterscheiden, daher wird standardmäßig jeder Wert aus einem einzelnen Buchstaben plus Doppelpunkt abgelehnt; nimm den Parameter aus, wenn er solche Werte legitim erhält
  • ein Nullbyte (\x00) enthält

Die ..-Prüfung arbeitet komponentenbasiert, nicht als Teilstringsuche. Werte wie v1.0..v2.0 oder HEAD~3..HEAD kommen durch, weil .. dort kein eigenständiges Pfadsegment ist.

Diese Prüfungen gelten für den dekodierten Wert, sie fangen Traversal also unabhängig davon ab, wie es im URI kodiert war (../etc, ..%2Fetc, %2E%2E/etc, ..%5Cetc, %00 werden alle abgefangen).

Check

Lies manuals://../etc/passwd vom Server oben, und der Request wird rundweg abgelehnt: Das Template-Matching stoppt beim ersten Fehlschlag, sodass kein späteres (womöglich großzügigeres) Template als Fallback probiert wird. Der Client sieht denselben -32602-Fehler „Unknown resource“ wie bei einem URI, der auf gar kein Template passt, und read_manual läuft nie.

Dateisystem-Handler: safe_join verwenden

Die eingebauten Prüfungen stoppen die häufigen Fälle, können aber deine Sandbox-Grenze nicht kennen. Für Dateisystemzugriffe verwende safe_join, um den Pfad aufzulösen und zu verifizieren, dass er innerhalb deines Basisverzeichnisses bleibt:

server.py
from pathlib import Path

from mcp.server import MCPServer
from mcp.shared.path_security import safe_join

mcp = MCPServer("Bookshop")

DOCS_ROOT = Path("./manuals")


@mcp.resource("manuals://{+path}")
def read_manual(path: str) -> str:
    """A staff manual page, served from a directory on disk."""
    return safe_join(DOCS_ROOT, path).read_text()

safe_join fängt Symlink-Ausbrüche, ..-Sequenzen und Tricks mit absoluten Pfaden ab, die eine einfache Stringprüfung übersehen würde. Verlässt der aufgelöste Pfad DOCS_ROOT, löst es PathEscapeError aus, der beim Client als ResourceError ankommt.

Wenn die Standardwerte im Weg stehen

Manchmal blockieren die Prüfungen legitime Werte. Ein Tool für den Katalogimport könnte absichtlich einen absoluten Pfad erhalten, oder ein Parameter könnte eine relative Referenz wie ../sibling sein, die dein Handler sicher interpretiert, ohne das Dateisystem anzufassen. Nimm diesen Parameter aus oder lockere die Richtlinie für den ganzen Server:

server.py
from mcp.server import MCPServer
from mcp.server.mcpserver import ResourceSecurity

mcp = MCPServer("Bookshop")


@mcp.resource(
    "imports://preview/{+source}",
    security=ResourceSecurity(exempt_params={"source"}),
)
def preview_import(source: str) -> str:
    """Preview a catalog import. `source` may be an absolute path."""
    return f"Would import from {source}"


relaxed = MCPServer(
    "Bookshop",
    resource_security=ResourceSecurity(reject_path_traversal=False),
)


@relaxed.resource("imports://preview/{+source}")
def preview_import_relaxed(source: str) -> str:
    """The server-wide flag exempts every resource on `relaxed`."""
    return f"Would import from {source}"
  • security=ResourceSecurity(exempt_params={"source"}) am Dekorator überspringt die Prüfungen für diesen einen Parameter auf dieser einen Ressource. Der Rest des Servers behält die Standardrichtlinie.
  • resource_security= am MCPServer-Konstruktor setzt den Standard für jede Ressource. Hier schaltet relaxed die ..-Prüfung ganz ab.

Die konfigurierbaren Prüfungen:

Einstellung Standardwert Was sie tut
reject_path_traversal True Lehnt ..-Sequenzen ab, die das Ausgangsverzeichnis verlassen
reject_absolute_paths True Lehnt /foo, C:\foo, UNC-Pfade und laufwerksrelatives C:foo ab (fängt auch x:y ab)
reject_null_bytes True Lehnt Werte ab, die \x00 enthalten
exempt_params leer Parameternamen, für die Prüfungen übersprungen werden

Diese Prüfungen sind ein heuristischer Vorfilter; für Dateisystemzugriffe bleibt safe_join die Eindämmungsgrenze.

Tip

Kann dein Handler den Request nicht erfüllen (die Datei existiert nicht, die ID ist unbekannt), löse eine Exception aus. Das SDK macht daraus eine Fehler-Response. Den Unterschied zwischen einem Protokollfehler und einem Tool-Fehler erklärt Fehler behandeln.

Ressourcen auf dem Low-Level-Server

Wenn du auf dem Low-Level-Server aufbaust (siehe Der Low-Level-Server), registrierst du Handler für die Protokollmethoden resources/list und resources/read direkt. Es gibt keinen Dekorator; du gibst die Protokolltypen selbst zurück.

Statische Ressourcen

Für feste URIs führe eine Registry und verteile anhand exakter Übereinstimmung:

server.py
from mcp.server import Server, ServerRequestContext
from mcp.types import (
    ListResourcesResult,
    PaginatedRequestParams,
    ReadResourceRequestParams,
    ReadResourceResult,
    Resource,
    TextResourceContents,
)

RESOURCES = {
    "config://shop": '{"currency": "USD", "tax_rate": 0.08}',
    "status://health": "ok",
}


async def list_resources(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListResourcesResult:
    return ListResourcesResult(resources=[Resource(name=uri, uri=uri) for uri in RESOURCES])


async def read_resource(ctx: ServerRequestContext, params: ReadResourceRequestParams) -> ReadResourceResult:
    if (text := RESOURCES.get(params.uri)) is not None:
        return ReadResourceResult(contents=[TextResourceContents(uri=params.uri, text=text)])
    raise ValueError(f"Unknown resource: {params.uri}")


server = Server("Bookshop", on_list_resources=list_resources, on_read_resource=read_resource)

Der List-Handler teilt Clients mit, was verfügbar ist; der Read-Handler liefert den Inhalt. Prüfe zuerst deine Registry, falle auf Templates (unten) zurück, falls du welche hast, und löse für alles andere eine Exception aus.

Templates

Die Template-Engine, die MCPServer verwendet, liegt in mcp.shared.uri_template und funktioniert eigenständig. Du bekommst dasselbe Parsing und Matching; Routing und Sicherheitsrichtlinie verdrahtest du selbst.

server.py
from mcp.server import Server, ServerRequestContext
from mcp.shared.path_security import contains_path_traversal, is_absolute_path
from mcp.shared.uri_template import UriTemplate
from mcp.types import (
    ListResourceTemplatesResult,
    PaginatedRequestParams,
    ReadResourceRequestParams,
    ReadResourceResult,
    ResourceTemplate,
    TextResourceContents,
)

TEMPLATES = {
    "manuals": UriTemplate.parse("manuals://{+path}"),
    "books": UriTemplate.parse("books://{isbn}"),
}

MANUALS = {"printing/setup.md": "# Printer setup", "returns.md": "# Returns policy"}
BOOKS = {"978-0441172719": "Dune by Frank Herbert"}


def read_manual_safely(path: str) -> str:
    if contains_path_traversal(path) or is_absolute_path(path):
        raise ValueError("rejected")
    return MANUALS[path]


async def read_resource(ctx: ServerRequestContext, params: ReadResourceRequestParams) -> ReadResourceResult:
    if (matched := TEMPLATES["manuals"].match(params.uri)) is not None:
        text = read_manual_safely(str(matched["path"]))
        return ReadResourceResult(contents=[TextResourceContents(uri=params.uri, text=text)])

    if (matched := TEMPLATES["books"].match(params.uri)) is not None:
        text = BOOKS[str(matched["isbn"])]
        return ReadResourceResult(contents=[TextResourceContents(uri=params.uri, text=text)])

    raise ValueError(f"Unknown resource: {params.uri}")


async def list_resource_templates(
    ctx: ServerRequestContext, params: PaginatedRequestParams | None
) -> ListResourceTemplatesResult:
    return ListResourceTemplatesResult(
        resource_templates=[
            ResourceTemplate(name=name, uri_template=str(template)) for name, template in TEMPLATES.items()
        ]
    )


server = Server(
    "Bookshop",
    on_read_resource=read_resource,
    on_list_resource_templates=list_resource_templates,
)

In den hervorgehobenen Zeilen passieren drei Dinge:

  • Einmal parsen, pro Request matchen. UriTemplate.parse() baut das Template; template.match(uri) gibt die extrahierten Variablen als dict zurück, oder None, wenn der URI nicht passt. Die URL-Dekodierung geschieht innerhalb von match(); die dekodierten Werte werden unverändert zurückgegeben, ohne Pfadsicherheitsprüfung. Die Werte kommen als Strings heraus: Konvertiere sie selbst (int(matched["id"]), Path(matched["path"])).
  • Die Sicherheitsprüfungen selbst anwenden. Die ..- und Absolutpfad-Prüfungen, die MCPServer standardmäßig ausführt, liegen in mcp.shared.path_security. read_manual_safely ruft sie auf, bevor es MANUALS anfasst. Ist ein Parameter kein Dateisystempfad (eine ISBN, eine Suchanfrage), überspring die Prüfungen für diesen Wert: Du steuerst die Richtlinie pro Handler statt über ein Konfigurationsobjekt.
  • Die Templates aus derselben Quelle auflisten. Clients entdecken Templates über resources/templates/list. str(template) gibt den ursprünglichen Template-String zurück, sodass Auflistung und Matcher eine einzige Quelle der Wahrheit teilen.

Zusammenfassung

  • {name} passt auf ein Segment; {+name} behält die Schrägstriche; {?a,b} zieht aus dem Query-String; {/name*} teilt Segmente in eine Liste auf.
  • Zwei Variablen ohne etwas dazwischen oder eine zweite mehrteilige Variable werden beim Parsen abgelehnt. Ein Parameter, der an eine abschließende {?...}/{&...}-Query-Variable gebunden ist, muss einen Python-Standardwert deklarieren.
  • Annotiere den Parameter (order_id: int), und das SDK konvertiert.
  • Die Standard-Sicherheitsrichtlinie lehnt .., absolute Pfade und Nullbytes ab, bevor dein Handler läuft; überschreibe sie pro Ressource mit security=ResourceSecurity(...) oder serverweit mit resource_security=.
  • Für Dateisystemzugriffe ist safe_join die Eindämmungsgrenze.
  • Auf dem Low-Level-Server parst du mit UriTemplate.parse(), matchst mit .match() und wendest mcp.shared.path_security selbst an.