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:
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"verirmanuals://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,pathdeğişkeninin nerede bitipextdeğ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 ilex:ygibi 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:
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:
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. MCPServerkurucusundakiresource_security=, her kaynak için varsayılanı belirler. Buradarelaxed,..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:
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.
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şkenleridictolarak, URI uymuyorsaNonedöndürür. URL kod çözmematch()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 denetimlerimcp.shared.path_securityiç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şınasecurity=ResourceSecurity(...)ile, sunucu genelinderesource_security=ile geçersiz kılın. - Dosya sistemi erişimi için kapsama sınırı
safe_join'dur. - Düşük seviyeli
ServerüzerindeUriTemplate.parse()ile ayrıştırın,.match()ile eşleştirin vemcp.shared.path_security'yi kendiniz uygulayın.