Ana içeriğe geç

URI şablonları ve yol güvenliği

Makine çevirisi

Bu sayfa İngilizce dokümantasyondan otomatik olarak çevrildi; esas alınması gereken sürüm İngilizce sayfadır. Yanlış görünen bir şey varsa, nasıl bildireceğinizi Çeviriler sayfası açıklar.

Bu sayfa, @mcp.resource dekoratörünün kabul ettiği URI şablonu sözdiziminin ve SDK'nın çıkarılan değerlere uyguladığı yol güvenliği politikasının başvuru kaynağıdır. Kaynakların ne olduğuna ve ne zaman kullanılacağına dair bir giriş için Kaynaklar sayfasıyla başlayın; bu sayfa, kaynak bildirmeye zaten alışkın olduğunuzu ve operatör setinin tamamını, güvenlik ayarlarını ya da düşük seviyeli bağlantıları aradığınızı varsayar.

Şablon sözdizimi RFC 6570 standardıdır. SDK, gelen resources/read URI'lerini eşleştirmek için seçilmiş bir alt kümeyi destekler; buna ek olarak, sunmayı amaçladığınız dizinin dışına çözümlenecek değerleri reddeden bir güvenlik katmanı vardır. Protokol düzeyindeki ayrıntılar (mesaj biçimleri, yaşam döngüsü, sayfalama) için MCP kaynaklar belirtimine bakın.

Operatör setinin tamamı

Düz yer tutucu {user_id}, Kaynaklar sayfasının tanıttığı biçimdir. Dört operatör biçimi daha var; yan yana görebilmeniz için hepsi tek bir sunucuda:

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])

Vurgulanan her dekoratör, URI'yi parçalamanın farklı bir yoludur. Aşağıdaki bölümler bunları yukarıdan aşağıya ele alır.

Basit genişletme: {name}

books://{isbn} düz, gündelik biçimdir. Yer tutucu isbn parametresine eşlenir; yani books://978-0441172719 okuyan bir istemci get_book("978-0441172719") çağrısına yol açar.

Düz bir {name} ilk / karakterinde durur. books://978/extra eşleşmez çünkü 978'den sonraki eğik çizgi yakalamayı bitirir ve /extra artar.

Tür dönüşümü

Çıkarılan değerler dize olarak gelir, ancak daha belirli bir tür bildirebilirsiniz; SDK dönüştürür. orders://{order_id}, parametresi order_id: int olan bir fonksiyona düşer; dolayısıyla orders://12345 okumak get_order("12345") değil get_order(12345) çağrısını yapar. İşleyici, tür dönüştürme yapmadan üzerinde aritmetik işlem yapar (order_id + 1).

Çok segmentli yollar: {+name}

Eğik çizgi içeren bir değeri yakalamak için {+name} kullanın. manuals://{+path} ile:

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

Değer hiyerarşik olduğunda {+name} biçimine başvurun: dosya sistemi yolları, iç içe nesne anahtarları, vekillik ettiğiniz URL yolları.

Sorgu parametreleri: {?a,b,c}

reviews://{isbn}{?limit,sort}, limit ve sort parametrelerini ? işaretinin ardına koyar. Yol hangi kitap olduğunu belirler; sorgu onu nasıl okuduğunuzu ayarlar.

Sorgu parametreleri esnek eşleştirilir: sıra önemli değildir, fazlalıklar yok sayılır ve verilmeyen parametreler fonksiyonunuzun varsayılanlarına düşer. Yani reviews://978-0441172719, limit=10, sort="newest" kullanır; reviews://978-0441172719?sort=top ise yalnızca sort değerini geçersiz kılar.

Liste olarak yol segmentleri: {/name*}

Her yol segmentini eğik çizgili tek bir dize yerine ayrı birer liste öğesi olarak istiyorsanız {/name*} kullanın. shelves://browse{/path*} ile, shelves://browse/fiction/sci-fi okuyan bir istemci browse_shelf(["fiction", "sci-fi"]) çağrısına yol açar.

Şablon başvurusu

En yaygın kalıplar:

