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:
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.mdergibtpath = "returns.md"manuals://printing/setup.mdergibtpath = "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, wopathendet undextbeginnt. 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 wiex:ysind 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:
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:
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=amMCPServer-Konstruktor setzt den Standard für jede Ressource. Hier schaltetrelaxeddie..-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:
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.
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 alsdictzurück, oderNone, wenn der URI nicht passt. Die URL-Dekodierung geschieht innerhalb vonmatch(); 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, dieMCPServerstandardmäßig ausführt, liegen inmcp.shared.path_security.read_manual_safelyruft sie auf, bevor esMANUALSanfasst. 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 mitsecurity=ResourceSecurity(...)oder serverweit mitresource_security=. - Für Dateisystemzugriffe ist
safe_joindie Eindämmungsgrenze. - Auf dem Low-Level-
Serverparst du mitUriTemplate.parse(), matchst mit.match()und wendestmcp.shared.path_securityselbst an.