docs: publish translated docs in twelve languages and the tool that maintains them (#3280)

This commit is contained in:
Max 2026-08-14 17:07:34 +01:00 • committed by GitHub
commit 52ad0a8876
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 89737 additions and 54 deletions

View file

@ -0,0 +1,134 @@
---
translation:
sections: [b50152f05c81e786, b302059b22fb7cb4, 85682a1bf561243a, 53fc48838eb6837a, b24190e0842786ec, 85f93e150fc9b240]
tool: 1
---
# Context nesnesi {#the-context}
Bir aracın argümanları modelden gelir. Geri kalan her şey (hizmet verdiğiniz istek, içinde yaşadığınız sunucu, istemciye geri konuşmanın bir yolu) tek bir nesneden gelir: **`Context`**.
Onu siz oluşturmazsınız, yapılandırmazsınız da. Yalnızca istersiniz.
## İsteyin {#ask-for-it}
Herhangi bir araca `Context` ile işaretlenmiş bir parametre ekleyin:
```python title="server.py" hl_lines="2 8"
--8<-- "docs_src/context/tutorial001.py"
```
* SDK her istek için yeni bir `Context` oluşturur ve onu içeri geçirir.
* Parametrenin **adı önemli değildir**. `ctx`, `context`, `c`: SDK onu tür işaretinden bulur.
* Kaynaklar ve prompt'lar da aynı şekilde bir tane bildirebilir.
* `ctx.request_id`, fonksiyonunuzun şu anda hizmet verdiği isteğin kimliğidir.
!!! info
FastAPI kullandıysanız bu hareketi görmüşsünüzdür: bir parametreyi çatının kendi türüyle
(orada `Request`, burada `Context`) bildirirsiniz ve çatı onu sağlar. Kaydedilecek bir şey yok,
yapılandırılacak bir şey yok: mekanizmanın tamamı tür işaretinden ibarettir.
### Model için görünmez {#invisible-to-the-model}
İçselleştirilmesi gereken kısım burası. `tools/list`'in `search_books` için bildirdiği girdi şeması şöyle:
```json
{
"type": "object",
"properties": {
"query": {"title": "Query", "type": "string"}
},
"required": ["query"],
"title": "search_booksArguments"
}
```
Tek bir özellik. `ctx` bir argüman değildir: şemada asla görünmez, modele asla söylenmez ve hiçbir istemci onu dolduramaz. Sizinle SDK arasındaki bir sözleşmedir, iletilen veride görünmez.
### Deneyin {#try-it}
Sunucuyu MCP Inspector ile çalıştırın:
```console
uv run mcp dev server.py
```
`search_books` formunda tek bir `query` alanı var. Onu `dune` ile çağırın:
```text
[request 3] Found 3 books matching 'dune'.
```
Sayı, bu isteğin denk geldiği sıra numarasıdır. Aracı yeniden çağırın, değişir: her istek kendi `Context`'ini alır.
## Size ne sağlar {#what-it-gives-you}
Enjekte edilen nesne küçüktür. `request_id` dışında:
* `await ctx.read_resource(uri)`: bir aracın içinden sunucunun **kendi** kaynaklarından birini okur. Bir sonraki bölüm.
* `await ctx.report_progress(progress, total, message)`: uzun bir çağrı sırasında çağırana ilerlemeyi akış halinde bildirir. Ayrıntıların tamamı **[İlerleme](progress.md)** sayfasında.
* `await ctx.elicit(message, schema)` ve `await ctx.elicit_url(...)`: aracı duraklatır ve kullanıcıya bir soru sorar. Bu da **[Elicitation](elicitation.md)**.
* `ctx.session`: bu istemciyle konuşmanın sunucu tarafı. İstemciye gönderdiğiniz bildirimler burada yaşar; son bölüm onu kullanır.
* `ctx.headers`: aktarımın taşıdığı istek başlıkları, stdio'da ise `None`. Özel bir başlığı `(ctx.headers or {}).get("x-...")` ile okuyun. Başlıklar istemcinin sağladığı girdidir; bir yerel ayar ya da özellik bayrağı için uygundur, kimlik için asla.
* `ctx.request_context`: istek başına tutulan ham kayıt. Elinizin gideceği alan `lifespan_context`'tir, yani başlangıç kodunuzun yield ettiği nesne (bkz. **[Lifespan](lifespan.md)**).
Log tutma bu listede bilerek yok. Bir sunucu, diğer her Python programı gibi Python'ın `logging` modülüyle log tutar. **[Log tutma](logging.md)** bunun nedenini anlatan kısa sayfadır.
!!! tip
Enjeksiyon yalnızca kaydettiğiniz fonksiyon için gerçekleşir. Aracınızın çağırdığı bir yardımcı
fonksiyon kendi `Context`'ini almaz; `ctx`'i sıradan bir argüman olarak aşağıya geçirin. Başka bir
yerden alınabilecek ortamda asılı bir "geçerli bağlam" yoktur.
## Kendi kaynaklarınızı okuma {#read-your-own-resources}
Bir sunucunun kaynakları yalnızca istemciler için değildir. Bir araç da onları okuyabilir:
```python title="server.py" hl_lines="16"
--8<-- "docs_src/context/tutorial002.py"
```
`ctx.read_resource`, URI'yi `resources/read`'e hizmet veren aynı kayıt defteri üzerinden çözümler; böylece araç, istemcinin alacağının aynısını alır: içerik bloğu başına bir tane olmak üzere `ReadResourceContents` öğelerinden oluşan yinelenebilir bir nesne. Bu URI için bir tane var:
```python
contents.content # 'fiction, non-fiction, poetry'
contents.mime_type # 'text/plain'
```
* `content`, `genres()`'in döndürdüğünün ta kendisidir. Tek bir doğruluk kaynağı: istemci kaynağa göz atar, araçlarınız onu tüketir, kimse dizgeyi kopyalamaz.
* `describe_catalog`'un tek parametresi `Context`'tir, bu yüzden girdi şemasında **hiçbir özellik yoktur**. Model onu `{}` ile çağırır.
## İstemciye listenin değiştiğini söyleme {#tell-the-client-the-list-changed}
Bir sunucunun sundukları içe aktarma anında sabitlenmez. Çalışma zamanında bir araç kaydedin, ardından istemciye söyleyin:
```python title="server.py" hl_lines="15-16"
--8<-- "docs_src/context/tutorial003.py"
```
* `mcp.add_tool(recommend_book)` düz bir fonksiyonu araç olarak kaydeder: ad, açıklama ve şema tam olarak `@mcp.tool()`'un yapacağı gibi türetilir.
* `await ctx.session.send_tool_list_changed()`, `notifications/tools/list_changed` bildirimini gönderir. Onu alan bir istemci `tools/list`'i yeniden çağırır ve `recommend_book`'u görür.
Kardeşleri `send_resource_list_changed()`, `send_prompt_list_changed()` ve belirli tek bir kaynaktaki değişiklik için `send_resource_updated(uri)`'dir.
Bir 2026-07-28 bağlantısında istemciler değişiklik bildirimlerini yalnızca kendilerinin açtığı bir `subscriptions/listen` akışı üzerinden alır; bu yüzden yukarıdaki `send_*` yöntemleri o akışlara ulaşmaz. `Context`'in yayımlama yöntemleri abone olunmuş tüm akışlara aynı anda iletir: `await ctx.notify_tools_changed()`, `await ctx.notify_prompts_changed()`, `await ctx.notify_resources_changed()` ve `await ctx.notify_resource_updated(uri)`. Kopyalar arasında ölçekleme dahil ayrıntıların tamamı **[Abonelikler](subscriptions.md)** sayfasında.
!!! check
Kimse `enable_recommendations`'ı çalıştırmadan önce, vaat ettiğiniz araç mevcut değildir. Yine de
çağırın; sonuç, modelin okuyabileceği bir hatadır:
```text
Unknown tool: recommend_book
```
`enable_recommendations`'ı çalıştırın, aynı çağrı bu kez başarılı olur. Araç listesi gerçekten
dinamiktir: `tools/list`, *tam şu anda* ne kayıtlıysa onu yansıtır.
## Özet {#recap}
* Bir parametreyi `Context` ile işaretleyin (bir araçta, kaynakta ya da prompt'ta), SDK onu enjekte eder. Ad size kalmış.
* Model için görünmezdir: girdi şeması yalnızca gerçek argümanlarınızı içerir.
* `ctx.request_id` isteği tanımlar; `ctx.request_context.lifespan_context` başlangıç kodunuzun yield ettiği şeydir.
* `await ctx.read_resource(uri)`, bir aracın sunucunun kendi kaynaklarını okumasını sağlar.
* `ctx.session` istemciye giden geri kanaldır: `send_tool_list_changed()` ve kardeşleri, değiştirdiğiniz bir listeyi yeniden çekmesini söyler.
* İlerleme bildirme ve elicitation da `Context`'ten başlar; her birinin kendi sayfası var.
Modelin asla görmediği, kendi fonksiyonlarınızın doldurduğu parametreler **[Bağımlılıklar](dependencies.md)** sayfasında.

View file

@ -0,0 +1,168 @@
---
translation:
sections: [b0389403e98d25ad, e2cf58b43b285e86, a363e1a38e1a5971, 6cfac078feb18013, b4535bd61df337e6, e97ed44207f929fd]
tool: 1
---
# Bağımlılıklar {#dependencies}
Bir aracın argümanları modelden gelir. Bazı değerlerse asla modelden gelmemelidir: kayıtlarınızdan bakılan bir fiyat, yalnızca bir insanın verebileceği bir onay, modelin uydurarak yanlış yapabileceği her şey.
**Bağımlılıklar**, kendi fonksiyonlarınızın doldurduğu parametrelerdir. Parametreye tür açıklamasını eklersiniz, fonksiyonu belirtirsiniz; SDK da araç çalışmadan önce onu çağırır.
## Bir bağımlılık bildirme {#declare-one}
Parametrenin türünü `Annotated[...]` içine sarın ve `Resolve(fn)` ekleyin:
```python title="server.py" hl_lines="18-19 23"
--8<-- "docs_src/dependencies/tutorial001.py"
```
* `check_stock` bir **çözümleyicidir**: SDK'nın `reserve_book`'tan önce çalıştırdığı, dönüş değeri `stock` argümanı hâline gelen sıradan bir fonksiyon.
* `title` parametresi, aracın kendi `title` argümanıdır ve **ada göre** eşleştirilir. Çözümleyici, araç gövdesinin göreceği doğrulanmış değerin aynısını görür.
* Araç gövdesi, zaten var olan bir `Stock` ile işe başlar. Araçta arama kodu yok, "ya yoksa" diye başlayan bir giriş yok.
!!! info
FastAPI kullandıysanız bu, `Depends`'in karşılığıdır. Aynı hamle, aynı gerekçe: fonksiyon
neye ihtiyacı olduğunu bildirir, framework bunu sağlar ve bağlantı tür açıklamasında durur.
### Modele görünmez {#invisible-to-the-model}
`tools/list`'in `reserve_book` için bildirdiği giriş şeması şöyle:
```json
{
"type": "object",
"properties": {
"title": {"title": "Title", "type": "string"}
},
"required": ["title"],
"title": "reserve_bookArguments"
}
```
Tek bir özellik. **[Context nesnesi](context.md)** sayfasındaki `Context` gibi, çözümlenmiş bir parametre de sizinle SDK arasındaki bir sözleşmedir: `stock` şemada yer almaz, modele ondan hiç söz edilmez ve yine de bir `stock` değeri gönderen istemci yok sayılır. Aracınızın alabileceği tek değer çözümleyicinin değeridir.
Asıl mesele de bu son kısım. Modelin sağlayamadığı bir parametre, modelin yanlış yapamayacağı bir parametredir.
### Deneyin {#try-it}
Sunucuyu MCP Inspector ile çalıştırın:
```console
uv run mcp dev server.py
```
`reserve_book` formunda tek bir `title` alanı var. `stock` hiçbir yerinde yok. `Dune` ile çağırın:
```text
Reserved 'Dune' (6 copies left).
```
Araç gövdesi hiçbir şey aramadı: önce `check_stock` çalıştı, döndürdüğü `Stock` da argüman olarak geldi. `Neuromancer`'ı deneyin; aynı çözümleyici araca sıfır verir.
!!! tip
Araç gövdesinde doğrudan `check_stock(title)` da çağırabilirdiniz. Değer bir yardımcı fonksiyon
çağrısından fazlasını hak ettiğinde onu bağımlılık olarak bildirin: stok bilgisine ihtiyaç duyan
her araç aynı parametreyi bildirir ve kaç tanesi bildirirse bildirsin SDK çözümleyiciyi çağrı
başına en fazla bir kez çalıştırır. Sonraki bölümler geri kalanını ekler: birbirine bağımlı
çözümleyiciler ve kullanıcıya soru soran çözümleyiciler.
## Bağımlılıkların bağımlılıkları {#dependencies-of-dependencies}
Bir çözümleyici, aynı tür açıklamasıyla kendi bağımlılıklarını bildirebilir:
```python title="server.py" hl_lines="22 29-30"
--8<-- "docs_src/dependencies/tutorial002.py"
```
* `estimate_delivery`, `check_stock`'a bağımlıdır. SDK grafiği sırayla çalıştırır: önce stok, sonra tahmin, sonra araç.
* Hem `stock` hem `delivery` sonuçta `check_stock`'a ihtiyaç duyar, ama o **çağrı başına bir kez** çalışır. Tek bir envanter sorgusu, iki tüketici.
* Kaydedilecek hiçbir şey yok. Grafik, tür açıklamalarının *ta kendisidir*.
!!! check
Çağrı başına bir kez çalıştığına körü körüne inanmayın. `check_stock`'un içine bir `print` koyun
ve Inspector'dan `order_book`'u çağırın: çağrı başına tek satır. İki tüketici, tek sorgu.
SDK grafiği araç çağrıldığında değil, kaydedildiğinde analiz eder. Sınıflandıramadığı bir parametre (ne `Context`, ne `Resolve(...)`, ne de bir araç argümanının adı) ve çözümleyiciler arasındaki bir döngü, her ikisi de başlangıçta `InvalidSignature` fırlatır. Sunucu, daha hiçbir istemci bağlanmadan başarısız olur; hataya yol açan parametre veya çözümleyici hata mesajında adıyla belirtilir.
Bir çözümleyicinin parametreleri tıpkı bir aracınkiler gibi çözümlenir: başka bir `Resolve(...)`, ada göre aracın kendi argümanları veya `Context` (`ctx.headers`, lifespan (yaşam döngüsü) nesnesi, hepsi).
!!! warning
HTTP aktarımlarında `Context`, `ctx.headers`'ı da içerir. Başlıklar, her araç argümanı gibi
**istemcinin sağladığı girdidir**: bir yerel ayar veya özellik bayrağı için uygundur, kimlik için
asla. Çağıranın kim olduğu, herkesin ayarlayabileceği bir başlıktan değil, yetkilendirme
katmanınızdan gelir (**[Yetkilendirme](../run/authorization.md)**).
!!! tip
*Çağrı başına bir kez* tam olarak bunu ifade eder: bir sonraki `tools/call`, `check_stock`'u
yeniden çalıştırır. Bir istekten uzun yaşaması gereken bir kaynağın (bir veritabanı havuzu, bir
HTTP istemcisi) yeri **[Lifespan](lifespan.md)** sayfasıdır; bir çözümleyici ona
`ctx.request_context.lifespan_context` üzerinden ulaşabilir.
## Yalnızca gerektiğinde sormak {#ask-when-you-must}
Bir çözümleyici yanıtı bilmek zorunda değildir. `Elicit(message, Model)` döndürebilir; SDK da kullanıcıya sorar. Bu, sizin yerinize çalıştırılan **[Elicitation](elicitation.md)** (kullanıcıdan bilgi isteme) mekanizmasıdır:
```python title="server.py" hl_lines="26-32 39"
--8<-- "docs_src/dependencies/tutorial003.py"
```
* Stokta varsa: `confirm_backorder` doğrudan bir `Backorder` döndürür. **Soru yok, gidiş-dönüş yok.** Kullanıcı yalnızca yanıtı önemli olduğunda rahatsız edilir.
* Stokta yoksa: SDK elicitation'ı gönderir, yanıtı `Backorder`'a göre doğrular ve enjekte eder. Çözümleyiciniz protokole hiç dokunmaz.
* Araç, `backorder.confirm`'ü diğer argümanlar gibi okur. **Hayır** yanıtı da bir yanıttır: elicitation `confirm=False` ile kabul edilir, araç çalışır ve sipariş verilmez. Sormak, araç gövdesindeki bir tesisat işi değil, bir ön koşul hâline geldi.
Peki ya kullanıcı hiç yanıt vermezse, yani soruyu reddeder veya iptal ederse?
!!! check
`Neuromancer` için `order_book`'u çalıştırın ve soruyu reddedin. Tür açıklaması
`Annotated[Backorder, Resolve(...)]` biçiminde yazıldığında araç gövdesi hiç çalışmaz; çağrı,
modelin okuyabileceği bir hata sonucuyla başarısız olur:
```text
Error executing tool order_book: Resolver for parameter 'backorder' could not resolve: elicitation was decline
```
Bir ön koşul için doğru varsayılan budur: yanıt yoksa sipariş de yok. Reddetme, aracınızın ele almak istediği bir sonuçsa (ön siparişi atlayıp yine de başka bir kitap önermek gibi) tür açıklamasını bunun yerine `ElicitationResult[Backorder]` olarak yazın; araç, dallanabileceği tam accept/decline/cancel sonucunu alır. **[Elicitation](elicitation.md)** sayfası bu biçimi ve sormaya dair diğer her şeyi gösterir: şema kuralları, üç yanıt, konuşmanın istemci tarafı.
!!! info
Framework, sorunun aktarımını anlaşılan protokol sürümüne göre seçer; yukarıdaki kod her
ikisinde de aynıdır. **2026-07-28** ve sonrasında soru, çok turlu (multi-round-trip) bir
`tools/call` içinde taşınır: sunucu soruyu döndürür, istemcinin `elicitation_callback`'i
yanıtlar ve `Client` çağrıyı sizin yerinize yeniden dener
(**[Çok turlu istekler](multi-round-trip.md)**). **2025-11-25** ve öncesinde ise çağrının
ortasında senkron bir elicitation isteğidir. Her soru çağrı başına tam olarak bir kez sorulur;
bu, çözümleyici hakkında değil soru hakkında bir garantidir. Çok turlu biçimde, çağrı bir sorudan
sonra her sürdüğünde herhangi bir çözümleyici yeniden çalışabilir; dolayısıyla
`return Elicit(...)` satırından önceki kod bu turların her birinde çalışır. Kaydedilmiş yanıt,
tekrarlanan soruyu kullanıcıya yeniden sormadan karşılar. Kaydedilmiş bir yanıta yalnızca
çözümleyici soru sorduğunda başvurulur; `check_stock` gibi *sormadan* yanıt veren bir
çözümleyici her zaman kendi hesapladığı değeri sağlar. Her yanıt kendi sorusuyla
eşleştirildiğinden, elicitation yapan bir çözümleyici sorusunu aracın argümanlarından ve önceki
yanıtlardan deterministik olarak türetmelidir. Çağrı başına üretilen bir değer (bir
`default_factory` kimliği, bir zaman damgası) her turda yeniden türetilir ve yanıtın bağlanması
gereken bir soruda yer almamalıdır. Bu tür uçucu verilerden kurulan bir soru, kaydedilmiş her
yanıtı bayat gösterir; bu yüzden sunucu, istemcinin tur sınırı çağrıyı sonlandırana kadar
soruyu her turda yeniden sorar.
## Kullanıcıya değil, istemciye sormak {#ask-the-client-not-the-user}
Elicitation, bir çözümleyicinin sorabileceği üç sorudan biridir ve çok turlu akış başkasına izin vermez. Diğer ikisi kullanıcıya değil **istemciye** gider: istemci üzerinden bir LLM çağrısı çalıştırmak için `Sample(...)` (bir `sampling/createMessage` isteği), istemcinin güncel kök dizinlerini (roots) almak için `ListRoots()` döndürün. Hiçbirinin accept/decline sonucu yoktur; tüketici, tür açıklaması olarak doğrudan sonuç türünü yazar: `CreateMessageResult` (istek `tools` veya `tool_choice` taşıdığında `CreateMessageResultWithTools`) ya da `ListRootsResult`:
```python title="server.py" hl_lines="10-15 21"
--8<-- "docs_src/dependencies/tutorial004.py"
```
* Framework bunları tıpkı `Elicit` gibi yönlendirir: **2026-07-28** sürümünde çok turlu `tools/call` içinde, **2025-11-25** sürümünde bağımsız sunucu->istemci isteği üzerinden. Bildirilmemiş bir yetenek, çağrıyı `-32021` protokol hatasıyla reddeder (`sampling`, `roots`, form kipinde `elicitation`; istek `tools` veya `tool_choice` taşıdığında `sampling.tools`).
* Yukarıdaki bilgi kutusunun sorular hakkında söylediği her şey aynen geçerlidir: bir `Sample` isteği, kaydedilmiş sonucuyla birebir gösterimine göre eşleştirilir; bu yüzden onu aracın argümanlarından ve önceki yanıtlardan deterministik olarak kurun. Böylece istemci, LLM çağrısının bedelini tur başına değil araç çağrısı başına bir kez öder. Kaydedilmiş sonuç çağrının geri kalanında `request_state` içinde taşınır; bu nedenle çok büyük bir tamamlama, kalan her gidiş-dönüşü ağırlaştırır.
* Bağımsız örnekleme (sampling) ve kök dizinler *özellikleri* 2026-07-28 itibarıyla kullanım dışı bırakıldı (SEP-2577). İstemcinin modeline ihtiyaç duyan yeni sunucular bu taşıyıcı üzerinden sorar; duymayanlar doğrudan bir LLM sağlayıcısıyla entegre olmalıdır. `"none"` dışındaki `include_context` değerlerinin kendisi de kullanım dışıdır; bunları kullanmayın.
## Özet {#recap}
* Bir araç parametresinde `Annotated[T, Resolve(fn)]`: SDK `fn`'i çalıştırır ve dönüş değerini enjekte eder.
* Çözümlenmiş bir parametre modele görünmez; istemci de onu sağlayamaz. Modelin uydurmaması gereken değerlerin (fiyatlar, kimlikler, izinler) yeri burasıdır.
* Bir çözümleyicinin parametreleri de aynı şekilde çözümlenir: `Context`, başka bir `Resolve(...)` veya ada göre bir araç argümanı. Grafik, kaç tüketicisi olursa olsun her çözümleyiciyi tur başına en fazla bir kez çalıştırır; her soru tam olarak bir kez sorulur ve çağrı bir sorudan sonra sürdüğünde herhangi bir çözümleyici yeniden çalışabilir.
* Hatalı grafikler çağrı ortasında değil, kayıt sırasında `InvalidSignature` ile başarısız olur.
* Kullanıcıya sormak için, yalnızca mecbur kaldığınızda, `Elicit(message, Model)` döndürün. Sarmalanmamış tür açıklamaları reddedildiğinde çağrıyı iptal eder; `ElicitationResult[T]` aracın dallanmasına izin verir.
* İstemciden bir LLM tamamlaması veya kök dizin listesi istemek için `Sample(...)` ya da `ListRoots()` döndürün; yalın sonuç enjekte edilir.
Sunucunuzun başlangıçta bir kez kurduğu durum ve bir işleyicinin ona nasıl ulaştığı **[Lifespan](lifespan.md)** sayfasının konusudur.

View file

@ -0,0 +1,191 @@
---
translation:
sections: [335ca2a0b266f003, d1ad562d3fe87bc0, 0bb1396c86daeba4, d1cb1235bb9ee267, 833179c09d239c83, e5d6dec2d2e655e8]
tool: 1
---
# Elicitation {#elicitation}
İşinin yarısına gelmiş ve tek bir yanıtı eksik olan bir aracın başarısız olması gerekmez.
**Elicitation** (kullanıcıdan bilgi isteme) onun sormasını sağlar. Araç çağrısının ortasında kullanıcıya bir soru gelir ve verdiği yanıt aynı fonksiyon çağrısına geri döner.
İki mod var:
* **Form modu**: bir değere ihtiyacınız vardır (bir onay, bir tarih, bir miktar). Alanları siz tanımlarsınız, formu istemci çizer.
* **URL modu**: kullanıcının başka bir yere gitmesi gerekir (bir OAuth onay ekranı, bir ödeme sayfası). Orada yaptığı hiçbir şey protokolden geçmez.
Sormanın da iki yolu var. İlk başvurmanız gereken bir **çözümleyicidir**: soruyu bir parametreye asarsınız ve SDK sorar; hangi bağlantı olursa olsun, istemci hangi protokol neslini konuşursa konuşsun. Doğrudan yol olan `await ctx.elicit(...)`, *sunucudan* *istemciye* giden bir istektir; bu kanal yalnızca eski nesil bir bağlantıdaki (spesifikasyon sürümü 2025-11-25 veya öncesi) istemciler için vardır. İkisi de bu sayfada; çözümleyiciyle başlayın.
## Çözümleyiciyle sorma {#ask-with-a-resolver}
Aracın tamamının önünde duran bir soru (*emin misiniz? eşleşen üç hesaptan hangisi?*) araç gövdesinden çıkarılıp bir **çözümleyiciye** taşınabilir; soruyu sizin yerinize framework sorar.
`Annotated[T, Resolve(fn)]` ile işaretlenmiş bir parametre, araç gövdesinden önce `fn` çalıştırılarak doldurulur. Çözümleyici değeri zaten biliyorsa doğrudan döndürür; framework'ün sormasını istiyorsa `Elicit(...)` döndürür:
```python title="server.py" hl_lines="24-30 35-36"
--8<-- "docs_src/elicitation/tutorial004.py"
```
* `confirm_delete`, aracın kendi `path` argümanını adıyla okur, klasörü listeler ve **yalnızca gerektiğinde sorar**: boş bir klasör, istemciye hiç gidip dönmeden `Confirm(ok=True)` olarak çözümlenir.
* `delete_folder`, `ElicitationResult[Confirm]` tür ipucunu kullanır; bu yüzden framework sonucun tamamını enjekte eder ve araç her durumu `match` ile ele alır: kabul edip onaylama, kabul edip tutma (`ok=False`), reddetme, iptal.
* `confirm` parametresi aracın girdi şemasında hiç görünmez: `path`'i istemci sağlar, `confirm`'ü çözümleyici.
Aracın dallanması gerekmiyorsa bunun yerine sarmalanmamış modeli işaretleyin (`Annotated[Confirm, Resolve(confirm_delete)]`): kabulde modeli alır, ret veya iptalde ise çağrı bir hatayla sonlanır.
Çözümleyici **her** bağlantıda çalışır. Eski nesil bağlantıdaki bir istemciye SDK soruyu doğrudan gönderir; **2026-07-28** bağlantısında ise SDK soruyu çağrıdan *döndürür* ve istemcinin bir sonraki denemesi yanıtı taşır. Çözümleyiciniz aradaki farkı hiçbir zaman bilmez; arka planda olan biten **[Çok turlu istekler](multi-round-trip.md)** (multi-round-trip) sayfasında.
Sormak, bir çözümleyicinin yapabileceklerinden yalnızca biri. Genel mekanizma (sormadan hesaplayan bağımlılıklar, bağımlılıkların bağımlılıkları, modelin neyi sağlayıp neyi sağlayamayacağı) **[Bağımlılıklar](dependencies.md)** sayfasında.
## Aracın içinden sorma {#ask-from-inside-the-tool}
Bir araç kendi gövdesinin ortasında durup da sorabilir.
!!! warning
`ctx.elicit()` ve `ctx.elicit_url()`, *sunucudan* *istemciye* giden isteklerdir; bu kanal
yalnızca eski nesil bir bağlantıdaki (spesifikasyon sürümü **2025-11-25** veya öncesi)
istemciler için vardır. **2026-07-28** bağlantısında sunucunun başlattığı istek yoktur,
bu yüzden bu çağrılar başarısız olur. Çözümleyici ikisinde de çalışır. Ayrıntıların tamamı
**[Protokol sürümleri](../protocol-versions.md)** sayfasında.
`await ctx.elicit()` bir mesaj ve bir Pydantic modeli alır:
```python title="server.py" hl_lines="9-11 20-23 25"
--8<-- "docs_src/elicitation/tutorial001.py"
```
* Size `ctx.elicit`'i veren **`Context`** parametresidir; her araç bir tane alabilir. Bu nesnenin kendi sayfası var: **[Context nesnesi](context.md)**.
* `AlternativeDate`, istediğiniz yanıtın **şemasıdır**.
* Araç `async def`. Öyle olmak zorunda: ortada durup bir insanı bekler.
* Başka herhangi bir tarihte araç hemen döner. Yalnızca mecbur kaldığında sorar.
* Kullanıcının kabul ettiği tarih yine `book_table`'ın kendisinden geçer. Yanıt da diğerleri gibi bir girdidir: kendisi de tamamen dolu olan bir alternatif körlemesine onaylanmaz, yeniden sorulur.
### İstemcinin aldığı {#what-the-client-receives}
İstemci mesajınızı ve yanında modelden üretilmiş bir JSON Schema alır:
```json
{
"properties": {
"accept_alternative": {
"description": "Try another date?",
"title": "Accept Alternative",
"type": "boolean"
},
"date": {
"default": "2025-12-26",
"description": "Alternative date (YYYY-MM-DD)",
"title": "Date",
"type": "string"
}
},
"required": ["accept_alternative"],
"title": "AlternativeDate",
"type": "object"
}
```
Bu şema formun kendisidir. `Field(description=...)` etikettir; bir varsayılan değer girdiyi önceden doldurur ve alanı isteğe bağlı yapar. Bu, **[Araçlar](../servers/tools.md)** sayfasının bir aracın argümanları için anlattığı Pydantic'ten JSON Schema'ya dönüşüm mekanizmasının aynısıdır.
!!! warning
Bir elicitation şeması, bir aracın girdi şeması kadar ifade gücüne sahip değildir. Yalnızca
düz, ilkel alanlar: `str`, `int`, `float`, `bool` veya dizelerden oluşan bir `Literal`
(bir `enum`'a dönüşür). Modelin içine bir model koyarsanız `ctx.elicit`, istemciye hiçbir
şey gönderilmeden önce istisna fırlatır:
```text
TypeError: Elicitation schema field 'address' rendered as {'$ref': '#/$defs/Address'}, which is not a valid PrimitiveSchemaDefinition
```
Bir insanı işinin ortasında bölüyorsunuz. Yanıt iç içe yapı gerektiriyorsa, o zaten
aracın bir argümanı olmalıydı.
### Üç yanıt {#the-three-answers}
`result.action` kullanıcının ne yaptığını söyler ve tam olarak üç olasılık vardır:
* `"accept"`: formu gönderdi. `result.data`, zaten doğrulanmış bir `AlternativeDate` örneğidir.
* `"decline"`: hayır dedi.
* `"cancel"`: seçim yapmadan soruyu kapattı.
`result.data` yalnızca `"accept"` durumunda vardır; örneğin önce `result.action`'ı denetlemesinin nedeni budur. Tür denetleyiciniz bu sırayı zorunlu kılar: `result.action == "accept"` sonrasında `result.data` bir `AlternativeDate`'tir; öncesinde `.data` diye bir şey hiç yoktur.
Ret bir hata değildir. Reddetmenin ne anlama geldiğine araç karar verir (burada: rezervasyon yok) ve modele normal şekilde yanıt verir.
!!! tip
Yanıt, kodunuz görmeden önce modelinize göre doğrulanır. Bir `bool` için `"maybe"` gönderen
bir istemci rezervasyonunuzu bozmaz: çağrı bir şema uyuşmazlığı hatasıyla başarısız olur,
`if`'iniz hiç çalışmaz.
## Kullanıcıyı bir URL'ye gönderme {#send-the-user-to-a-url}
Bazı şeyler modelden veya istemciden geçmemelidir: kimlik bilgileri, kart numaraları, OAuth onayı. Bunlar için veri istemezsiniz; kullanıcıdan bir yere gitmesini istersiniz:
```python title="server.py" hl_lines="10-14 23"
--8<-- "docs_src/elicitation/tutorial002.py"
```
* `ctx.elicit_url()`; mesajı, ziyaret edilecek **URL**'yi ve sizin seçtiğiniz bir `elicitation_id`'yi alır: sunucunuz içinde bu elicitation'ı tanımlayan herhangi bir dize.
* Sonuçta bir eylem vardır, başka hiçbir şey yoktur. `"accept"`, kullanıcının URL'yi açmayı kabul ettiği anlamına gelir; öbür taraftaki işi bitirdiği anlamına **gelmez**.
* Ödeme bant dışında, kullanıcının tarayıcısı ile ödeme sağlayıcınız arasında gerçekleşir. MCP üzerinden hiçbir içerik geri gelmez.
İkinci araca bakın. Sunucunuz bant dışı akışın bittiğini öğrendiğinde (bir webhook, bir yoklama; burada ikinci bir araç olarak modellenmiş), `ctx.session.send_elicit_complete(...)` aynı `elicitation_id` ile `notifications/elicitation/complete` gönderir. İstemci, *"ödeme bekleniyor..."* göstermeyi bırakabileceğini böyle anlar. Bu olmadan istemci yalnızca tahmin yürütebilir.
## İstemci tarafı {#the-client-side}
Sunucular sorar. İstemciler `Client(...)`'a bir **`elicitation_callback`** geçirerek yanıtlar:
```python title="client.py" hl_lines="6-7 18"
--8<-- "docs_src/elicitation/tutorial003.py"
```
* Tek bir callback iki modu da ele alır. `params`, `ElicitRequestFormParams` ile `ElicitRequestURLParams`'ın bir birleşimidir; dallanma `isinstance` ile yapılır.
* URL için `params.url`'yi kullanıcıya gösterir ve seçtiği eylemi döndürürsünüz. Asla `content` yok.
* Form için gerçek bir uygulama `params.requested_schema`'yı çizer ve kullanıcının girdisini `content` olarak döndürür. Buradaki ise hazır bir yanıtla her zaman evet der; bir testte tam da istediğiniz callback budur.
* Callback'i geçirmek aynı zamanda **yetenek bildirimidir**: sunucu bu istemciye soru sorulabileceğini böyle öğrenir. Bir istemcinin sunucu adına yanıtlayabileceği diğer şeyler **[İstemci callback'leri](../client/callbacks.md)** sayfasında.
!!! info
Elicitation *sunucudan* *istemciye* giden bir istektir ve bunlar yalnızca klasik
el sıkışmalı bir oturumda vardır; bu istemcinin `mode="legacy"` geçirmesinin nedeni budur.
**2026-07-28** bağlantısında bir araç bunun yerine soruyu çağrıdan *döndürerek* sorar;
o akış **[Çok turlu istekler](multi-round-trip.md)** sayfasında.
### Deneyin {#try-it}
Form modundaki `ctx.elicit` kullanan `server.py` dosyasını (`book_table` olanı) Streamable HTTP üzerinde başlatın (tek satırlık komut **[Sunucunuzu çalıştırma](../run/index.md)** sayfasında), ardından istemcinin `main()` fonksiyonunu çalıştırın ve `book_table`'dan Noel günü için rezervasyon isteyin.
Callback kendisine gönderilen soruyu yazdırır:
```text
No tables for 2 on 2025-12-25. Would you like to try another date?
```
`{"accept_alternative": True, "date": "2025-12-27"}` ile yanıt verir ve bunca zamandır `await ctx.elicit(...)` içinde bekleyen araç rezervasyonu tamamlar:
```text
Booked a table for 2 on 2025-12-27.
```
Şimdi URL modundaki `server.py` dosyasına geçin ve aynı `main()`'i `pay_deposit`'e yöneltin: aynı callback diğer dala girer, ödeme bağlantısını yazdırır ve araç *"Complete the payment in your browser."* ile geri döner. Çağrının ortasında, iki yönde de tek bir tur.
!!! check
Şimdi `Client`'tan `elicitation_callback=` parametresini kaldırın ve `book_table`'ı Noel günü
için yeniden çağırın. Çağrının tamamı bir protokol hatasıyla başarısız olur:
```text
Elicitation not supported
```
Hiç callback kaydetmemiş bir istemci `elicitation` yeteneğini hiç bildirmemiştir, dolayısıyla
soracak kimse yoktur. Aracınız `"decline"` almadı; bir istisna aldı. Buna göre tasarlayın:
her elicitation'ın "ya soramazsam?" sorusuna mantıklı bir yanıtı olmalı.
## Özet {#recap}
* `Annotated[T, Resolve(fn)]` ile işaretlenmiş bir parametreyi bir çözümleyici doldurur; çözümleyici sorması gerektiğinde `Elicit(...)` döndürür. Her bağlantıda çalışır.
* Şema düz bir Pydantic modelidir: yalnızca ilkel alanlar, dönüşte doğrulanır.
* `result.action`; `"accept"`, `"decline"` veya `"cancel"` olur; `result.data` yalnızca kabulde vardır.
* `await ctx.elicit(message, schema=Model)` araç gövdesinin içinden sorar; `await ctx.elicit_url(message, url, elicitation_id)` ise modelden geçmemesi gereken her şey içindir (`ctx.session.send_elicit_complete(elicitation_id)` bant dışı kısmın bittiğini söyler). İkisi de sunucudan istemciye giden isteklerdir: istemcinin eski nesil bir bağlantıda olmasını gerektirirler.
* İstemci, params türüne göre dallanan tek bir `elicitation_callback` ile yanıtlar; yeteneği bildiren şey onu kaydetmektir.
* 2026-07-28 bağlantısında sunucu soruyu itmek yerine döndürür; aynı callback'i **[Çok turlu istekler](multi-round-trip.md)** besler.
O dönüşün altında yatan her şey (yeniden deneme döngüsü, `requestState`'i koruma, akışı kendiniz yürütme) **[Çok turlu istekler](multi-round-trip.md)** sayfasında.

View file

@ -0,0 +1,37 @@
---
translation:
sections: [424930166c4bc6f3]
tool: 1
---
# İşleyicinin içinde {#inside-your-handler}
Bir işleyicinin argümanları istemciden gelir. Okuyabildiği *diğer* her şey ve
çalışırken yapabildiği her şey burada.
Okuyabildikleri:
* **[Context nesnesi](context.md)**, herhangi bir işleyicinin isteyebileceği
tek ek parametredir: canlı istek, başlıkları, oturumu, ayrıca ilerleme ve
değişiklik bildirimi eylemleri.
* **[Bağımlılıklar](dependencies.md)**, modelin hiç görmediği
parametrelerdir; değerlerini `Resolve` ile kendi fonksiyonlarınız doldurur.
* **[Lifespan](lifespan.md)** (yaşam döngüsü), sunucunun başlangıçta bir kez
oluşturduğu durumu ve bir işleyicinin bu duruma `Context` üzerinden nasıl
ulaştığını ele alır.
Çalışırken yapabildikleri:
* **[Elicitation](elicitation.md)** (kullanıcıdan bilgi isteme) ve onu taşıyan
2026-07-28 deseni olan **[Çok turlu istekler](multi-round-trip.md)**
(multi-round-trip) ile kullanıcıdan ek girdi istemek.
* Kullanım dışı bırakılmış ama hâlâ sunulan
**[Örnekleme (sampling) ve kök dizinler (roots)](sampling-and-roots.md)**
ile istemciden bir LLM tamamlaması ya da çalışma alanı klasörlerini istemek.
* Yavaş bir işte **[İlerleme](progress.md)** bildirmek.
* **[Log tutma](logging.md)** ile log yazmak (sunucuyu kim işletiyorsa onun
için, standart hataya).
* **[Abonelikler](subscriptions.md)** ile abone olmuş istemcilere bir şeyin
değiştiğini bildirmek.
Henüz bir işleyici kaydetmediyseniz **[Araçlar](../servers/tools.md)**
sayfasıyla başlayın. Buradaki her sayfa bir işleyiciniz olduğunu varsayar.

View file

@ -0,0 +1,107 @@
---
translation:
sections: [f3ca8ac5f90f2dfa, 85a1ef3588ba0736, 563346d4d5804933, 9e3528340d0bab53]
tool: 1
---
# Lifespan {#lifespan}
Gerçek sunucuların çoğu, ömürleri boyunca bir şeyi elde tutar: bir veritabanı havuzu, bir HTTP istemcisi, yüklenmiş bir model.
Bunu her çağrıda yeniden kurmak istemezsiniz, ama düzgünce kapatmak istersiniz. İşte **lifespan** (yaşam döngüsü) bunun için var.
## Türü belirli bir lifespan {#a-typed-lifespan}
Lifespan, sunucuyu alan ve **tek bir nesne** `yield` eden bir `@asynccontextmanager`'dır. Yield ettiğiniz şey, sunucu çalıştığı sürece her işleyicinin erişimindedir.
```python title="server.py" hl_lines="25-31 34 38 40"
--8<-- "docs_src/lifespan/tutorial001.py"
```
Aşağıdan yukarıya okuyun:
* `app_lifespan`, `Database`'i `yield`'den **önce** bağlar, **sonra** da bir `finally` içinde bağlantısını keser. İşte başlatma ve kapatma.
* Bir `AppContext` yield eder: kurduğunuz şeyleri tutan düz bir dataclass. Bugün bir alan, yarın on.
* Bağlamanın tamamı `MCPServer("Bookshop", lifespan=app_lifespan)` satırından ibaret.
* Aracın içinde, yield edilen nesne `ctx.request_context.lifespan_context`'tir.
Lifespan **bir kez** çalışır. Sunucu başladığında (ilk istekten önce) içine girilir, sunucu durduğunda içinden çıkılır. Aradaki her istek aynı `AppContext`'i paylaşır.
!!! info
Daha önce bir FastAPI `lifespan`'i yazdıysanız bunu zaten biliyorsunuz. Aynı dekoratör, aynı `yield`, aynı `finally`.
### Modelin gördüğü {#what-the-model-sees}
Yeni bir şey yok. `ctx` bir **Context** parametresidir; bu yüzden SDK onu enjekte eder ve girdi şemasına hiç ulaşmaz:
```json
{
"type": "object",
"properties": {
"genre": {"title": "Genre", "type": "string"}
},
"required": ["genre"],
"title": "count_booksArguments"
}
```
Modelin geçirebileceği tek argüman `genre`. Lifespan sunucunuzun kendi işidir.
`@mcp.resource()` ve `@mcp.prompt()` fonksiyonları da `ctx` parametresi alabilir; bir sonraki bölümün açıklayacağı bir nedenle bu parametre yalın `Context` olarak yazılır. `ctx`'in taşıdığı her şey **[Context nesnesi](context.md)** sayfasında.
### Gerçekten türü belirli {#it-really-is-typed}
Tür açıklamasına bir daha bakın: `ctx: Context[AppContext]`.
Tür denetleyiciniz için `ctx.request_context.lifespan_context`'in bir `AppContext` **olmasını** sağlayan işte bu tek tür parametresidir. `.db` otomatik tamamlanır; `.dbb` ise daha sunucuyu çalıştırmadan hata verir.
Bunun yerine yalın `Context` yazarsanız `lifespan_context`'in türü `dict[str, Any]` olur: tür denetleyicisinin, lifespan'inizin ne yield ettiğini bilmesinin yolu yoktur. Nesne çalışma zamanında yine oradadır; yalnızca yardımı kaybedersiniz.
!!! warning
`Context[AppContext]` **yalnızca araçlara özgü** bir yazımdır. Bunu bir `@mcp.resource()` ya da
`@mcp.prompt()` fonksiyonuna koyarsanız o işleyiciye yapılan her çağrı başarısız olur. İstemciye bir hata döner,
sunucu log'u da nedenini gösterir:
```text
Context is not available outside of a request
```
Kaynaklarda ve prompt'larda yalın `ctx: Context` yazın. Lifespan'inizin yield ettiği nesne
çalışma zamanında yine `ctx.request_context.lifespan_context`'tir; vazgeçtiğiniz şey nesne değil,
tür parametresidir.
!!! tip
Her zaman bir lifespan vardır. Siz bir tane geçirmezseniz SDK'nın varsayılanı boş bir `dict` yield eder;
dolayısıyla `ctx.request_context.lifespan_context` `{}` olur, asla `None` değil. Yalın `Context`'in
onu `dict[str, Any]` olarak türlendirmesinin nedeni de bu varsayılandır.
## İşleyişi gözlemleme {#watch-it-happen}
"Başlatma ilk istekten önce çalışır" cümlesi, körü körüne inanmak zorunda kalmamanız gereken türden bir cümle.
Sunucuyu yaşam döngüsüne kadar sadeleştirin: `Database`'e bir `connected` bayrağı verin, `connect()` ve `disconnect()` içinde değiştirin ve onu bildiren bir araç ekleyin.
```python title="server.py" hl_lines="11 14 17 25 44"
--8<-- "docs_src/lifespan/tutorial002.py"
```
`database`'in modül düzeyinde durmasının tek bir nedeni var: ona sunucunun *dışından* bakabilmeniz.
!!! check
Üç an, üç değer:
* Sunucu başlamadan önce `database.connected` `False`'tur. Modülü içe aktarmak hiçbir şeyi bağlamadı.
* Çalışırken `database_status` aracını çağırın; sonuç `"connected"` olur.
* Sunucuyu durdurun, `finally` bloğu çalışır: `database.connected` yeniden `False` olur.
İş tam olarak koyduğunuz yerde yapıldı: `yield`'in etrafında; ne içe aktarma sırasında ne de istek başına.
## Özet {#recap}
* `lifespan=`, sunucuyu alan ve tek bir nesne `yield` eden bir `@asynccontextmanager` alır.
* `yield`'den önceki kod başlatmadır. Sonrasındaki `finally` kapatmadır.
* İstek başına değil, sunucunun tüm ömrü boyunca bir kez çalışır.
* `yield` ettiğiniz şey her araçta, kaynakta ve prompt'ta `ctx.request_context.lifespan_context` olur.
* `ctx: Context[AppContext]` bu erişimi araçlarda tam tür bilgisiyle donatır. Kaynaklar ve prompt'lar yalın `Context` alır.
* `lifespan=` yoksa boş bir `dict` gelir, asla `None` değil.
Çağrının ortasında durup kullanıcıya yalnızca onun bildiği bir şeyi soran işleyici, **[Elicitation](elicitation.md)** (kullanıcıdan bilgi isteme) sayfasının konusu.

View file

@ -0,0 +1,87 @@
---
translation:
sections: [c93a3e1aefd77955, 7851abd5ec54393b, f49d1ca2f330f9cd, c03764bd9dfeef7b, 4a0391691a674ae4, 2df5cd279eabf9f5]
tool: 1
---
# Log tutma {#logging}
Bir araçtan log yazmak, başka herhangi bir Python fonksiyonundan log yazmaktan farksızdır: standart kütüphaneyle.
MCP'de protokol düzeyinde bir **logging yeteneği** vardır: bir sunucu, `Context` nesnesindeki metotlar aracılığıyla log mesajlarını istemciye bildirim olarak gönderebilir. Spesifikasyonun 2026-07-28 sürümü **bu yeteneği kullanım dışı bırakır ve yerine bir şey koymaz**; bu yüzden bu belgeler onu öğretmez. Nelerin kullanım dışı bırakıldığının ve bunların yerine ne yapılacağının tam listesi **[Kullanım dışı özellikler](../deprecated.md)** sayfasında.
Bunun yerine yapacağınız şey, diğer her Python programında yaptığınızdır: standart kütüphane.
## Log yazan bir araç {#a-tool-that-logs}
```python title="server.py" hl_lines="1 5 13"
--8<-- "docs_src/logging/tutorial001.py"
```
* `logging.getLogger(__name__)` size modülünüzün adını taşıyan bir logger verir. Onu bir kez, en üstte oluşturun.
* Aracın içinde, başka herhangi bir fonksiyonda olduğu gibi `logger.info(...)`'yu çağırırsınız. Enjekte edilecek bir şey yok, `await` edilecek bir şey yok, MCP'ye özgü bir şey yok.
!!! check
Aracı çağırın ve sonucun tamamına bakın:
```python
result.content # [TextContent(text="Found 3 books matching 'dune'.")]
result.structured_content # {'result': "Found 3 books matching 'dune'."}
```
Log satırı bunun hiçbir yerinde yok. Log tutma **sizin** içindir; sunucuyu işleten kişi için. Model
onu asla görmez. Modelin okuması gereken bir şey varsa onu `return` edin.
## Nereye gider {#where-it-goes}
Bir **stdio** sunucusu için bu soru her zamankinden daha önemlidir. Host, sunucunuzu bir alt süreç olarak başlattı ve MCP mesajlarını sunucunun **stdout** akışından okuyor. Standart hata sizindir.
Standart kütüphane zaten doğru olanı yapar: log çıktısı varsayılan olarak `sys.stderr`'e gider. `logger.info(...)` satırlarınız terminale (ya da host alt sürecin stderr'ini nereye topluyorsa oraya) düşer ve protokol akışı temiz kalır.
!!! tip
Bir stdio sunucusunda `print()` kullanmayın. `print`, **stdout**'a yazar ve stdout protokole aittir.
SDK, hizmet verirken gerçekten *flush edilen* stdout çıktısını stderr'e yönlendirir; bu yüzden
iletilen veriyi bozamaz. Ancak blok tamponlamalı bir süreçte `print()` çıktısı genellikle flush
edilmeden `sys.stdout`'un tamponunda bekler; yorumlayıcı çıkışta tamponu boşaltınca da doğrudan
protokol akışına dökülür. Yönlendirildiğinde bile satır, log çıktısının arasına ham hâlde düşer:
düzeyi yoktur, logger adı yoktur, onu filtrelemenin bir yolu yoktur.
`logger.debug("got here")` de aynı tek satırlık çabadır ve doğru yere gider.
## Düzey {#the-level}
`logging.basicConfig()`'i kendiniz çağırmanız gerekmez. Bir `MCPServer` oluşturmak bunu zaten yaptı: standart hataya yönlendirilmiş bir işleyiciyle, `log_level=` olarak geçirdiğiniz düzeyde. Yani `logger.debug(...)` satırlarınızı görmek için `MCPServer("Bookshop", log_level="DEBUG")` yeterlidir.
Varsayılan değer `"INFO"`.
`logging.basicConfig()` hâlihazırda var olan işleyicileri asla değiştirmez. Sunucuyu oluşturmadan önce log yapılandırmasını kendiniz yaparsanız sizin yapılandırmanız geçerli olur.
## Deneyin {#try-it}
Sunucuyu MCP Inspector ile çalıştırın:
```console
uv run mcp dev server.py
```
**Tools** sekmesinden `search_books`'u çağırın. Inspector size sonucu gösterir: yalnızca dönüş değeri. Şu satır
```text
Searching for 'dune'
```
standart hataya gitti: terminale, iletilen veriye değil.
!!! info
Asıl istediğiniz *izleme* (tracing) ise (her istek, ne kadar sürdüğü, başarısız olup olmadığı),
log satırları değil span'ler istersiniz. Sunucunuz bunları zaten üretir: SDK varsayılan olarak her
mesajı OpenTelemetry ile izler. **[OpenTelemetry](../run/opentelemetry.md)** sayfasına bakın.
## Özet {#recap}
* MCP protokolünün logging yeteneği 2026-07-28 spesifikasyonuyla kullanım dışı bırakıldı ve yerine bir şey konmadı. Üzerine bir şey inşa etmeyin.
* Modül düzeyinde `logger = logging.getLogger(__name__)`, aracın içinde `logger.info(...)`. Kalıbın tamamı bu.
* Log çıktısı modele asla ulaşmaz. Yalnızca `return` ettiğiniz değer ulaşır.
* Standart hata sizindir; stdout protokole aittir. SDK hizmet verirken flush edilmiş başıboş stdout çıktısını stderr'e yönlendirir, ancak flush edilmemiş bir `print()` yine de çıkışta iletilen veriye dökülebilir ve yönlendirilen satırlar etiketsiz gelir; her kaydı flush eden bir işleyicisi olan `logging`'i kullanın.
* `MCPServer(..., log_level="DEBUG")` düzeyi ayarlar; önceden yaptığınız bir log yapılandırmasına ise dokunulmaz.
Bağlı istemcilere sunucunuzda bir şeyin (araç listesi, bir kaynak) değiştiğini bildirmek **[Abonelikler](subscriptions.md)** sayfasının konusu.

View file

@ -0,0 +1,193 @@
---
translation:
sections: [74011e683045eea9, 9b64cc175c18b6a9, 4b41be4824030397, e3b1502da786ec33, 71e41161f143c6a9, 9ec2c1eeb8c36378, 8dd027377d46448b, f81491125dcbfe8b]
tool: 1
---
# Çok turlu istekler {#multi-round-trip-requests}
Bazen bir araç işini tek turda bitiremez. Yalnızca kullanıcıda olan bir şeye ihtiyaç duyar: bir seçim, bir onay, bir kimlik bilgisi.
2026-07-28 öncesinde sunucu bunu **geri** çağırarak elde ederdi: asıl isteği işlemenin tam ortasında istemciye kendi isteğini, örneğin bir elicitation (kullanıcıdan bilgi isteme) ya da bir örnekleme (sampling) çağrısını açardı. 2026-07-28 spesifikasyonu bu geri kanalı (back-channel) emekliye ayırıyor.
Bunun yerine sunucu **döndürür**.
## Geri çağırmak yerine döndürme {#return-dont-call-back}
Sunucu `tools/call` isteğini `CallToolResult` yerine bir **`InputRequiredResult`** ile yanıtlar. İşi iki alanı yapar:
* **`input_requests`**: sunucunun hâlâ ihtiyaç duyduğu şeyler; anahtarları sunucunun seçtiği adlar olan bir dict. Her değer bir `ElicitRequest`, bir `CreateMessageRequest` ya da bir `ListRootsRequest` olur.
* **`request_state`**: opak bir token. İstemci yeniden denemede onu olduğu gibi geri yollar. Onu okuyan tek şey sunucunuzdur.
İstemci her isteği karşılar, ardından yanıtlarını `input_responses` içinde, token'ı da `request_state` içinde taşıyarak **aynı aracı yeniden** çağırır. Sunucunun eksiği artık tamamdır ve normal bir `CallToolResult` döndürür.
Protokolün tamamı bu. Her adım istemciden sunucuya giden sıradan bir istektir. Hiçbir şey ters yönde akmaz.
## Sunucu tarafı {#the-server-side}
`@mcp.tool()` üzerinde bunu elle kurmanız nadiren gerekir: kullanıcıya soran (`Elicit`), istemcinin LLM'inden örnekleme yapan (`Sample`) veya kök dizinlerini (roots) listeleyen (`ListRoots`) bir bağımlılık bildirin; SDK `InputRequiredResult`'ı sizin yerinize döndürür. Bu biçim **[Bağımlılıklar](dependencies.md)** sayfasının konusudur. İki biçim bir arada kullanılamaz: bir çağrının tek bir `input_responses`/`request_state` kanalı vardır, bu yüzden `Resolve(...)` parametreleri kullanan bir araç gövdesinden ayrıca `InputRequiredResult` döndüremez. Bildirilmiş bir `InputRequiredResult` dönüş türü kayıt sırasında reddedilir (`InvalidSignature`); bildirilmemiş olanı ise çağrıyı çalışma zamanında başarısız kılar. Elle kurulan biçim **düşük seviyeli** `Server`'dır; onun `on_call_tool` işleyicisi iki sonuç türünden herhangi birini döndürebilir:
```python title="server.py" hl_lines="43-46"
--8<-- "docs_src/mrtr/tutorial001.py"
```
* `on_call_tool`'un tür ipucu `-> CallToolResult | InputRequiredResult` şeklindedir. İkincisini döndürmek sunucu tarafı API'sinin tamamıdır.
* İlk çağrıda `params.input_responses` değeri `None`'dır; bu yüzden koruma koşulu devreye girer ve işleyici yanıtlamak yerine sorar.
* Yeniden denemede, istemcinin gönderdiği `ElicitResult`, sunucunun `input_requests` içinde kullandığı **aynı anahtarın** (`"region"`) altında durur.
O dosyadaki geri kalan her şey (açık `input_schema`, elle kurulan `CallToolResult`) sıradan düşük seviyeli `Server`'dır ve **[Düşük seviyeli Server](../advanced/low-level-server.md)** sayfasında anlatılır. Bu sayfa yalnızca ikinci dönüş türünü ekler.
## Araçların ötesi {#beyond-tools}
`tools/call` özel değildir: 2026-07-28 sürümünde bir sunucu `prompts/get` ve `resources/read` isteklerini de aynı şekilde yanıtlayabilir. `MCPServer` üzerinde bir `@mcp.prompt()` fonksiyonu (ya da bir `@mcp.resource()` **şablon** fonksiyonu) `InputRequiredResult`'ı kendisi döndürür ve yeniden denemenin yanıtlarını bağlamdan okur:
```python title="server.py" hl_lines="20 22 24"
--8<-- "docs_src/mrtr/tutorial004.py"
```
* İlk tur `InputRequiredResult`'ı döndürür. Yeniden denemede `ctx.input_responses` yanıtları aynı anahtarlar altında tutar ve fonksiyon olağan sonucunu döndürür: burada prompt mesajları, bir şablon kaynak için kaynak içeriği.
* Ayarladığınız bir `request_state`, sunucudaki diğer her şey gibi, ağa çıkmadan önce mühürlenir ve geri geldiğinde doğrulanır; aşağıdaki **[`requestState`'i koruma](#protecting-requeststate)** bölümü mührün size ne sağladığını ve anahtarları ne zaman yapılandırmanız gerektiğini anlatır.
* Bağımlılık biçimi uymadığında bir `@mcp.tool()` fonksiyonu da sonucu aynı şekilde doğrudan döndürebilir.
* Statik `@mcp.resource()` fonksiyonları buna katılmaz: `Context` almazlar, dolayısıyla yeniden denemeyi hiçbir zaman okuyamazlar. Yalnızca şablon kaynaklar soru sorabilir.
* Aşağıdaki nesil kuralları aynen geçerlidir: 2026 öncesi bir oturumda `InputRequiredResult` döndürmek, uyarının anlattığı aynı `-32603` hatasıdır.
## İstemci tarafı {#the-client-side}
Döngüyü sizin yerinize `Client` çalıştırır.
Sunucunun isteyebileceği callback'leri (`elicitation_callback`, `sampling_callback`, `list_roots_callback`) kaydedin ve aracı çağırın. Bir `InputRequiredResult` geldiğinde `Client`, `input_requests` içindeki her girdiyi eşleşen callback'e yönlendirir, yanıtlar ve geri yollanan `request_state` ile yeniden dener ve bir `CallToolResult` dönene kadar devam eder:
```python title="client.py" hl_lines="11 12"
--8<-- "docs_src/mrtr/tutorial003.py"
```
* Bu `elicitation_callback`, 2026 öncesi bir sunucunun geri kanal üzerinden gönderdiği `elicitation/create` isteğinin ulaşacağı callback'in aynısıdır. `sampling/createMessage` ile `sampling_callback`, `roots/list` ile `list_roots_callback` için de aynısı geçerlidir: 2026-07-28 sürümünde bağımsız sunucu->istemci RPC'leri artık yoktur, ancak birebir aynı `ElicitRequest` / `CreateMessageRequest` / `ListRootsRequest` yükleri `input_requests` içinde taşınır ve aynı üç callback'e yönlendirilir. Tek bir callback seti her iki nesle de hizmet verir.
* `call_tool` düz bir `CallToolResult` döndürür. Aradaki turlar çağırana görünmez.
* `get_prompt` ve `read_resource` aynı döngüyü yürütür.
!!! check
Callback'i kaydetmezseniz döngü ilk turda başarısız olur: SDK'nın yedek callback'i her
elicitation'ı bir hatayla yanıtlar ve `call_tool`, *"Elicitation not supported"* mesajıyla
`MCPError` fırlatır.
Döngü sınırlıdır. Varsayılan üst sınır `Client(..., input_required_max_rounds=10)` değeridir; bunu aştıktan sonra hâlâ `InputRequiredResult` döndüren bir sunucu `call_tool`'un hata fırlatmasına yol açar. Bir tur yalnızca `request_state` taşıyıp hiç `input_requests` taşımıyorsa `Client` yeniden denemeden önce kısa bir süre bekler (50 ms'den başlayıp iki katına çıkarak 250 ms tavanına ulaşır); böylece yalnızca *"henüz bitmedi"* diyen bir sunucu sürekli yoklanmaz.
### Döngüyü kendiniz yürütme {#driving-the-loop-yourself}
Otomatik döngü tek süreçli bir istemci için yeterlidir. Şu durumlarda döngüyü kendiniz üstlenin:
* İstemciniz **dağıtık** yapıdaysa: soruyu kullanıcıya gösteren süreç `call_tool`'u çağıran süreç değildir, bu yüzden yeniden denemeyi başka bir worker gönderir. `request_state`, kendi depolamanız üzerinden bu sınırın ötesine taşıdığınız kalıcı saklanabilir token'dır; `input_responses` ise karşı tarafın onunla birlikte geri gönderdiği şeydir.
* Her turu **incelemek** istiyorsanız: her `input_requests` girdisini loglamak ya da denetlemek, belirli istek türlerini reddetmek veya adımlar arasında kendi bekleme (backoff) stratejinizi uygulamak.
* Tur sayısına değil **gerçek süreye** dayalı bir sınır istiyorsanız: `input_required_max_rounds`'a güvenmek yerine kendi döngünüzü `anyio.fail_after(...)` içine sarın.
Alttaki oturuma inin; orada `allow_input_required=True` size birleşim türünü doğrudan verir:
```python title="client.py" hl_lines="12 13 19"
--8<-- "docs_src/mrtr/tutorial002.py"
```
* `client.session.call_tool(..., allow_input_required=True)` dönüş türünü `CallToolResult | InputRequiredResult` olarak genişletir. Onu yeniden daraltan `isinstance` denetimidir.
* `request_state` artık sizin elinizde. Adımlar arasında onu bir yere yazın; konuşma yepyeni bir süreçten kaldığı yerden sürebilir.
* `input_requests` içindeki her girdi için `input_responses` içine **aynı anahtarla** bir `InputResponse` koyarsınız. Kullanıcı arayüzünüzün yeri `fulfil`'dir; buradaki, yanıtı sabit kodlar.
* Her adımda aynı araç adı, aynı `arguments`. Yeniden deneme yeni bir yöntem değil, asıl çağrının yeniden yapılmasıdır.
## `requestState`'i koruma {#protecting-requeststate}
Yukarıdaki her şey `request_state`'i bir yankı olarak ele alır; ağ üzerinde de bundan ibarettir. Ancak istemci onu adımlar arasında elinde tutar (süreçler arasında bir yere yazmak tam da önceki bölümün onayladığı şeydir), dolayısıyla geri gelen şey **istemcinin sağladığı girdidir**: değiştirilmiş, süresi dolmuş ya da bambaşka bir çağrıdan alınmış olabilir. Spesifikasyon, durumun yetkilendirmeyi, kaynak erişimini veya iş mantığını etkileyebildiği her yerde sunucuların bu durumun bütünlüğünü korumasını ve doğrulama başarısız olduğunda turu reddetmesini zorunlu kılar.
`MCPServer` onu varsayılan olarak korur. Her sunucu, süreç başlarken üretilen bir anahtar altında giden `requestState`'i mühürler ve gelen her yankıyı (çözümleyici durumunu da elle kurulan durumu da) doğrular. Hiçbir şey yapılandırmaz, düz metin yazar ve düz metin okursunuz; ağ üzerinde yalnızca opak, şifreli bir token taşınır.
Varsayılan anahtar süreçle birlikte doğar ve ölür; tek bir sürecin ötesine dağıtım yapmadan önce bilmeniz gereken tek şey budur:
```python
from mcp.server.mcpserver import MCPServer, RequestStateSecurity
# Multi-instance or restart-surviving: one or more shared secret keys (>= 32 bytes each).
mcp = MCPServer("fleet", request_state_security=RequestStateSecurity(keys=[key]))
```
* **Varsayılan (yapılandırma yok)** tek bir sürece uygundur: stdio ya da tam olarak bir HTTP worker'ı. Başka bir worker'a, yük dengeleyici arkasındaki başka bir örneğe ya da yeniden başlatma sonrası aynı sunucuya düşen bir yeniden deneme, o sürecin elinde olmayan bir anahtarla mühürlenmiştir; istemci aşağıdaki sabit ret yanıtını alır ve akışa baştan başlamak zorundadır.
* **`keys=[...]`**, bir yeniden denemenin **başka bir örneğe** ulaşabildiği (çok worker'lı `uvicorn`, yük dengelemeli HTTP) ya da yeniden başlatmalardan sağ çıkması gerektiği her durumda zorunludur: her örnek, herhangi bir kardeşinin ürettiğini doğrular. Aynı mekanizma; üretilmiş bir anahtar yerine sizin gizli anahtarınız.
* Kendi kriptografiniz için (örneğin bir KMS ya da mevcut bir token servisi) `keys` yerine `RequestStateSecurity(codec=...)` geçirin; aşağıdaki **[Kendi kriptografinizi getirme](#bring-your-own-crypto)** bölümü sözleşmeyi anlatır.
### Mührün taşıdıkları {#what-the-seal-carries}
Varsayılan da olsa yapılandırılmış da olsa, ağ üzerindeki `requestState` şifreli ve kimliği doğrulanmış bir token'dır. Kodunuz onu hiç görmez: işleyiciler ve çözümleyiciler düz metin yazar, düz metin okur (`ctx.request_state`); SDK çıkışta mühürler, girişte doğrular. Bütünlüğün ötesinde her token şunlara bağlanır:
* **Bir zaman penceresi.** Her tur yeni bir son kullanma süresiyle yeniden mühürler; bu yüzden `RequestStateSecurity(ttl=...)` (varsayılan 600 saniye) akışın tamamını değil, tur başına düşünme süresini sınırlar.
* **Kimliği doğrulanmış principal.** İstek, SDK'nın doğruladığı bir OAuth erişim token'ı taşıdığında durum, token'ın istemcisine, yayımcısına (issuer) ve öznesine (subject) bağlanır: bir kullanıcı için üretilmiş durum, iki kullanıcı aynı OAuth istemcisini paylaşsa bile başka bir kullanıcı altında başarısız olur. Özne sağlamayan bir doğrulayıcı, bağlamayı yalnızca istemci kimliğine indirger; URL tabanlı istemci kimliklerinde bu kimliği o istemci yazılımının tüm kullanıcıları paylaşır. Kimlik doğrulama SDK dışında sonlandırıldığında (öndeki bir vekil sunucu) ya da aktarımda kimlik doğrulama yoksa bağlanacak bir principal yoktur ve `RequestStateSecurity(bind_principal=...)` kendi kimlik sinyalinizden bir tane sağlamadıkça bu denetim etkisizdir. Token doğrulayıcınız hangi bileşenleri sağlıyorsa bunları tutarlı biçimde sağlamalıdır: bazı isteklerde özneyi ekleyip bazılarında atlayan bir doğrulayıcı, principal'ı akışın ortasında değiştirir ve süren turlar reddedilir.
* **Kaynaklandığı istek.** Yöntem, araç ya da prompt adı (veya kaynak URI'si) ve argümanların bir özeti (digest). Başka bir araca, başka argümanlara ya da başka bir yönteme karşı yeniden oynatılan bir token başarısız olur.
* **Sorulan sorunun ta kendisi.** Her çözümleyici yanıtı, hem ilk geldiği turda hem de kaydedilmiş bir yanıt sonradan yeniden kullanıldığında, istemciye gösterilen oluşturulmuş soruya sabitlenir. Mesajı yeniden yazılmış ya da şeması değişmiş bir sürümü dağıtırsanız sunucu bayat bir yanıtı tüketmek yerine yeniden sorar. Aynı sabitleme ters yönde de işler: mesajları çağrıya özgü verilerden değil, aracın argümanlarından türetin. Bir zaman damgasından ya da canlı bir kurdan kurulan mesaj her turda farklı oluşur; bu yüzden kaydedilmiş her yanıt bayat görünür ve sunucu, istemcinin tur sınırı çağrıyı sonlandırana kadar yeniden sorar.
Bunların hepsi SDK'nın işidir; sizin değil, kendinizinkini getirseniz bile codec'in de değil.
### Anahtar rotasyonu {#rotating-keys}
Yeni durumu `keys[0]` mühürler; listedeki her anahtar doğrular. Kesintisiz rotasyon, her biri bir sonrakinden önce tamamen yayılmış üç aşamadan oluşur:
```python
RequestStateSecurity(keys=[OLD, NEW]) # 1: every instance learns to verify NEW; OLD still mints
RequestStateSecurity(keys=[NEW, OLD]) # 2: NEW mints; in-flight OLD state keeps verifying
RequestStateSecurity(keys=[NEW]) # 3: one ttl after phase 2 is fully out, retire OLD
```
Asla önce üreten anahtarı terfi ettirmeyin: bazı örneklerin henüz doğrulayamadığı bir anahtarla üretmek, yayılımın ortasında süren turları düşürür.
Anahtarların kapsamı tek bir servistir. Mühürlü zarf ayrıca sunucunun adını bir audience claim'i olarak taşır; bu yüzden tesadüfen aynı gizli anahtarı paylaşan başka bir servisin ürettiği token zaten reddedilir. Claim ancak ad kadar ayırt edicidir; bu yüzden açık bir politika verilen sunucunun gerçek bir adı olmalı ya da `RequestStateSecurity(audience=...)` ayarlamalıdır: adsız bir sunucu oluşturulurken istisna fırlatır. `audience=` ayrıca bir servisin başka bir servisin ürettiği durumu kabul etmesi gereken, bilinçli kurulmuş çok servisli topolojilere de hizmet eder. (Yapılandırmasız varsayılan muaftır: anahtarı süreçten hiç çıkmaz, dolayısıyla audience claim'inin ekleyeceği bir şey yoktur.)
### Kendi kriptografinizi getirme {#bring-your-own-crypto}
`RequestStateSecurity(codec=...)`, `seal(bytes) -> str` ve `unseal(str) -> bytes` yöntemleri olan ve kendisinin üretmediği her token için `InvalidRequestState` fırlatan herhangi bir şeyi kabul eder. Klasik biçim, bir KMS üzerinden zarf şifrelemedir: başlangıçta bir veri anahtarını bir kez açar ve token başına kriptografiyi yerel tutarsınız:
```python title="server.py" hl_lines="12 26-27 34-35 38"
--8<-- "docs_src/mrtr/tutorial005.py"
```
TTL, principal bağlama ve istek bağlama codec'in işi **değildir**: SDK bunları her codec için `seal`'den önce yüke damgalar ve `unseal`'den sonra yeniden doğrular. Bir codec'in tek yükümlülüğü bütünlük (kurcalanmışsa istisna fırlatmak) ve ideal olarak gizliliktir.
### Doğrulama başarısız olduğunda {#when-verification-fails}
Gelen her başarısızlık (kurcalanmış, süresi dolmuş, başka bir isteğe ya da principal'a karşı yeniden oynatılmış veya bu sunucunun bilmediği bir anahtarla mühürlenmiş olsun) aynı yanıtı alır:
```json
{"code": -32602, "message": "Invalid or expired requestState"}
```
Her neden için tek bir sabit mesaj; böylece ağ üzerinden hangi denetimin başarısız olduğu asla açığa çıkmaz, gerçek neden sunucu log'una gider. `tools/call`, `prompts/get` ve `resources/read` üzerinden gelen her `requestState` denetlenir; hiç durum üretmeyen bir işleyiciye gelen de buna dahildir. Pratikte en sık görülen ret bir saldırgan değildir: varsayılan, sürece özel anahtarın bir yeniden başlatma öncesinden ya da başka bir örnekten gelen bir yeniden denemeyle karşılaşmasıdır. İstemci akışı yeniden başlatır; bu önemli olduğunda çözüm `keys=[...]` kullanmaktır.
### Elle kurulan durum {#hand-built-state}
Kendiniz ayarladığınız bir `request_state`'i (bir araç, prompt ya da kaynak şablonu fonksiyonundan `InputRequiredResult` döndürerek), çözümleyici durumunu işleyen aynı mekanizma tek satır kod değişmeden mühürler ve doğrular: düz metin yazın, düz metin okuyun; yukarıdaki her bağlama geçerlidir.
SDK'nın, yapılandırılmış olsa bile sizin yerinize sabitleyemeyeceği tek şey soru kimliğidir: durumunuzdaki bir yanıtın *sizin* sorularınızdan hangisine ait olduğunu bilmez. Yanıtları soruya göre anahtarlayarak saklıyorsanız duruma kendi soru tanımlayıcınızı ekleyin ve yeniden denemede onu denetleyin.
Düşük seviyeli `Server` hiçbir şeyin hazır gelmediği katmandır: `MCPServer`'ın aksine, sınırı kendiniz ekleyene kadar hiçbir şey mühürlenmez ve bunu yapana kadar `request_state` ağ üzerinden tam yazıldığı gibi geçer. Tek satırlık katılım **[Düşük seviyeli Server](../advanced/low-level-server.md#the-other-handlers)** sayfasında gösterilir.
## 2026-07-28 sürümüne özgü bir sonuç {#a-2026-07-28-result}
`InputRequiredResult` yalnızca **2026-07-28** protokol sürümünde vardır. Bellek içi `Client(server)` onu sizin yerinize anlaşarak belirler; ağ üzerinde `mode="auto"` keşfeder. Bağlandıktan sonra `client.protocol_version` size ne elde ettiğinizi söyler.
!!! warning
2026 öncesi bir oturumda `InputRequiredResult` koyacak bir yer yoktur. `mode="legacy"` bir
bağlantıda işleyicinizden bir tane döndürürseniz çalıştırıcı onu anlaşılan sürüme
serileştiremez; istemciye `-32603` *"Handler returned an invalid result"* hatası döner. Her iki
nesle de hizmet veren bir sunucu, ona el atmadan önce `ctx.protocol_version` değerini
denetlemelidir.
!!! info
**URL kipinde elicitation**, 2026 bağlantısında tam olarak bu mekanizmayı kullanır.
`input_requests` içindeki girdi, parametreleri `ElicitRequestURLParams` olan bir
`ElicitRequest`'tir; kullanıcı bant dışı akışı tamamlar ve istemciniz çağrıyı yeniden dener.
Aynı döngü, yeni API yok. Üst seviyeli sunucu tarafı **[Elicitation](elicitation.md)**
sayfasındadır.
## Özet {#recap}
* 2026-07-28 sürümünde, çağrının ortasında girdiye ihtiyaç duyan bir sunucu `InputRequiredResult` **döndürür**. İstemciye asla istek açmaz.
* `input_requests` ihtiyaç duyduklarıdır. `request_state` yalnızca sunucunun okuduğu opak bir devam token'ıdır.
* Yeniden deneme döngüsünü `Client` sizin yerinize çalıştırır: `elicitation_callback` / `sampling_callback` / `list_roots_callback` kaydedin, `call_tool` düz bir `CallToolResult` döndürür. `input_required_max_rounds` (varsayılan 10) onu sınırlar.
* Turları incelemek ya da kalıcı saklamak için `client.session.call_tool(..., allow_input_required=True)` kullanın ve `while isinstance(result, InputRequiredResult)` döngüsünü kendiniz üstlenin.
* `@mcp.tool()` üzerinde, kullanıcıya soran bir bağımlılık bu sonucu sizin yerinize üretir (**[Bağımlılıklar](dependencies.md)**); elle kurulan biçim **düşük seviyeli** `Server`'dır.
* Prompt'lar ve kaynaklar da katılır: bir `@mcp.prompt()` ya da şablon `@mcp.resource()` fonksiyonu `InputRequiredResult`'ı kendisi döndürür ve yeniden denemede `ctx.input_responses`'ı okur.
* `requestState` istemcinin sağladığı girdi olarak geri gelir; bu yüzden `MCPServer` onu (çözümleyici durumunu da elle kurulan durumu da) varsayılan olarak sürece özel bir anahtar altında mühürler. Çok örnekli dağıtımlar, her örneğin bir kardeşinin ürettiğini doğrulayabilmesi için `RequestStateSecurity(keys=[...])` (ya da özel bir codec) geçirir. Mühür her token'ı bir zaman penceresine, kaynaklandığı isteğe ve (istek SDK'nın doğruladığı kimlik doğrulama bilgisini taşıdığında ya da `bind_principal=` kendi kimlik sinyalinizi sağladığında) kimliği doğrulanmış principal'a bağlar (**[`requestState`'i koruma](#protecting-requeststate)**).
Sunucunun başlattığı örneklemenin ve itme tarzı geri kanalın geri kalanının yerini alan mekanizma budur; **[Kullanım dışı özellikler](../deprecated.md)** sayfasına bakın.

View file

@ -0,0 +1,122 @@
---
translation:
sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 0284b215e85366c4, 8534d8dbb4053a70, 2966fac6fe697007]
tool: 1
---
# İlerleme {#progress}
Otuz saniye süren ve otuz saniye boyunca hiçbir şey söylemeyen bir araç bozuk görünür.
**İlerleme bildirimleri** bunu çözer. Araç ne kadar ilerlediğini bildirir; bununla ne çizeceğine istemci karar verir: bir çubuk, dönen bir simge, bir log satırı.
## Araçtan bildirme {#report-it-from-the-tool}
Bir **`Context`** parametresi alın ve `report_progress`'i çağırın:
```python title="server.py" hl_lines="8 11"
--8<-- "docs_src/progress/tutorial001.py"
```
Üç argüman var ve ne anlama geldiklerine siz karar verirsiniz:
* `progress`: ne kadar ilerlediğiniz. Spesifikasyon bunun her bildirimde **artmasını** şart koşar; asla bir değeri tekrarlamayın veya geriye gitmeyin.
* `total`: biliyorsanız, toplamda ne kadar iş olduğu. İsteğe bağlı.
* `message`: *bu* adım hakkında insanların okuyabileceği tek bir satır. İsteğe bağlı.
`ctx` tür ipucu sayesinde enjekte edilir ve model onu asla görmez: `import_catalog`'un girdi şemasında tek bir özellik var, `urls`. **[Context nesnesi](context.md)** sayfası baştan sona bu nesneyi anlatır; ilerleme, onun size sunduklarından biridir.
## İstemciden dinleme {#listen-for-it-from-the-client}
İstemci, `call_tool`'a `progress_callback=` geçirerek **çağrı başına** dahil olur:
```python title="client.py" hl_lines="7 16"
import anyio
from mcp import Client
from server import mcp
async def show(progress: float, total: float | None, message: str | None) -> None:
print(f"{message} ({progress}/{total})")
async def main() -> None:
async with Client(mcp) as client:
result = await client.call_tool(
"import_catalog",
{"urls": ["https://example.com/a.json", "https://example.com/b.json"]},
progress_callback=show,
)
print(result.structured_content)
anyio.run(main)
```
Callback, sunucunun bildirdiklerini olduğu gibi alan `async` bir fonksiyondur: `progress`, `total`, `message`.
!!! info
`Client(mcp)` doğrudan sunucu nesnesine, bellek içinde bağlanır; **[Test etme](../get-started/testing.md)**
sayfasının üzerine kurulduğu istemcinin aynısıdır. `Client` hangi aktarımı kullanırsa kullansın
`progress_callback` aynı parametredir; birazdan göreceğiniz *zamanlama* ise bellek içi bağlantıya
özgüdür. Bu bağlantı callback'inizi satır içinde çalıştırır, bu yüzden her bildirim `call_tool`
dönmeden önce ulaşır. Gerçek bir aktarım üzerinde bildirimler sonuçla yarışır ve yavaş bir callback,
`call_tool` döndükten sonra hâlâ çalışıyor olabilir.
### Deneyin {#try-it}
`client.py` dosyasını `server.py` dosyasının yanına koyun ve çalıştırın:
```console
python client.py
```
```text
Imported https://example.com/a.json (1/2)
Imported https://example.com/b.json (2/2)
{'result': 'Imported 2 records.'}
```
Sunucudaki her `await ctx.report_progress(...)`, istemcide sırasıyla bir `show` çağrısına dönüştü ve her iki satır da `call_tool` dönmeden **önce** yazdırıldı. İlerleme sonucun içine paketlenmez; araç hâlâ çalışırken akar.
!!! warning
`progress_callback` `Client`'a değil, **çağrıya** aittir. Bunun için bir kurucu argümanı yoktur,
çünkü farklı çağrılar farklı callback'ler ister: biri bir indirme çubuğunu sürer, sonraki bir
log satırını.
!!! check
Şimdi `progress_callback=show` kısmını silin ve yeniden çalıştırın:
```text
{'result': 'Imported 2 records.'}
```
Hata yok, uyarı yok, sonuç aynı. `report_progress`, **çağıran taraf ilerleme istemediğinde hiçbir
şey yapmaz**; bu yüzden koşulsuz bildirirsiniz ve birinin dinleyip dinlemediğini asla merak etmeniz
gerekmez.
## Toplamı bilmediğinizde {#when-you-dont-know-the-total}
`total`, paydayı bildiğiniz durumlar içindir. Çoğu zaman bilmezsiniz: bir akışı boşaltıyor, bir imleç üzerinde ilerliyor ya da uzunluk başlığı olmayan bir şey indiriyorsunuzdur.
Belirtmeyin:
```python title="server.py" hl_lines="20"
--8<-- "docs_src/progress/tutorial002.py"
```
Callback `total=None` alır. İstemci yine de *etkinlik* gösterebilir ("şimdiye kadar 3 tane içe aktarıldı...") ama yüzde gösteremez. Daha güzel bir çubuk için toplam uydurmayın.
!!! tip
`progress`'in belirli bir şeyi sayması gerekmez. Bayt, satır, sayfa: kullanıcının tanıyacağı
birimi seçin ve yalnızca tutabileceğiniz bir `total` sözü verin.
## Özet {#recap}
* `Context` alan herhangi bir araçtan `await ctx.report_progress(progress, total=None, message=None)`.
* İstemci `call_tool`'a `progress_callback=` geçirir: çağrı başına, asla `Client` üzerinde değil.
* Callback `async (progress, total, message) -> None` biçimindedir ve araç hâlâ çalışırken tetiklenir.
* Çağrıda callback yoksa `report_progress` hiçbir şey yapmaz. Koşulsuz bildirin.
* Bilmediğinizde `total`'ı vermeyin; callback `None` alır.
İlerleme, çalışan bir aracın *kullanıcıya* gösterdiği şeydir. *Sizin* için, yani sunucuyu işleten kişi için yazdığı log satırları ise ayrı bir kanaldır: **[Log tutma](logging.md)**.

View file

@ -0,0 +1,51 @@
---
translation:
sections: [5c82b20cbd65ded0, 9dc22632be79a533, 1fb8f452e990c456, 42666ab914ff0cb1, c4e0cb3667fd5ff9]
tool: 1
---
# Örnekleme ve kök dizinler {#sampling-and-roots}
Bir işleyici, bağlı istemciden iki şey daha isteyebilir: istemcinin kendi modelinden bir tamamlama (**örnekleme (sampling)**) ve istemcinin çalışma alanı klasörleri (**kök dizinler (roots)**).
İkisi de SDK'nın konuştuğu her protokol sürümünde hâlâ çalışır. Ancak tasarımınızı bunların üzerine kurmadan önce uyarıyı okuyun:
!!! warning "2026-07-28 spesifikasyonuyla kullanım dışı bırakıldı"
Örnekleme ve kök dizinler `2026-07-28` itibarıyla kullanım dışı bırakıldı ([SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/2577)). Tamamen işlevsel olmaya devam ederler ve kaldırılmaya aday hâle gelmeden önce en az on iki ay boyunca spesifikasyonda kalırlar; yine de yeni uygulamalar bunların üzerine kurulmamalıdır. Önerilen geçiş yolları: örnekleme yerine doğrudan LLM sağlayıcınızın API'siyle entegre olun; kök dizinler yerine dizinleri araç parametreleri, kaynak URI'leri veya sunucu yapılandırması üzerinden geçirin. SDK genelindeki liste **[Kullanım dışı özellikler](../deprecated.md)** sayfasında.
## Örnekleme: istemcinin modelini ödünç alma {#sampling-borrow-the-clients-model}
Bir çözümleyici `Sample(...)` döndürür ve araç tamamlamayı alır; bu, **[Bağımlılıklar](dependencies.md)** sayfasında `Elicit`'i çalıştıran bağımlılık mekanizmasının aynısıdır:
```python title="server.py" hl_lines="10-15 19"
--8<-- "docs_src/sampling_and_roots/tutorial001.py"
```
* `Sample(messages, max_tokens=...)`, `sampling/createMessage` parametrelerini yansıtır. Enjekte edilen değer istemcinin `CreateMessageResult`'ıdır; `tools` veya `tool_choice` geçirirseniz bunun yerine bir `CreateMessageResultWithTools` olur.
* İstemcinin `sampling` yeteneğini bildirmiş olması gerekir (`tools` veya `tool_choice` geçiriyorsanız `sampling.tools`). Bildirmediyse çağrı, istemcinin işleyemeyeceği bir istek göndermek yerine `-32021` protokol hatasıyla başarısız olur. Geri kanalı (back-channel) olmayan 2026 öncesi bir oturum, gönderecek bir yer olmadığından her zamanki geri-kanal-yok hatasıyla başarısız olur.
* `2026-07-28`'de istek, çok turlu (multi-round-trip) akışın içinde iletilir (**[Çok turlu istekler](multi-round-trip.md)**); `2025-11-25`'te ise istemciye gönderilen bağımsız bir istektir. Kod her iki durumda da aynıdır, ancak çok turlu kurala dikkat edin: istek, yeniden deneme turları boyunca birebir aynı şekilde oluşmalıdır. Bu yüzden onu yalnızca aracın argümanlarından ve diğer kararlı verilerden oluşturun.
* `include_context`'e dokunmayın: `"none"` dışındaki değerlerin kendisi de kullanım dışı bırakıldı (SEP-2596) ve neredeyse hiçbir istemcinin bildirmediği bir yetenek gerektirir.
## Kök dizinler: bu nereye gitmeli? {#roots-where-should-this-go}
Kök dizinler, istemcinin sunucunun üzerinde çalışabileceğini söylediği klasörlerdir. Bilgilendirme amaçlı bir yönlendirmedir, erişim denetimi mekanizması değil. Bir çözümleyici `ListRoots()` döndürür:
```python title="server.py" hl_lines="10-11 15"
--8<-- "docs_src/sampling_and_roots/tutorial002.py"
```
* Enjekte edilen `ListRootsResult`, bir `Root` listesi taşır: her biri bir `file://` URI'si ve isteğe bağlı bir görünen ad.
* Denetim örneklemeyle aynıdır: bildirilmiş bir `roots` yeteneği yoksa çağrı, isteği göndermek yerine `-32021` ile başarısız olur.
Bağlantının diğer ucunda istemci, her iki isteği de zaten sahip olduğu callback'lerle yanıtlar: **[İstemci callback'leri](../client/callbacks.md)** sayfasında anlatılan `sampling_callback` ve `list_roots_callback`.
## 2025 neslinden bağlantılarda {#on-2025-era-connections}
`ctx.session.create_message(...)` ve `ctx.session.list_roots()`, oturumu doğrudan yöneten kod için hâlâ mevcuttur. Yalnızca bir geri kanalın bulunduğu yerde (2025 neslinden, durumsuz olmayan bağlantılarda) çalışırlar ve çağrıldıklarında kullanım dışı bırakma uyarısı verirler. Desteklenen biçim yukarıdaki çözümleyici işaretçileridir: iletim yolunu anlaşılan sürüme göre seçerler ve uyarı vermezler.
## Özet {#recap}
* Bir çözümleyiciden `Sample(...)` veya `ListRoots()` döndürün; araç `CreateMessageResult`'ı veya `ListRootsResult`'ı diğer bağımlılıklar gibi alır.
* İstemcinin eşleşen yeteneği bildirmesi gerekir; aksi hâlde çağrı, bir istek gönderilmek yerine `-32021` ile başarısız olur.
* İki özellik de `2026-07-28`'de kullanım dışı bırakıldı: şimdilik tamamen işlevsel, ancak yeni tasarımlar için yanlış tercih. Örnekleme yerine sağlayıcı API'lerini, kök dizinler yerine açık parametreleri tercih edin.
Yavaş bir aracın ne kadar ilerlediğini bildirme: **[İlerleme](progress.md)**.

View file

@ -0,0 +1,152 @@
---
translation:
sections: [60a9de8a0bdaa531, 317bbe7e4355cdcc, a61d660c8029e04a, 8f7e82fcb88df8a9, b165db51249ff8ed, 266f56fb798068a4, 7c0e57030b622139, df18d7c2417a9883]
tool: 1
---
# Abonelikler {#subscriptions}
Bir sunucunun kataloğu sabit değildir. Çalışma zamanında yeni araçlar ortaya çıkar, bir kaynak URI'sinin ardındaki içerik değişir.
İstemci bunlardan **abonelikler** sayesinde haberdar olur. İstemci tek bir `subscriptions/listen` isteği gönderir ve bu isteğin yanıtı akışın *ta kendisidir*: açık kalır ve istemcinin istediği değişiklik bildirimlerini taşır.
## Değişikliği araçtan yayımlama {#publish-it-from-the-tool}
Size düşen tek satır: değişikliği yayımlayın.
```python title="server.py" hl_lines="20 32"
--8<-- "docs_src/subscriptions/tutorial001.py"
```
* `await ctx.notify_resource_updated("board://sprint")` bu URI'ye abone olmuş her açık akışa ulaşır. Başka kimseye değil.
* `await ctx.notify_tools_changed()` araç listesi değişikliklerini isteyen her akışa ulaşır. Bunu alan istemci `tools/list`'i yeniden çağırır ve artık `sprint_report`'u görür.
* Kardeş metotlar `notify_prompts_changed()` ve `notify_resources_changed()`.
* Abone yoksa iş de yok. Boştaki bir sunucuda yayımlamak hiçbir şey yapmaz; bu yüzden kimsenin dinleyip dinlemediğini asla kontrol etmezsiniz. Neyin değiştiğini bildirirsiniz, o kadar.
`MCPServer`, `subscriptions/listen`'ı sizin yerinize sunar. Protokol düzeyindeki yükümlülükler (ilk çerçeve olarak onay, akış başına filtreleme, her çerçevede abonelik kimliği) SDK'nın işidir.
!!! check
Ağ üzerinde, filtresinde `board://sprint` geçen bir akış `complete_task` çalıştıktan sonra şöyle görünür:
```json
{"method": "notifications/subscriptions/acknowledged",
"params": {"notifications": {"resourceSubscriptions": ["board://sprint"]}, "_meta": {"io.modelcontextprotocol/subscriptionId": "listen-1"}}}
{"method": "notifications/resources/updated",
"params": {"uri": "board://sprint", "_meta": {"io.modelcontextprotocol/subscriptionId": "listen-1"}}}
```
Güncellemenin neyi *taşımadığına* dikkat edin: panonun kendisini. Her çerçeve, listen isteğinin JSON-RPC kimliğini `_meta` altında taşır ve bu kimlik abonelik kimliğidir. Onu istemci üretir: Python `Client`'ı `"listen-1"` gibi dizeler kullanır; başka istemciler tamsayı kullanabilir.
## Yalnızca istenenler {#only-what-was-asked-for}
Filtre bir sözleşmedir. Araç listesi değişikliklerini ve tek bir kaynak URI'sini isteyen bir akış bu iki türü alır, başka hiçbir şeyi almaz. Bir prompt değişikliği yayımlarsanız o akış sessiz kalır.
`MCPServer` kaynak URI'lerini birebir dize olarak eşleştirir; bu yüzden `board://sprint` URI'sini belirten bir akış `board://sprint/tasks/1` hakkında hiçbir şey duymaz. Belirtim, sunucunun abone olunan bir URI'nin alt kaynağındaki değişikliği bildirmesine izin verir; `MCPServer` bunu hiç yapmaz ama istemciler bunu bekleyecek şekilde yazılmıştır.
Akışın *olmadığı* iki şey:
* **Bir yeniden oynatma log'u değildir.** Kopan bir akış gitmiştir; kimse bağlı değilken yayımlanan olaylar kuyruğa alınmaz. İstemciler yeniden dinler ve yeniden getirir.
* **2025 yolu değildir.** `resources/subscribe` çağırmış istemcilere `ctx.session.send_resource_updated(uri)` hizmet verir. `notify_*` metotları yalnızca `subscriptions/listen` akışlarına ulaşır.
## Kimin izleyebileceğine karar verme {#deciding-who-may-watch}
Varsayılan olarak istenen her tür ve URI kabul edilir: her çağıran, yayımladığınız her URI'yi izleyebilir. Okuma işleyicinize hiçbir şey danışmaz, çünkü kimse okumuyordur. `files://{name}` işleyicinizin geri çevireceği bir çağıran yine de `files://payroll.csv` üzerinde bir akış açıp onun değiştiğini, hem de ne zaman değiştiğini öğrenebilir. İçeriği asla öğrenemez ve neyin var olduğunu yoklayamaz; çünkü bilinmeyen bir URI de kabul edilir ve yalnızca hiç tetiklenmez. Dar ama gerçek bir açık; bu yüzden çok kiracılı bir sunucudan kullanıcıya özel URI'ler yayımlamadan önce erişimi denetleyin.
Bu denetimi bir middleware (ara katman) üstlenir. `subscriptions/listen` isteğini SDK onaylamadan önce görür ve çağıran okuyamayacağı bir şey istediğinde isteği reddeder:
```python title="server.py" hl_lines="19-26 29"
--8<-- "docs_src/subscriptions/tutorial006.py"
```
* `ctx.params` ham istektir; bu yüzden middleware onu `SubscriptionsListenRequestParams` olarak kendisi doğrular ve istemcinin istediği filtreyi okur.
* Reddetmek, `call_next(ctx)`'ten önce fırlatılan bir `MCPError` demektir: istemci o hatayı alır, akış almaz ve bağlantı devam eder. Mesajı tek tip tutun ve hiçbir URI adı vermeyin; böylece bir ret hangi URI'lerin korunduğunu asla doğrulamaz.
* Tek bir `can_access(user, uri)` her iki soruyu da yanıtlar. Kaynak işleyicisi ona `resources/read` sırasında sorar; middleware ise `subscriptions/listen` sırasında. Tabloyu bir veritabanıyla ya da RBAC sisteminizle değiştirin, ikisi de uyumlu kalır.
* Karar akışın ömrü boyunca geçerlidir. Olay başına yeniden denetim yoktur; bu yüzden bir çağıranın erişimi akış ortasında sona erebiliyorsa (süresi dolan bir token gibi), sona erdiğinde o çağıranın bağlantısını kapatın.
Middleware sözleşmesinin tamamı, başka neleri sardığı ve neden geçici (provisional) olarak işaretlendiği de dahil, **[Middleware](../advanced/middleware.md)** sayfasında.
## İstemci tarafı {#the-client-end}
İşte o akışın diğer ucunda, panoyu takip eden bir istemci:
```python title="client.py" hl_lines="15"
--8<-- "docs_src/subscriptions/tutorial003.py"
```
`client.listen(...)`'a girmek isteği gönderir ve sizin onayınızı bekler; yani blok başladığında akış canlıdır ve türü belirli her olay bir yeniden getirme işaretidir, asla bir yük (payload) değildir. Sözleşmenin tamamı tek bir ekranda bu. İstemci tarafıyla ilgili geri kalan her şey kendi sayfasında: ana akışın yanında izleme, akış sonlanmaları ve yeniden dinleme. *İstemciler* altındaki **[Abonelikler](../client/subscriptions.md)** sayfasına bakın.
## Tek sürecin ötesine ölçekleme {#scaling-past-one-process}
Yayımlar, işleyicinizden açık akışlara bir `SubscriptionBus` üzerinden gider. Varsayılanı bellek içidir: tek süreç, içindeki tüm akışlar. Bir yük dengeleyicinin arkasında replikalar çalıştırana kadar doğru yanıt budur; çünkü o noktada bir istemcinin akışı tek bir replikaya bağlı kalır ve başka bir replikadaki yayımın ona ulaşması gerekir.
Bu birleşim noktasını siz uygularsınız: pub/sub arka ucunuzun üzerinde iki metot.
```python
from collections.abc import Callable
from redis.asyncio import Redis
from mcp.server.mcpserver import MCPServer
from mcp.server.subscriptions import ServerEvent # SubscriptionBus is a Protocol: no base class
class RedisSubscriptionBus:
def __init__(self, redis: Redis) -> None:
self._redis = redis
self._listeners: dict[object, Callable[[ServerEvent], None]] = {}
async def publish(self, event: ServerEvent) -> None:
await self._redis.publish("mcp-events", encode(event)) # to every replica
def subscribe(self, listener: Callable[[ServerEvent], None]) -> Callable[[], None]:
token = object()
self._listeners[token] = listener
def unsubscribe() -> None:
self._listeners.pop(token, None)
return unsubscribe
mcp = MCPServer("Sprint Board", subscriptions=RedisSubscriptionBus(redis))
```
`encode` size aittir; her replikada gelen mesajların kodunu çözüp kayıtlı her dinleyiciyi çağıran okuyucu görev de öyle. Dinleyiciler senkrondur, istisna fırlatmamalıdır ve sunucunun olay döngüsünde çalışır.
Veri yolu türü belirli `ServerEvent` değerleri taşır (dört küçük dataclass), asla JSON-RPC değil. Damgalama, filtreleme ve akış yaşam döngüleri SDK'da kalır; bu yüzden bir veri yolu uygulaması protokolü bozamaz. Yalnızca olayları süreçler arasında taşıyabilir.
Bir isteğin dışından yayımlamak için veri yolunu kendiniz oluşturun ki referansı elinizde olsun. Hiçbir şey geçirmediğinizde `MCPServer` içeride bir tane kurar ve onu dışarı açmaz.
```python
from mcp.server.subscriptions import InMemorySubscriptionBus, ToolsListChanged
bus = InMemorySubscriptionBus()
mcp = MCPServer("Sprint Board", subscriptions=bus)
async def tools_reloaded() -> None:
await bus.publish(ToolsListChanged()) # from a lifespan task, a webhook, anywhere
```
## Düşük düzeyli bileşim {#the-low-level-composition}
Düşük düzeyli `Server`'da önceden bağlanmış hiçbir şey yoktur; aynı parçalar üç satırda bir araya gelir:
```python title="server.py" hl_lines="8-9 47"
--8<-- "docs_src/subscriptions/tutorial002.py"
```
* Veri yolu sizindir; bu yüzden doğrudan ona yayımlarsınız: `await bus.publish(ResourceUpdated(uri=...))`. İşleyicilerinizin erişebileceği bir yere koyun: burada modül kapsamı, daha büyük bir uygulamada lifespan (yaşam döngüsü).
* `ListenHandler(bus)`, `MCPServer`'ın kaydettiği işleyicinin aynısıdır ve `on_subscriptions_listen=` sıradan bir işleyici yuvasıdır. Farklı bir anlam için o yuvaya kendi çağrılabilir nesnenizi koyun; o zaman belirtim yükümlülükleri size geçer: önce onaylayın, her çerçeveyi abonelik kimliğiyle damgalayın, filtrenin dışında hiçbir şey iletmeyin.
* `ListenHandler.close()` her açık akışı düzgünce sonlandırır. Her biri son çerçevesi olarak listen isteğinin sonucunu alır; bu, belirtimin sunucunun aboneliği bilerek sonlandırdığını söyleme biçimidir. Metot, bu akışlar boşaltmayı bitirmeden döner; bu yüzden aktarımı kapatmadan önce onlara kısa bir süre tanıyın. Onsuz, akışlar istemci bağlantıyı kestiğinde sona erer.
## Özet {#recap}
* İstemci tek bir `subscriptions/listen` isteğiyle katılır ve yanıt akışın kendisidir. Bunu sunmak yerleşiktir.
* `ctx.notify_*` ile yayımlarsınız; damgalama, filtreleme ve yaşam döngüsü işini SDK yapar.
* Olaylar işarettir, yük değil. Her iki uç da yeniden getirir.
* İstemci tarafı `async with client.listen(...)` bloğudur: ayrıntıları *İstemciler* altındaki **[Abonelikler](../client/subscriptions.md)** sayfasında.
* Düşük düzeyli `Server`'da aynı parçaları kendiniz birleştirirsiniz: bir veri yolu, `ListenHandler(bus)`, `on_subscriptions_listen` yuvası.
* Yatay ölçekleme, `SubscriptionBus`'ı (iki metot) uygulamak ve onu `MCPServer(subscriptions=...)` olarak geçirmek demektir.
Tüm bunları sunan sunucuyu ister tek replikanın ister yirmisinin arkasında çalıştırma konusu **[Dağıtım ve ölçekleme](../run/deploy.md)** sayfasında.