Kalıp Örnek girdi Elde ettiğiniz
{name} alice "alice"
{name} docs/intro.md eşleşme yok (/ karakterinde durur)
{+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"]

Ayrıştırıcının reddettikleri

Birkaç şablon biçimi, ilk istekte başarısız olmak yerine en baştan yakalanır. @mcp.resource, şablonu dekoratör çalıştığında ayrıştırır; bu yüzden bunların hiçbiri çalışan bir sunucuya ulaşmaz.

UriTemplate.parse(), şu durumlarda InvalidUriTemplate fırlatır:

  • Aralarında hiçbir şey olmayan iki değişken. manuals://{+path}{ext} reddedilir: eşleştirme, path değişkeninin nerede bitip ext değişkeninin nerede başladığını ayırt edemez. Aralarına bir sabit koyun (manuals://{+path}/{ext}) ya da kendi ayırıcısını sağlayan bir operatör kullanın. manuals://{+path}{.ext} kabul edilir, çünkü {.ext} . karakterini kendisi getirir.
  • Birden fazla çok segmentli değişken. Şablon başına en fazla bir {+var}, {#var} ya da patlatılmış (exploded) değişken ({/var*}, {.var*}, {;var*}). İki tanesi doğası gereği belirsizdir: fazladan bir segmenti hangisinin yutacağına karar vermenin ilkeli bir yolu yoktur.
  • Olağan sözdizimi hataları: kapatılmamış bir süslü parantez, iki kez kullanılan bir değişken adı ya da SDK'nın desteklemediği bir RFC 6570 özelliği, örneğin {var:3} önek değiştiricisi veya {?vars*} sorgu patlatması.

Bunun üstüne, bir işleyici parametresi şablonun sondaki {?...}/{&...} dizisindeki bir sorgu değişkenine bağlı olup Python varsayılanı yoksa @mcp.resource ValueError fırlatır. Bu değişkenler esnek eşleştirilir (istemci herhangi birini atlayabilir); bu yüzden varsayılanı olmayan bir parametre, onu atlayan ilk istekte yalnızca anlaşılmaz bir iç hata olarak ortaya çıkardı. Yukarıdaki sunucudaki reviews://{isbn}{?limit,sort} düzgün biçimli sürümdür: limit ve sort varsayılan taşır.

Güvenlik

Şablon parametreleri istemciden gelir. Denetlenmeden dosya sistemi veya veritabanı işlemlerine akarlarsa, ../../etc/passwd gibi değerler sunmayı amaçladığınız dizinin dışına çözümlenebilir.

SDK'nın varsayılan olarak denetledikleri

İşleyiciniz çalışmadan önce SDK, şu özelliklere sahip her parametreyi reddeder:

  • .. bileşenleriyle başlangıç dizininden kaçacak olanlar
  • mutlak yol (/etc/passwd, C:\Windows) ya da Windows sürücüye göreli yol (C:foo) gibi görünenler. Sürücüye göreli bir değer ile x:y gibi ad alanlı bir tanımlayıcı dize olarak ayırt edilemez; bu yüzden tek harf artı iki nokta üst üste biçimindeki her değer varsayılan olarak reddedilir. Parametre meşru olarak böyle değerler alıyorsa onu muaf tutun
  • null bayt (\x00) içerenler

.. denetimi alt dize taraması değil, bileşen tabanlıdır. v1.0..v2.0 ya da HEAD~3..HEAD gibi değerler geçer, çünkü orada .. tek başına bir yol segmenti değildir.

Bu denetimler kodu çözülmüş değere uygulanır; dolayısıyla URI içinde nasıl kodlanmış olursa olsun dizin geçişini yakalarlar (../etc, ..%2Fetc, %2E%2E/etc, ..%5Cetc, %00 hepsi yakalanır).

Check

Yukarıdaki sunucudan manuals://../etc/passwd okuyun; istek doğrudan reddedilir: şablon eşleştirme ilk başarısızlıkta durur, bu yüzden sonraki (muhtemelen daha gevşek) hiçbir şablon yedek olarak denenmez. İstemci, hiçbir şablonla eşleşmeyen bir URI için göreceği -32602 "Unknown resource" hatasının aynısını görür ve read_manual hiç çalışmaz.

Dosya sistemi işleyicileri: safe_join kullanın

Yerleşik denetimler yaygın durumları durdurur ama sizin sandbox sınırınızı bilemez. Dosya sistemi erişimi için yolu çözümlemek ve temel dizininizin içinde kaldığını doğrulamak üzere safe_join kullanın:

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, basit bir dize denetiminin kaçıracağı sembolik bağlantı kaçışlarını, .. dizilerini ve mutlak yol hilelerini yakalar. Çözümlenen yol DOCS_ROOT dışına çıkarsa PathEscapeError fırlatır; bu, istemciye ResourceError olarak yansır.

Varsayılanlar engel olduğunda

Bazen denetimler meşru değerleri engeller. Bir katalog içe aktarma aracı bilerek mutlak bir yol alabilir ya da bir parametre, işleyicinizin dosya sistemine dokunmadan güvenle yorumladığı ../sibling gibi göreli bir başvuru olabilir. O parametreyi muaf tutun ya da politikayı tüm sunucu için gevşetin:

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}"
  • Dekoratördeki security=ResourceSecurity(exempt_params={"source"}), denetimleri yalnızca o kaynaktaki o tek parametre için atlar. Sunucunun geri kalanı varsayılan politikayı korur.
  • MCPServer kurucusundaki resource_security=, her kaynak için varsayılanı belirler. Burada relaxed, .. denetimini tamamen kapatır.

Yapılandırılabilir denetimler:

Ayar Varsayılan Ne yapar
reject_path_traversal True Başlangıç dizininden kaçan .. dizilerini reddeder
reject_absolute_paths True /foo, C:\foo, UNC yollarını ve sürücüye göreli C:foo değerini reddeder (x:y de yakalanır)
reject_null_bytes True \x00 içeren değerleri reddeder
exempt_params boş Denetimlerin atlanacağı parametre adları

Bu denetimler sezgisel bir ön süzgeçtir; dosya sistemi erişimi için kapsama sınırı safe_join olmaya devam eder.

Tip

İşleyiciniz isteği karşılayamıyorsa (dosya yok, kimlik bilinmiyor) bir istisna fırlatın. SDK bunu bir hata yanıtına dönüştürür. Protokol hatası ile araç hatası arasındaki fark için Hataları ele alma sayfasına bakın.

Düşük seviyeli Server üzerinde kaynaklar

Düşük seviyeli Server üzerine inşa ediyorsanız (bkz. Düşük seviyeli Server), resources/list ve resources/read protokol metotları için işleyicileri doğrudan kaydedersiniz. Dekoratör yoktur; protokol türlerini kendiniz döndürürsünüz.

Statik kaynaklar

Sabit URI'ler için bir kayıt defteri tutun ve tam eşleşmeye göre yönlendirin:

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)

list işleyicisi istemcilere nelerin mevcut olduğunu bildirir; read işleyicisi içeriği sunar. Önce kayıt defterinizi denetleyin, varsa şablonlara (aşağıda) geçin, geri kalan her şey için istisna fırlatın.

Şablonlar

MCPServer'ın kullandığı şablon motoru mcp.shared.uri_template içinde yaşar ve tek başına çalışır. Aynı ayrıştırma ve eşleştirmeyi alırsınız; yönlendirmeyi ve güvenlik politikasını kendiniz kurarsınız.

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,
)

Vurgulanan satırlarda üç şey oluyor:

  • Bir kez ayrıştırın, istek başına eşleştirin. UriTemplate.parse() şablonu oluşturur; template.match(uri) çıkarılan değişkenleri dict olarak, URI uymuyorsa None döndürür. URL kod çözme match() içinde olur; kodu çözülmüş değerler yol güvenliği doğrulaması yapılmadan olduğu gibi döndürülür. Değerler dize olarak çıkar: kendiniz dönüştürün (int(matched["id"]), Path(matched["path"])).
  • Güvenlik denetimlerini kendiniz uygulayın. MCPServer'ın varsayılan olarak çalıştırdığı .. ve mutlak yol denetimleri mcp.shared.path_security içinde yaşar. read_manual_safely, MANUALS'a dokunmadan önce bunları çağırır. Bir parametre dosya sistemi yolu değilse (ISBN, arama sorgusu), o değer için denetimleri atlayın: politikayı bir yapılandırma nesnesi üzerinden değil, işleyici başına siz denetlersiniz.
  • Şablonları aynı kaynaktan listeleyin. İstemciler şablonları resources/templates/list üzerinden keşfeder. str(template) özgün şablon dizesini geri verir; böylece listeleme ile eşleştirici tek bir doğruluk kaynağını paylaşır.

Özet

  • {name} tek bir segmentle eşleşir; {+name} eğik çizgileri korur; {?a,b} sorgu dizesinden çeker; {/name*} segmentleri bir listeye böler.
  • Aralarında hiçbir şey olmayan iki değişken ya da ikinci bir çok segmentli değişken ayrıştırma anında reddedilir. Sondaki bir {?...}/{&...} sorgu değişkenine bağlı parametre bir Python varsayılanı bildirmelidir.
  • Parametreye tür ipucu verin (order_id: int); SDK dönüştürür.
  • Varsayılan güvenlik politikası .., mutlak yolları ve null baytları işleyiciniz çalışmadan önce reddeder; kaynak başına security=ResourceSecurity(...) ile, sunucu genelinde resource_security= ile geçersiz kılın.
  • Dosya sistemi erişimi için kapsama sınırı safe_join'dur.
  • Düşük seviyeli Server üzerinde UriTemplate.parse() ile ayrıştırın, .match() ile eşleştirin ve mcp.shared.path_security'yi kendiniz uygulayın.