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
8 changed files with 89737 additions and 54 deletions

View file

@ -0,0 +1,122 @@
---
translation:
sections: [9e7b9a1710e5aeba, b74ca4c1d2ddddee, fa8714e61bf90c5a, 04db67a886b7271c, 857690fb8f876800]
tool: 1
---
# Caching hints {#caching-hints}
2026-07-28 protocol पर server `tools/list`, `prompts/list`, `resources/list`, `resources/templates/list`, `resources/read` और `server/discover` के लिए जो भी result लौटाता है, उसमें दो fields होते हैं: `ttlMs`, यानी client कितने milliseconds तक उस result को fresh मान सकता है, और `cacheScope`, यानी cache किया गया result users के बीच share किया जा सकता है (`"public"`) या किसी एक authorization context का है (`"private"`)।
server खुद कुछ भी cache नहीं करता। ये fields एक **घोषणा** हैं: "यह tool list सबके लिए एक जैसी है और अगले एक मिनट तक नहीं बदलेगी।" इसके बाद client (या आपके आगे लगा कोई gateway) round trip छोड़ सकता है। hints मानना या न मानना client की मर्ज़ी है; उन्हें भेजना server का काम है, और SDK यह आपके लिए कर देता है।
बिना कुछ configure किए हर result `ttlMs: 0, cacheScope: "private"` कहता है: तुरंत stale, कभी share नहीं। यह हमेशा सुरक्षित है और हमेशा spec के अनुरूप। अगर आपकी lists सच में स्थिर हैं और सभी callers के लिए एक जैसी हैं, तो construction के समय ही यह बता दें:
```python title="server.py" hl_lines="5-8"
--8<-- "docs_src/caching/tutorial001.py"
```
* map की keys **method name** हैं, और सिर्फ़ वही छह cacheable methods वैध keys हैं। parameter का type `Mapping[CacheableMethod, CacheHint]` है, इसलिए editor keys को autocomplete करता है और चलाने से पहले ही typo पकड़ लेता है; जो कुछ type checker से बच निकलता है, वह construction के समय raise होता है।
* जिस method का आप ज़िक्र नहीं करते, उसके defaults बने रहते हैं। map overrides का समूह है, manifest नहीं।
* `CacheHint(ttl_ms=5_000)` ने `scope` को unset छोड़ा, इसलिए वह `"private"` ही रहता है: हर caller के लिए पाँच second की freshness। scope और TTL अलग-अलग फ़ैसले हैं।
* `"server/discover"` भी वैध key है, क्योंकि discovery result किसी भी list की तरह cacheable है।
!!! warning
`cacheScope: "public"` का मतलब है कि आपका cache किया गया response **किसी को भी** दिया जा
सकता है। shared gateway बेझिझक एक user का result दूसरे को थमा देगा, भले ही request
authenticated रही हो। किसी result को `"public"` तभी mark करें जब वह हर caller के लिए एक जैसा
हो, और `cacheScope` को कभी access control की तरह इस्तेमाल न करें: यह label है, ताला नहीं।
## Per-handler override {#per-handler-override}
low-level `Server` पर handlers अपने results खुद बनाते हैं, और `ttl_ms` / `cache_scope` result models पर बस fields हैं। जो handler इन्हें explicitly set करता है, वह constructor map पर हमेशा भारी पड़ता है, field दर field:
```python title="server.py" hl_lines="10 16"
--8<-- "docs_src/caching/tutorial002.py"
```
handler ने `ttl_ms=1_000` कहा और scope के बारे में कुछ नहीं। wire पर: `ttlMs: 1000` (handler वाला, map का `60_000` नहीं) और `cacheScope: "public"` (map वाला, क्योंकि handler ने इसे unset छोड़ा)। explicit, configured पर भारी पड़ता है, और configured, default पर। यह हर field पर अलग से लागू होता है, इसलिए handler एक field को पक्का कर सकता है और दूसरे को server-wide policy पर छोड़ सकता है।
यही उन dynamic मामलों का रास्ता भी है जिन्हें constructor जान नहीं सकता: जो handler `resources/read` को हर user के हिसाब से filter करता है, वह बाकी तरह से public server में किसी एक URI के लिए `cache_scope="private"` लौटा सकता है।
paginated lists पर एक सावधानी: protocol की माँग है कि एक list के **हर page पर वही `cacheScope`** हो। constructor map यह अपने आप पूरा करता है, क्योंकि उसकी keys method हैं, page नहीं। लेकिन जो handler scope को खुद override करता है, उस consistency की ज़िम्मेदारी उसी की है: इसे **हर** page पर override करें, सिर्फ़ cursor मौजूद होने पर नहीं, वरना पहले page और दूसरे page में मेल नहीं रहेगा।
## client को क्या दिखता है {#what-the-client-sees}
2026-07-28 session पर `Client` आपके लिए hints का पालन करता है: इसमें built-in response cache है, जो default रूप से चालू रहता है। जो result `ttlMs` के साथ आता है, वह store हो जाता है, और उस TTL के भीतर वैसा ही call cache से serve होता है, बिना round trip के। जिस result में **कोई** hint नहीं होता, वह cache नहीं होता: बिना hint वाले results को `CacheConfig.default_ttl_ms` मिलता है, जिसका default `0` है (तुरंत stale), इसलिए जो server कुछ भी declare नहीं करता, उसे ठीक वैसा ही call-दर-call traffic दिखता है जैसा हमेशा दिखता था।
```python title="client.py" hl_lines="33 35 38"
--8<-- "docs_src/caching/tutorial003.py"
```
चार calls, तीन fetches। दूसरे call को fresh entry मिली और वह server तक पहुँचा ही नहीं; (inject की गई) clock को TTL से आगे बढ़ाने पर तीसरे ने फिर से fetch किया; चौथे ने `cache_mode="refresh"` कहा। यह kwarg पाँचों caching verbs पर मौजूद है (`list_tools`, `list_prompts`, `list_resources`, `list_resource_templates`, `read_resource`):
* `"use"` (default) fresh entry हो तो उसे serve करता है, और न हो तो fetch करके store करता है।
* `"refresh"` कभी serve नहीं करता: यह fetch करता है और result store करता है, जो कुछ cache में था उसे बदलते हुए।
* `"bypass"` cache को छुए बिना round trip करता है: न read, न write।
एक नियम `"use"` से ऊपर है: **`meta` वाले calls हमेशा server तक पहुँचते हैं।** जिस request में `meta` set हो (progress token, tracing fields), उसे wire request की उम्मीद होती है, इसलिए `cache_mode="use"` में उसे `"refresh"` माना जाता है: cache read छोड़ दिया जाता है, और fetch किया गया result फिर भी cache की entry की जगह ले लेता है। `"bypass"` और explicit `"refresh"` हमेशा की तरह ही बर्ताव करते हैं।
caching पूरी तरह बंद करने के लिए `Client(server, cache=None)` से construct करें: हर call फिर से round trip है, और `cache_mode`, भले ही अब भी स्वीकार होता है, कुछ नहीं करता।
scope का पालन भी अपने आप होता है: `"private"` entries cache के *partition* (नीचे देखें) से बँधी होती हैं, जबकि `"public"` वाली चाहें तो ज़्यादा व्यापक sharing चुन सकती हैं। और जिन entries का नाम notifications लेते हैं, ठीक उनके लिए **notifications TTL पर भारी पड़ते हैं**: `list_changed` notification मेल खाती cached listing को evict कर देता है, और `resources/updated` ठीक उसी URI के तहत store किए गए cached read को evict करता है, चाहे वे कितने भी fresh रहे हों। 2026-07-28 connection पर ये notifications `subscriptions/listen` stream पर आते हैं जिसे आप `client.listen(...)` से खोलते हैं, और eviction आपके watcher को event दिखने से पहले पूरा हो जाता है; **[Subscriptions](subscriptions.md)** वही page है।
`resources/updated` पर एक सावधानी: eviction सिर्फ़ exact URI पर होता है। store contract में कोई enumerate या scan operation नहीं है (reference TypeScript implementation की तरह ही), इसलिए *sub*-resource URI वाला notification उसके parent के cached read को evict नहीं करता। अगर आपका server sub-resources का संकेत इसी तरह देता है, तो parent को `cache_mode="refresh"` से फिर fetch करें।
### इसे configure करना: `CacheConfig` {#configuring-it-cacheconfig}
```python
from mcp.client import CacheConfig
client = Client("https://api.example.com/mcp", cache=CacheConfig(default_ttl_ms=5_000))
```
* `store`: entries कहाँ रहती हैं। default हर client के लिए नया in-memory store है; clients या processes के बीच cache share करना हो तो अपना `ResponseCacheStore` implementation (जैसे Redis-backed) pass करें। contract types (`ResponseCacheStore`, `CacheKey`, `CacheEntry`, और default `InMemoryResponseCacheStore`) `mcp.client` से import किए जा सकते हैं। एक lookup एक के बाद एक ज़्यादा से ज़्यादा दो store `get` जारी कर सकता है (पहले private arm, फिर public), इसलिए remote store की latency की उम्मीदें उसी हिसाब से तय करें। custom store के लिए explicit `partition` **ज़रूरी** है।
* `partition`: authorization-context label, जो shared store के भीतर एक principal की `"private"` entries को किसी दूसरे को serve होने से रोकता है।
* `target_id`: explicit server identity, custom transports और in-process servers के लिए (नीचे देखें)।
* `default_ttl_ms`: उन results पर लागू TTL जिनमें `ttlMs` hint नहीं है। default `0` बिना hint वाले results को uncached छोड़ देता है।
* `share_public`: server ने जिन entries को `"public"` बताया, उन्हें partitions के पार serve करना (नीचे देखें)। default रूप से बंद।
* `clock`: wall-clock source, epoch seconds में। ऊपर के उदाहरण की तरह एक inject करें, और expiry tests में sleep की ज़रूरत नहीं रहती।
!!! warning "Partition = verified principal"
`partition` किसी **verified credential** से निकालें, जैसे validate किए गए token का subject। इसे request में आए data से कभी न निकालें, और server URL से भी कभी नहीं (server identity key की एक अलग axis है)। SDK एक library है जिसका अपना कोई authentication नहीं: trust anchor वही है जो `CacheConfig` construct करता है, यानी deployment, tenant नहीं। multi-tenant gateway हर authenticated principal के लिए एक अलग `CacheConfig` बनाता है।
partition `Client` के पूरे जीवनकाल के लिए स्थिर भी रहता है। अगर connection का authorization context session के बीच बदलता है (जैसे किसी दूसरे principal के रूप में re-authentication), तो cache उसके साथ नहीं बदलता; नए principal के लिए नया `Client` construct करें।
cache keys में **server की identity** भी होती है: वह URL string जिसे आपने dial किया, जिसमें से `user:pass@` userinfo हटा दी जाती है और बाकी byte-दर-byte वैसी ही रहती है। न case folding, न query reordering, न trailing slash की सफ़ाई। कम normalize करने से सिर्फ़ sharing घटती है, जबकि ज़्यादा normalize करने से दो tenants (`?tenant=a` बनाम `?tenant=b`) आपस में मिल सकते हैं, इसलिए ऊपरी तौर पर अलग URL बस entries share नहीं करते। जब कोई URL नहीं होता (in-process server, या `Transport` instance), तो client को उसकी जगह हर instance के लिए एक random identity मिलती है; server को नाम देने के लिए `CacheConfig.target_id` set करें (custom store के साथ यह ज़रूरी है, और construction यह बता देता है)। identity key material में जाने से पहले sha256-hash की जाती है, इसलिए जिस URL की query string में secrets हों, वह store keys में कभी नहीं दिखता। pre-hash रूप को खुद भी log न करें।
!!! warning "`share_public` server पर भरोसा करता है, पूरे fleet में"
default रूप से `"public"` entries भी अपने partition के भीतर ही रहती हैं। `share_public=True` उन entries को, जिन्हें server ने `cacheScope: "public"` mark किया, store इस्तेमाल करने वाले **हर** partition को serve करता है, और उन सबकी ओर से server के वर्गीकरण पर भरोसा करता है। जो server per-tenant data पर `"public"` की मुहर लगा देता है (bug से या बदनीयती से), वह फिर एक tenant का response बाकियों को leak कर देता है। यह flag जान-बूझकर सिर्फ़ constructor स्तर पर है: per-call `cache_mode` caching को सीमित कर सकता है, लेकिन per-call कोई भी चीज़ sharing को बढ़ा नहीं सकती।
### cache क्या कभी नहीं करता {#what-the-cache-never-does}
* **Session-tier calls इसे bypass करते हैं।** `client.session.list_tools()` और उसके साथी हमेशा round trip करते हैं; cache `Client` verbs पर रहता है।
* **`server/discover` इससे बाहर रहता है।** discover result एक बार, connect के समय, दिया जाता है और response cache में कभी नहीं जाता, भले ही उसमें `ttlMs` हो। अगर reconnect probe से बचने के लिए आप उसे खुद persist करते हैं ([`prior_discover`](../protocol-versions.md#reconnecting-with-prior_discover)), तो उसकी freshness का हिसाब आपका है: `DiscoverResult` में ठीक इसी काम के लिए `ttl_ms` और `cache_scope` पहले से parse किए हुए मौजूद हैं।
* **Continuation pages कभी cache नहीं होते।** सिर्फ़ बिना cursor वाले calls हिस्सा लेते हैं। expired cursor के कारण reject हुआ continuation page cached listing को *evict* ज़रूर करता है, क्योंकि listing उसके नीचे बदल गई।
* **Multi-round-trip reads कभी cache नहीं होते।** `input_responses`/`request_state` से seed किया गया `read_resource`, या ऐसा जो input rounds से होकर resolve होता है, कभी cache में नहीं जाता (spec का MUST)।
* **Notification eviction को notifications चाहिए।** eviction उतना ही अच्छा है जितनी transport की delivery, और आधुनिक in-process path (default `mode="auto"` के साथ `Client(server)`) आज standalone notifications deliver नहीं करता।
* **Eviction eventual है, तात्कालिक नहीं।** wire-path notifications spawn किए गए tasks से dispatch होते हैं, इसलिए किसी notification के आने से race कर रहे call को pre-eviction entry एक बार और serve हो सकती है; यह window dispatch latency से सीमित है, और eviction फिर भी हो ही जाता है।
* **कोई stale-if-error नहीं।** expired entry कभी इसलिए serve नहीं होती कि refetch fail हो गया; error आगे propagate होता है।
* **कोई early re-fetch नहीं।** store की गई entry तब तक serve होती है जब तक उसका TTL expire न हो जाए, और उसके बाद का अगला call round trip की कीमत चुकाता है; background में कुछ refresh नहीं होता।
* **कोई coalescing नहीं।** दो concurrent एक जैसे calls दो fetches हैं।
* **24 घंटे से ज़्यादा का TTL नहीं।** इससे बड़ा `ttlMs`, चाहे server ने भेजा हो या configure किया गया हो, store करते समय घटा दिया जाता है (`mcp.client.caching.MAX_TTL_MS`), जिससे यह सीमित रहता है कि कोई भी entry, hint चाहे कितना भी उदार हो, कितनी देर serve हो सकती है।
* **shared store** पर clients आपस में race करते हैं। जब किसी eviction ने चल रहे fetch को पीछे छोड़ दिया हो तो हर client अपना write छोड़ देता है, लेकिन कोई *co-tenant* client अब भी ऐसी entry वापस लिख सकता है जिसे किसी ऐसे eviction ने हटा दिया था जो उसने कभी देखा ही नहीं; और race का यह हिसाब-किताब भी खुद सीमित है: 4096 tracked keys के बाद सबसे पुरानी key का guard सबसे पहले हटता है। दोनों windows स्वीकार्य हैं, और ऊपर बताई गई TTL cap उन्हें बंद कर देती है।
* **protocol की अलग-अलग पीढ़ियों के बीच serve नहीं किया जाता।** entries negotiated protocol version तक सीमित हैं: shared persistent store पर कोई session कभी ऐसी entry serve नहीं करता जो किसी दूसरे negotiated version के तहत लिखी गई हो (वही listing पीढ़ी के हिसाब से सच में अलग होती है, क्योंकि SDK पुराने sessions के लिए 2026 वाले fields हटा देता है)। eviction भी इसी तरह सिर्फ़ मौजूदा पीढ़ी की entries को छूता है; दूसरी पीढ़ी की entries बस TTL से अपने आप पुरानी होकर हट जाती हैं।
### hints खुद पढ़ना {#reading-the-hints-yourself}
hints हर cacheable result पर सादे fields के रूप में भी मौजूद हैं (`result.ttl_ms` और `result.cache_scope`, पहले से parse किए हुए), अगर आप built-in cache के ऊपर (या उसकी जगह) अपना हिसाब-किताब रखना चाहें।
किसी **पुराने server** (pre-2026 protocol) के सामने ये fields wire पर होते ही नहीं, और models अपने conservative defaults दिखाते हैं: `ttl_ms == 0` और `cache_scope == "private"`, stale और unshared, जो कुछ भी declare न करने वाले server के लिए सही मान्यता है। cache legacy session के साथ भी यही करता है: वहाँ hints कभी देखे ही नहीं जाते (wire पर चाहे जो keys आएँ), सिर्फ़ `default_ttl_ms` लागू होता है, और उसका default `0` कुछ भी cache नहीं करता, इसलिए pre-2026 connection ठीक वैसे ही बर्ताव करता है जैसे cache के आने से पहले करता था। अगर आपको "server ने 0 कहा" और "server ने कुछ नहीं कहा" में फ़र्क करना हो, तो `"ttl_ms" in result.model_fields_set` जाँचें: यह तभी set होता है जब field सच में आया हो।
## पुराने clients {#older-clients}
pre-2026 protocol versions वाले clients को इनमें से कोई भी field कभी नहीं दिखता; SDK उन connections के लिए serialization के समय इन्हें हटा देता है। hints एक बार configure करें; version के हिसाब से अलग कुछ लिखने को नहीं है।
## सारांश {#recap}
* छह methods में `ttlMs`/`cacheScope` होते हैं; SDK इनका default `0`/`"private"` रखता है, stale और unshared, हमेशा सुरक्षित।
* construction के समय `cache_hints={method: CacheHint(...)}` (`MCPServer` और `Server` दोनों में) हर method के लिए server-wide values set करता है।
* जो handler अपने result पर ये fields set करता है, वह map को override करता है, field दर field।
* `"public"` एक वादा है कि result हर caller के लिए एक जैसा है। यह access control नहीं है।
* `Client` hints का पालन अपने आप करता है: उसका response cache default रूप से चालू है, दोबारा fetch करने की बजाय fresh entries serve करता है, और जो servers (या sessions) कोई hint नहीं देते उनके लिए कुछ भी cache नहीं करता।
* हर call पर, `cache_mode="refresh"` दोबारा fetch करता है और `"bypass"` cache को छोड़ देता है; construction के समय `cache=None` इसे पूरी तरह बंद कर देता है।

View file

@ -0,0 +1,154 @@
---
translation:
sections: [adf3c545b5be46b6, 916cd3ab1c03f461, e9be7a8d0eb0a456, 565890a636288ecf, 6af7e49db9129ec3, 06b0238c174186af, 90c6043be435fcb0]
tool: 1
---
# Client callbacks {#client-callbacks}
MCP में लगभग हर request एक ही दिशा में जाती है: client से server की ओर।
server भी **client** से चीज़ें माँग सकता है: user से कोई सवाल पूछना, user के model से sampling करना, user के workspace folders की सूची लेना। इन requests का जवाब आप `Client(...)` को **callbacks** देकर देते हैं।
## पूछने वाला server {#a-server-that-asks}
यह एक ऐसा server है जिसका tool अपने आप पूरा नहीं हो सकता:
```python title="server.py" hl_lines="16"
--8<-- "docs_src/client_callbacks/tutorial001.py"
```
* `ctx.elicit(...)` **client को** `elicitation/create` request भेजता है और इंतज़ार करता है।
* जब तक कोई (form में कोई व्यक्ति, या आपका code) `name` नहीं देता, tool लौटता नहीं।
यह server वाला आधा हिस्सा है, और इसकी पूरी जानकारी **[Elicitation](../handlers/elicitation.md)** page में है। यह page wire का दूसरा सिरा है।
## Elicitation callback {#the-elicitation-callback}
```python title="client.py" hl_lines="6-10 16-17"
--8<-- "docs_src/client_callbacks/tutorial002.py"
```
* elicitation callback का रूप `async (context, params) -> ElicitResult` है।
* `params.message` सवाल है। `params.requested_schema` उस जवाब का JSON Schema है जो server चाहता है। असली client इससे form बनाकर दिखाता है; यह वाला अपने आप भर देता है।
* आप `ElicitResult(action="accept", content={...})` लौटाते हैं, या `action="decline"`, या `action="cancel"`। इनके अलावा सिर्फ़ एक विकल्प है `ErrorData(...)`, जो request को ठुकरा देता है और पूरा call fail कर देता है।
* `context` एक `ClientRequestContext` है: चालू `session`, server का `request_id`, और उसके साथ लगाया गया कोई भी `meta`।
!!! tip
`params` elicitation के दोनों modes का union है। यहाँ `params.mode` का मान `"form"` है; `"url"` request
में schema की जगह `params.url` आता है। एक ही callback दोनों को संभालता है; `params.mode` पर branch करें।
पूरा pattern **[Elicitation](../handlers/elicitation.md)** में दिखाया गया है।
### इसे आज़माएँ {#try-it}
`issue_card` call करें और दोनों सिरों को देखें।
आपके callback को server का सवाल मिलता है, पहले से parse किया हुआ:
```python
params.mode # 'form'
params.message # 'What name should go on the card?'
params.requested_schema # {'properties': {'name': {'title': 'Name', 'type': 'string'}},
# 'required': ['name'], 'title': 'CardHolder', 'type': 'object'}
```
वह जवाब देता है, tool के अंदर `ctx.elicit(...)` आगे बढ़ता है, और tool पूरा हो जाता है:
```python
result.content # [TextContent(type='text', text='Card issued to Ada Lovelace.')]
```
आपकी ओर से एक `tools/call`, server की ओर से वापस एक `elicitation/create`, जिसका जवाब आपके function ने दिया, और यह सब एक ही tool call के अंदर।
!!! info
`Client(...)` call पर `mode="legacy"` असल में काम कर रहा है। default रूप से `Client(...)` modern
protocol path negotiate करता है, और उस path में server-से-client requests के लिए कोई back-channel नहीं है:
आपका callback चलने से पहले ही `ctx.elicit` fail हो जाता है। यह transport तय नहीं करता; negotiated
protocol तय करता है, in-memory में भी और URL पर भी। जब भी आपके client को ऐसी किसी request का जवाब देना हो,
`mode="legacy"` तय करें; इस page के पीछे का हर test यही करता है। पूरी जानकारी **[Protocol versions](../protocol-versions.md)** में है।
2026-07-28 session पर callback बेकार नहीं होता, उसे input अलग तरीके से मिलता है: जब कोई tool
`ElicitRequest` वाला `InputRequiredResult` लौटाता है, तो `Client` उस entry को उसी
`elicitation_callback` को भेज देता है और आपके लिए call दोबारा करता है। वह flow **[Multi-round-trip requests](../handlers/multi-round-trip.md)** है।
## callback ही capability है {#a-callback-is-a-capability}
आपने server को कभी नहीं बताया कि आपका client elicitation requests का जवाब दे सकता है। SDK ने बताया।
जब client जुड़ता है तो वह अपनी `capabilities` घोषित करता है, जो server की capabilities का ठीक उल्टा रूप है। वह object आप नहीं लिखते। **callback register करना ही घोषणा है।**
| आप देते हैं | client घोषित करता है |
| --- | --- |
| `elicitation_callback=` | `"elicitation": {"form": {}, "url": {}}` |
| `sampling_callback=` | `"sampling": {}` |
| `list_roots_callback=` | `"roots": {"listChanged": true}` |
| इनमें से कोई नहीं | `{}` |
sampling की sub-capabilities ही एकमात्र बारीकी हैं: जब आपका sampler `tools` / `tool_choice` parameters संभालता हो, तो `sampling_callback` के साथ `sampling_capabilities=SamplingCapability(tools=SamplingToolsCapability())` दें। servers को `sampling.tools` घोषित दिखनी चाहिए, तभी वे इन्हें भेज सकते हैं।
`logging_callback` और `message_handler` इस table में नहीं हैं। वे notifications संभालते हैं, और notifications को किसी capability की ज़रूरत नहीं।
server इस घोषणा को `ctx.session.check_client_capability(...)` से पढ़ता है। ऐसा करने वाला एक tool जोड़ें:
```python title="server.py" hl_lines="23-31"
--8<-- "docs_src/client_callbacks/tutorial003.py"
```
सिर्फ़ `elicitation_callback` के साथ जुड़ें और इसे call करें:
```python
result.structured_content # {'result': ['elicitation']}
```
तीनों callbacks दें तो आपको `['elicitation', 'sampling', 'roots']` मिलता है। कोई न दें तो `[]` मिलता है।
!!! check
अब गलत काम करें: `elicitation_callback` के **बिना** जुड़ें और फिर भी `issue_card` call करें।
server की `elicitation/create` request फिर भी आपके client तक पहुँचती है, और SDK आपकी ओर से उसका
जवाब देता है, error के साथ, क्योंकि आपने कभी कहा ही नहीं कि आप इसे संभाल सकते हैं। वह error पूरे call को
डुबो देता है। `call_tool` कोई `is_error` result नहीं लौटाता; वह raise करता है:
```text
MCPError: Elicitation not supported
```
यह protocol error है (`-32600`, **invalid request**), tool error नहीं: model के पढ़ने और दोबारा
कोशिश करने के लिए इसमें कुछ नहीं है। इसीलिए `client_features` रखना फ़ायदेमंद है: अच्छे ढंग से बना server
पूछने से पहले जाँच लेता है।
## Deprecated जोड़ी {#the-deprecated-pair}
`sampling_callback` `sampling/createMessage` का जवाब देता है: server **आपके** model से कुछ complete करने को कहता है। `list_roots_callback` `roots/list` का जवाब देता है: server पूछता है कि वह किन directories में काम कर सकता है।
दोनों काम करते हैं। दोनों ऊपर वाले नियम का पालन करते हैं। और दोनों ऐसे RPCs को serve करते हैं जिन्हें **2026-07-28 spec हटा देता है**: modern server request के बीच में आपके client को वापस call नहीं करता, वह request को tool result के हिस्से के रूप में आपको वापस सौंप देता है (**[Multi-round-trip requests](../handlers/multi-round-trip.md)**)। callbacks खुद बेकार नहीं हुए हैं। जब किसी `InputRequiredResult` में `CreateMessageRequest` या `ListRootsRequest` होता है, तो `Client` का auto-loop उसे उसी `sampling_callback` या `list_roots_callback` को भेज देता है जो आपने यहाँ register किया था। पूरी सूची **[Deprecated features](../deprecated.md)** में है।
जो servers अभी आगे नहीं बढ़े हैं, उनसे बात करने के लिए आपको ये callbacks अब भी चाहिए। signatures:
```python title="client.py"
--8<-- "docs_src/client_callbacks/tutorial004.py"
```
* sampling callback को पूरा `CreateMessageRequestParams` (`messages`, `model_preferences`, `max_tokens`) मिलता है और वह `CreateMessageResult` लौटाता है। model **आप** चलाते हैं, जैसे चाहें; SDK सिर्फ़ request पहुँचाता है।
* roots callback कोई params नहीं लेता और `ListRootsResult` लौटाता है।
* दोनों में से कोई भी, मना करने के लिए, इसकी जगह `ErrorData(...)` लौटा सकता है।
इन्हें `Client(...)` को ठीक वैसे ही दें जैसे `elicitation_callback` को।
## Notification callbacks {#the-notification-callbacks}
दो और। इनमें से कोई कुछ घोषित नहीं करता।
`logging_callback` को server की भेजी हुई `notifications/message` मिलती है, `LoggingMessageNotificationParams` (`level`, `logger`, `data`) के रूप में। protocol logging खुद 2026-07-28 spec में deprecated है (इसकी जगह क्या करना है, यह **[Logging](../handlers/logging.md)** में है), इसलिए यह callback उन servers के लिए है जो इसे अब भी emit करते हैं। 2026 पीढ़ी के connection पर अकेला callback आपको कुछ नहीं दिलाता, क्योंकि 2026 servers log messages सिर्फ़ उन्हीं requests को भेजते हैं जो इसके लिए opt in करती हैं: हर request पर वह opt-in लगाने और उस level व उससे ऊपर के messages पाने के लिए `Client(...)` को `log_level="info"` (या कोई और level) दें। 2026 से पहले के servers इसे नज़रअंदाज़ करते हैं और अपना `logging/setLevel` वाला व्यवहार बनाए रखते हैं।
`message_handler` सब कुछ पकड़ने वाला है: session जो भी server notification सामने लाता है, वह इस तक पहुँचती है (उसके खास callback के अलावा), और stream-backed transport पर हर transport-level `Exception` भी। दो कभी नहीं पहुँचते: `notifications/cancelled` को SDK सामने लाने के बजाय खुद लागू करता है, और चालू `listen()` stream की subscription acknowledgment उसी stream में खप जाती है। parameter को `IncomingMessage` (`ServerNotification | Exception`, `mcp.client` से export किया हुआ) से annotate करें। जानने लायक एक ही pattern है `if isinstance(message, Exception): raise message`, ताकि टूटा हुआ connection चुपचाप गायब होने के बजाय ज़ोर से fail हो।
## सारांश {#recap}
* server client को requests भेज सकता है। आप उनका जवाब `Client(...)` को दिए गए callbacks से देते हैं।
* elicitation callback मौजूदा वाला है: `async (context, params) -> ElicitResult`, form और URL mode दोनों के लिए एक ही function।
* **callback register करना ही capability घोषित करना है।** इसके बिना SDK आपकी ओर से server की request ठुकरा देता है और पूरा call `MCPError` के साथ fail हो जाता है।
* server पूछने से पहले `ctx.session.check_client_capability(...)` से पता कर लेता है।
* `sampling_callback` और `list_roots_callback` इसी तरह काम करते हैं लेकिन deprecated features को serve करते हैं; modern servers इनकी जगह multi-round-trip requests इस्तेमाल करते हैं।
* `logging_callback` और `message_handler` को notifications मिलती हैं। वे कुछ घोषित नहीं करते।
`Client(...)` का पहला argument transport object है। हर प्रकार की जानकारी **[Client transports](transports.md)** में है।

View file

@ -0,0 +1,151 @@
---
translation:
sections: [a91322c46111d16d, 8e6fd6d6f59bb568, e7828fd2729b2c9d, a03ec26bfc678b65, 1034c653c0bcf1b0]
tool: 1
---
# Identity assertion {#identity-assertion}
साधारण OAuth provider (**[OAuth clients](oauth-clients.md)**) MCP server से एक सवाल पूछकर शुरू करता है: **आप किस authorization server पर भरोसा करते हैं?** जवाब जिधर इशारा करे, वह उधर चला जाता है, और फिर या तो कोई इंसान sign in करता है या उसकी जगह कोई pre-shared secret काम आता है।
Enterprise नहीं चाहता कि इनमें से कोई भी बात हर server के हिसाब से अलग तय हो। वह पहले से एक identity provider चलाता है (Okta, Microsoft Entra ID, या आपका अपना); user आज सुबह ही उसमें sign in कर चुका है; और यही वह एक जगह है जहाँ security team तय करना चाहती है कि कौन कहाँ तक पहुँच सकता है। [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990), यानी **Enterprise-Managed Authorization** extension, यह फ़ैसला वहीं ले जाता है। IdP एक short-lived JWT sign करता है, **Identity Assertion JWT Authorization Grant**, यानी **ID-JAG**: यह बयान कि **यह user**, **इस client** के ज़रिए, **इस MCP server** तक पहुँच सकता है। Client इसे देकर बदले में साधारण access token ले लेता है। न browser, न consent screen, न dynamic registration।
यह page उसी लेन-देन के दोनों सिरों के बारे में है। MCP server खुद कभी नहीं बदलता: वह अब भी **[Authorization](../run/authorization.md)** वाला resource server ही है, जो भी token सामने आए उसे जाँचता है।
## दो token requests {#two-token-requests}
यहाँ दो अलग-अलग authorities काम कर रही हैं, और उन्हें अलग-अलग नाम से पहचान लेना ही इस page को समझने का ज़्यादातर हिस्सा है। **Enterprise IdP** आपके organization का identity provider है: उसे पता है कि employee कौन है, policy वहीं रहती है, और ID-JAG वही जारी करता है। SDK उससे कभी बात नहीं करता। **MCP authorization server** वही पक्ष है जो **[Authorization](../run/authorization.md)** में था: MCP server के metadata में नामित issuer, वह चीज़ जो वे tokens बनाती है जिन्हें वह MCP server स्वीकार करता है। साधारण OAuth flow में ये दोनों भूमिकाएँ आमतौर पर एक ही system निभाता है। यहाँ ये दो हैं, और पूरा grant बस इतना है कि दूसरा पहले पर भरोसा करने को राज़ी हो।
Client इनमें से हर एक को एक token request भेजता है।
1. **Enterprise IdP को।** Client user के sign-in (उनका OpenID Connect ID token) के बदले ID-JAG लेता है। यह [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693) token exchange है, यह पूरी तरह आपके IdP का API है, और **SDK इसे नहीं करता**। यह आप करते हैं, एक async callback के अंदर। Policy का फ़ैसला भी यहीं होता है: जो IdP मना कर दे वह ID-JAG जारी ही नहीं करता, और पेश करने को कुछ बचता ही नहीं।
2. **MCP authorization server को।** Client ID-JAG को [RFC 7523](https://datatracker.ietf.org/doc/html/rfc7523) `jwt-bearer` grant के तहत पेश करता है (`grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer`, ID-JAG `assertion` के रूप में) और access token पाता है। **यही वह request है जो SDK भेजता है**, और इसे स्वीकार करना ही वह एक चीज़ है जो यह page authorization server में जोड़ता है।
नीचे सब कुछ दूसरी request के बारे में है: उसे भेजने वाला client और उसका जवाब देने वाला authorization server।
## Client {#the-client}
**`IdentityAssertionOAuthProvider`** `mcp.client.auth.extensions.identity_assertion` में रहता है। **[OAuth clients](oauth-clients.md)** के हर provider की तरह यह भी `httpx2.Auth` है: एक बनाएँ, उसे `auth=` पर रखें, और `httpx2.AsyncClient` transport को सौंप दें।
```python title="client.py" hl_lines="49-50 53-61"
--8<-- "docs_src/identity_assertion/tutorial001.py"
```
इसे नीचे से पढ़ें।
* `main()` वही standard OAuth-client वाला `main()` है (**[OAuth clients](oauth-clients.md)**), पंक्ति-दर-पंक्ति बिना बदलाव। यही बात है: एक बार provider बन जाए तो आगे किसी को पता नहीं चलता कि token किस grant से आया।
* Provider वह लेता है जो बाकी providers discover नहीं कर सकते: `client_id` और `client_secret` जो किसी ने authorization server के साथ **पहले से register** कर रखे हैं, उस authorization server का `issuer`, और `assertion_provider`, एक async callback जो माँगने पर ताज़ा ID-JAG लौटाता है।
* `storage` वही `TokenStorage` protocol है। सिर्फ़ दो token methods ही कभी call होते हैं; यहाँ dynamic registration नहीं है, इसलिए याद रखने को कोई `client_info` नहीं है।
### Assertion provider {#the-assertion-provider}
`fetch_id_jag(audience, resource)` ही वह इकलौता code है जो आप लिखते हैं। यह हर token exchange पर एक बार await होता है, construction के समय कभी नहीं, और सिर्फ़ तब **जब** authorization server का metadata fetch और validate हो चुका हो, इसलिए गलत configure किया गया issuer कभी assertion leak नहीं करवाता। इसके दो arguments उन claims में से दो हैं जिनके साथ ID-JAG बनना ज़रूरी है: `audience` authorization server का issuer है (ID-JAG का `aud`) और `resource` MCP server का canonical identifier है (ID-JAG का `resource`)। तीसरा वह है जो आपके पास पहले से है: ID-JAG के `client_id` claim में वही `client_id` होना चाहिए जो आपने provider को दिया, वरना authorization server exchange से मना कर देता है।
उसके ऊपर वाला `idp_issue_id_jag` **आपका code नहीं है**। वह identity provider की जगह खड़ा है, और assertion को उसी process में sign करता है ताकि file पूरी रहे और आप ID-JAG में जाने वाला हर claim पढ़ सकें। असली `fetch_id_jag` इसकी जगह पिछले section की पहली token request भेजता है: आपके IdP के सामने [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693) token exchange, जिसे Identity Assertion JWT Authorization Grant draft परिभाषित करता है और जिसे [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) profile करता है। Sign in किए हुए user का ID token `subject_token` के रूप में जाता है, `requested_token_type` ID-JAG का अपना URN है (`urn:ietf:params:oauth:token-type:id-jag`), `audience` और `resource` जस के तस आगे जाते हैं, और response में ID-JAG आता है। अपने IdP की documentation में इन्हीं नामों के साथ यही exchange ढूँढें।
!!! tip
हर exchange के लिए ताज़ा ID-JAG माँगा जाता है, और यही मक़सद है: यह एक बार इस्तेमाल होने वाला,
कुछ मिनट जीने वाला grant है, और इस page का authorization server एक ही ID-JAG को दो बार स्वीकार
करने से मना कर देता है। इसे cache न करें। इसके बदले जो access token मिलता है, दोबारा इस्तेमाल वही होता है।
### Issuer configuration है {#the-issuer-is-configuration}
उलटफेर यहाँ है। `OAuthClientProvider` resource server से पूछता है कि कौन सा authorization server इस्तेमाल करे, और जवाब जिधर इशारा करे उधर चला जाता है। यह provider ऐसा करने से मना करता है: `issuer` ज़रूरी है, [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414) metadata उसी issuer के अपने well-known path से fetch होता है, token endpoint उसी issuer के origin पर होना चाहिए, और resource server से कभी कुछ नहीं पूछा जाता।
Extension इसकी माँग नहीं करता; यह जान-बूझकर चुना गया ज़्यादा सख़्त रास्ता है। इस client के पास चुराने लायक दो चीज़ें हैं, एक pre-registered secret और एक audience-bound assertion, और जो client किसी compromised MCP server को खुद को हमलावर के authorization server की तरफ़ मोड़ने दे, वह दोनों उसी को post कर देगा। Construction के समय issuer को pin कर देने से वह बातचीत ही ख़त्म हो जाती है।
!!! warning
Configure किए गए `issuer` की तुलना metadata document के `issuer` field से RFC 8414 §3.3 के
simple string comparison से होती है: एक-एक character, आख़िरी slash समेत, बिना किसी normalization के।
इसका अंदाज़ा न लगाएँ। अपने authorization server से `/.well-known/oauth-authorization-server` fetch करें
और जो `issuer` value वह लौटाए उसे copy करें। इस page के authorization server के लिए वह
`https://auth.example.com/` है, slash के साथ, क्योंकि उसका issuer pydantic URL object से बना था।
Mismatch होने पर flow एक भी credential या assertion भेजे जाने से पहले `OAuthFlowError: Authorization server metadata issuer
mismatch` पर रुक जाता है।
### Confidential client {#a-confidential-client}
`client_secret` ज़रूरी है; इसके बिना constructor `ValueError` raise करता है। [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) के नीचे वाला IETF profile इस grant को confidential clients के लिए आरक्षित रखता है, SEP-990 client से authenticate करने की माँग करता है, और यह SDK shared secret पर ज़ोर देकर दोनों लागू करता है। `token_endpoint_auth_method` तय करता है कि यह कहाँ से होकर जाए: `client_secret_post` (default, form body में) या `client_secret_basic` (HTTP Basic header)। Profile `private_key_jwt` की भी इजाज़त देता है; यह provider उसे support नहीं करता।
!!! tip
`client_secret` को environment या किसी secret manager से पढ़ें, source control से कभी नहीं।
### Provider आपके लिए क्या करता है {#what-the-provider-does-for-you}
पहली request बिना authentication के जाती है, और server का `401` flow शुरू करता है।
1. **Discovery।** यह configure किए गए issuer के [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414) well-known path से authorization server metadata fetch करता है, जाँचता है कि document का `issuer` मेल खाता है, और जाँचता है कि token endpoint issuer के origin पर है।
2. **Assertion।** यह आपके `assertion_provider` को await करता है।
3. **Exchange।** यह token endpoint पर `jwt-bearer` grant POST करता है, `OAuthToken` store करता है, और आपकी मूल request `Authorization: Bearer ...` के साथ दोबारा भेजता है।
जिस `403` का `WWW-Authenticate` `insufficient_scope` बताता है, वह चरण 2 और 3 को आपके `scope` और challenge किए गए scope के union के साथ दोबारा चलाता है। (`scope` हमेशा सिर्फ़ एक माँग है; इस page का authorization server वही देता है जो ID-JAG कहता है, उससे ज़्यादा कुछ नहीं।) इसमें कहीं कोई refresh token नहीं है: access token expire होने पर अगला `401` ताज़ा ID-JAG बनवाता है और फिर exchange करता है, और **यही** वह lever है जो IdP के हाथ में है। नाकामियाँ **[OAuth clients](oauth-clients.md)** के बाकी हिस्से वाले वही दो exceptions हैं: discovery और validation के लिए `OAuthFlowError`, और जब token endpoint मना करे तब उसका subclass `OAuthTokenError`।
## Authorization server {#the-authorization-server}
ज़्यादातर बार आप यहीं रुक जाते हैं। MCP authorization server किसी और का product है, ID-JAGs स्वीकार करना उसकी configuration में चालू करने की चीज़ है, और [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) का SDK वाला आधा हिस्सा ऊपर का client है।
SDK खुद authorization server भी **बन** सकता है: `create_auth_routes` authorization server के routes एक list के रूप में लौटाता है जिसे कोई भी Starlette app mount कर सकता है; repository में `examples/servers/simple-auth/` इसी तरह एक चलाता है। SEP-990 उस surface में एक flag और एक method जोड़ता है:
```python title="auth_server.py" hl_lines="48-50 105-107"
--8<-- "docs_src/identity_assertion/tutorial002.py"
```
* `identity_assertion_enabled=True` सब कुछ gate करता है। बंद होने पर, जो default है, `/token` इस grant का जवाब `unsupported_grant_type` से देता है चाहे आपने hook implement किया हो, और metadata में इसका ज़िक्र नहीं होता। चालू होने पर metadata में `jwt-bearer` grant type जुड़ जाता है और `authorization_grant_profiles_supported` में `urn:ietf:params:oauth:grant-profile:id-jag` सूचीबद्ध हो जाता है; यही वह field है जिससे extension support का ऐलान करता है। (इस SDK का client इसे कभी नहीं पढ़ता: वह एक issuer के लिए provision किया गया है और सीधे माँग लेता है।)
* **`exchange_identity_assertion`** ही hook है। इसके चलने से पहले SDK client को authenticate कर चुका होता है, public clients को मना कर चुका होता है, और उन clients को मना कर चुका होता है जिनके registration में यह grant सूचीबद्ध नहीं है। आपको `IdentityAssertionParams` मिलता है (कच्चा `assertion`, माँगे गए `scopes` और `resource`) और आप सादा `OAuthToken` लौटाते हैं।
* Dynamic client registration इस grant को बिना शर्त मना करता है, इसलिए यहाँ `get_client` हाथ से provision किया गया client देता है। ID-JAG client खुद को register करके अस्तित्व में नहीं ला सकता।
* आधी class इनकारों से भरी है। `OAuthAuthorizationServerProvider` **पूरा** authorization server है, इसलिए वह authorization-code flow भी माँगता है; जो server users को sign in भी कराता है वह उन्हें सच में implement करता है, और इस वाले में ठीक एक ही दरवाज़ा है।
!!! warning
SDK assertion को कभी decode नहीं करता: सिर्फ़ आपके deployment को पता है कि वह किस IdP पर भरोसा करता है
और वह IdP कौन सी keys publish करता है, इसलिए `exchange_identity_assertion` के अंदर की हर चीज़ पर पूरा भार टिका है।
Signature को IdP की published keys (उसकी JWKS; यहाँ वाला shared secret demo का है) से verify करें,
और [RFC 7523](https://datatracker.ietf.org/doc/html/rfc7523) §3 के मुताबिक `iss` और `exp` भी। JWT header का `typ`
`oauth-id-jag+jwt` होना ज़रूरी करें; यह profile का बचाव है ताकि कोई और JWT grant बनाकर replay न किया जा सके।
`aud` आपका अपना issuer हो, यह ज़रूरी करें। ID-JAG का `client_id` claim उसी client के बराबर हो जिसे
handler ने authenticate किया, और उसका `resource` claim किसी ऐसे resource का नाम ले जिसे आप सच में serve करते हैं,
यह ज़रूरी करें। `jti` को assertion के `exp` तक track करें ताकि वह एक ही बार स्वीकार हो। और दिए गए scopes,
और सबसे बढ़कर जारी किए गए token का `resource`, validated ID-JAG से लें, request से कभी नहीं:
`params.resource` वही है जो client ने type किया। Processing के पूरे नियम
[Enterprise-Managed Authorization specification](https://modelcontextprotocol.io/extensions/auth/enterprise-managed-authorization) में हैं।
ख़राब assertion को `TokenError("invalid_grant", ...)` से reject करें। इस flow का दूसरा error code `invalid_target` है: जो ID-JAG किसी ऐसे resource का नाम ले जिसे आप serve नहीं करते, उसे इसी से मना किया जाता है, और यही इस server को किसी और के resource के लिए tokens बनाने से रोकता है। और दिए गए scopes ID-JAG के `scope` claim से आते हैं (जिस assertion में यह न हो उसे भी मना किया जाता है); आपका server शायद इसकी जगह user के groups map करे।
और ध्यान दें कि लौटाए गए `OAuthToken` में क्या नहीं है: refresh token। IdP यह तय करके कि अगला ID-JAG जारी करना है या नहीं, तय करता है कि इस user की पहुँच कब तक बनी रहे। यहाँ बना refresh token वह फ़ैसला चुपचाप वापस सौंप देता।
!!! info
जो server अब भी `auth_server_provider=` से अपना authorization server embed करता है, वह
`AuthSettings(identity_assertion_enabled=True)` के ज़रिए इसी code तक पहुँचता है। **[Authorization](../run/authorization.md)** समझाता है कि नए
servers को वहाँ से शुरू क्यों नहीं करना चाहिए।
!!! check
इस page की दोनों files को आपस में जोड़ दें और पूरा grant बस एक `POST /token` है:
```text
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
assertion=eyJhbGciOiJIUzI1NiIsInR5cCI6Im9hdXRoLWlkLWphZytqd3QifQ...
client_id=finance-agent
resource=http://localhost:8001/mcp
scope=notes:read
client_secret=finance-agent-secret
HTTP/1.1 200 OK
{"access_token": "mcp_...", "token_type": "Bearer", "expires_in": 300, "scope": "notes:read"}
```
न `/authorize`, न `/register`, न protected-resource-metadata fetch। Wire पर सिर्फ़ ये requests हैं:
वह जिस पर `401` आया, well-known fetch, यह exchange, और फिर bearer लगा हुआ साधारण
MCP traffic। और जो `sub` आपके validator ने ID-JAG से पढ़ा, tool के अंदर
`get_access_token().subject` ठीक वही बताता है।
### इसे आज़माएँ {#try-it}
SDK repository में `examples/stories/identity_assertion/` यही page असल में चलता हुआ है: वही `exchange_identity_assertion` validator, उसके tokens पर gate किया गया MCP server, एक stand-in IdP, और client, सब एक self-checking program में। `uv run python -m stories.identity_assertion.client --http` पूरा exchange चलाता है और assert करता है कि जिस user का नाम IdP ने लिया, tool को वही user दिखता है।
## सारांश {#recap}
* [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) यह फ़ैसला end user के बजाय enterprise identity provider को करने देता है कि client किन MCP servers तक पहुँच सकता है। IdP उस फ़ैसले को sign करके **ID-JAG** में डाल देता है।
* ID-JAG हासिल करना **आपके IdP** के सामने [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693) token exchange है, और SDK इसे नहीं करता। उसे MCP authorization server के सामने पेश करना [RFC 7523](https://datatracker.ietf.org/doc/html/rfc7523) `jwt-bearer` grant है, और SDK उसके दोनों पक्ष करता है।
* `IdentityAssertionOAuthProvider` एक और `httpx2.Auth` है: pre-registered confidential client, pin किया गया `issuer`, और एक `assertion_provider(audience, resource)` callback। न browser, न registration, न refresh token।
* Authorization server कभी resource server से discover नहीं होता। `issuer` को ठीक उसी string पर configure करें जो उसका metadata document देता है; तुलना एक-एक character की होती है।
* Server की तरफ़, `identity_assertion_enabled=True` और `exchange_identity_assertion`। SDK client को authenticate करता है और grant को gate करता है; ID-JAG validate करना पूरी तरह आपका काम है, और जारी किया गया token ID-JAG के `resource` से बँधा होता है, request के नहीं।
इकलौता पक्ष जिसे इस page ने कभी नहीं छुआ, वह MCP server है। अभी-अभी बनाए गए token के साथ वह जो करता है, वह **[Authorization](../run/authorization.md)** में पहले से कर ही रहा था।

View file

@ -0,0 +1,217 @@
---
translation:
sections: [ebef1e7a0df854f4, a4c687d3d627d516, 8e79141fc2985342, b345dd05b9c3c7ab, 80ce41579825a6fa, 5f0fa90494de8f65, 83d10514eaa62fa5, 9190555aa39a5d28, 84a4c9d8bf14dddb, 927d71cf40b58c30]
tool: 1
---
# Client {#the-client}
**`Client`** वह ज़रिया है जिससे Python program किसी MCP server से बात करता है।
यह एक object है जिसका एक ही lifecycle है: इसे बनाएँ, `async with` में enter करें, methods call करें। protocol का हर verb (tools की सूची लेना, किसी tool को call करना, resource पढ़ना, prompt render करना) इस पर एक `async` method है जो typed result लौटाता है।
## आपका पहला client {#your-first-client}
```python title="client.py" hl_lines="14-18"
--8<-- "docs_src/client/tutorial001.py"
```
ऊपर वाला server सिर्फ़ इसलिए है ताकि connect करने के लिए कुछ हो। client वे पाँच highlighted lines हैं।
* `Client(mcp)` को **server object ही** दिया गया है। यही in-memory transport है: न subprocess, न port, न HTTP। इस page का हर उदाहरण, और आपका लिखा हर test, इसी तरह connect करता है।
* `async with` ही **lifecycle** है। इसमें enter करते ही connect और negotiate होता है; बाहर निकलते ही disconnect। कोई `connect()` / `close()` जोड़ी नहीं है, और block खत्म होने के बाद `Client` दोबारा इस्तेमाल नहीं हो सकता।
* block के अंदर connection की जानकारी पहले से सादी properties के रूप में मौजूद है।
### `Client` को क्या दे सकते हैं {#what-you-can-pass-to-client}
`Client` एक positional argument लेता है और उसके type से transport तय करता है:
* `MCPServer` (या low-level `Server`) instance: **in-process** connect होता है।
* URL string (`Client("http://localhost:8000/mcp")`): Streamable HTTP, production वाला रास्ता।
* **transport**: कोई भी चीज़ जिसे आप `async with ... as (read, write)` कर सकें, जैसे subprocess को wrap करने वाला `stdio_client(...)`।
इस page की बाकी हर चीज़ तीनों में एक जैसी है। Headers, subprocesses, timeouts और `Transport` protocol का अपना अलग page है: **[Client transports](transports.md)**।
### connected client पर क्या है {#whats-on-a-connected-client}
चार read-only properties, जो block में enter करते ही भर जाती हैं:
* `client.server_info`: server की पहचान, या `None` अगर 2026 पीढ़ी का server इसे report नहीं करता (python-sdk servers default रूप से करते हैं)। यहाँ `server_info.name` `"Bookshop"` है, और `server_info.version` वही है जो server report करता है।
* `client.server_capabilities`: server क्या कर सकता है (`tools`, `resources`, `prompts`, `completions`, ...)। जो capability server के पास नहीं है वह `None` होती है।
* `client.protocol_version`: वह protocol version जिस पर दोनों पक्ष सहमत हुए। यहाँ यह `"2026-07-28"` है।
* `client.instructions`: server की `instructions=` string, या `None` अगर उसने कोई set नहीं की।
आपने कोई protocol version नहीं चुना। default रूप से `Client` server को probe करता है और पुराने servers पर पुराने classic handshake पर लौट आता है, इसलिए एक ही client किसी भी पीढ़ी के server के साथ काम करता है। जब इसे नियंत्रित करने की ज़रूरत हो, पूरी जानकारी **[Protocol versions](../protocol-versions.md)** में है।
!!! tip
`client.session` अंदर का `ClientSession` है, low-level escape hatch।
इस page की किसी भी चीज़ के लिए आपको इसकी ज़रूरत नहीं पड़ेगी।
## tools की सूची लेना {#listing-tools}
```python title="client.py" hl_lines="15-20"
--8<-- "docs_src/client/tutorial002.py"
```
`list_tools()` एक `ListToolsResult` लौटाता है; tools `.tools` में हैं। हर एक वह पूरी definition है जो host किसी model को देगा:
```python
tool.name # 'search_books'
tool.title # 'Search the catalog'
tool.description # 'Search the catalog by title or author.'
```
और `tool.input_schema` वह JSON Schema है जो server ने function के type hints से निकाला:
```json
{
"type": "object",
"properties": {
"query": {"title": "Query", "type": "string"},
"limit": {"default": 10, "title": "Limit", "type": "integer"}
},
"required": ["query"],
"title": "search_booksArguments"
}
```
UI को argument form दिखाने के लिए, और model को valid arguments बनाने के लिए, जो कुछ चाहिए वह सब इसी schema में है।
!!! tip
`title` optional है, इसलिए किसी इंसान को tools दिखाने वाले UI को चुनना पड़ता है: `title` हो तो वही,
नहीं तो `name`। `from mcp.shared.metadata_utils import get_display_name` ठीक यही करता है,
tools, resources, resource templates और prompts के लिए।
## tool call करना {#calling-a-tool}
`call_tool(name, arguments)` tool चलाता है और आपको `CallToolResult` वापस देता है।
```python title="client.py" hl_lines="26-33"
--8<-- "docs_src/client/tutorial003.py"
```
server का `lookup_book` एक Pydantic `Book` लौटाता है। client को यह दिखता है:
```python
result.content # [TextContent(type='text', text='{\n "title": "Dune",\n "author": "Frank Herbert",\n "year": 1965\n}')]
result.structured_content # {'title': 'Dune', 'author': 'Frank Herbert', 'year': 1965}
result.is_error # False
```
एक return value, पढ़ने की तीन चीज़ें। हर एक को पढ़ने वाला अलग है।
### `content`: जो model पढ़ता है {#content-what-the-model-reads}
`content` **content blocks** की एक `list` है, और content block एक union है: `TextContent`, `ImageContent`, `AudioContent`, `ResourceLink`, या `EmbeddedResource`। एक tool कई blocks लौटा सकता है, अलग-अलग तरह के।
इसीलिए `main` `block.text` को छूने से पहले `isinstance(block, TextContent)` से narrow करता है। ध्यान दें कि `isinstance` के बाहर कहीं `.text` नहीं है: type checker इसकी अनुमति नहीं देगा, क्योंकि `ImageContent` में `.data` है, `.text` नहीं। tool आपको क्या भेज सकता है, इस बारे में union ईमानदार है; आपका code भी होना चाहिए।
### `structured_content`: जो आपका application पढ़ता है {#structured_content-what-your-application-reads}
`structured_content` tool की return value JSON के रूप में है, जो tool के declared `output_schema` से मेल खाती है। न string parsing, न अंदाज़ा।
जब दोनों मौजूद हों तो वे जानबूझकर एक ही बात दो बार कहते हैं: `content` model के लिए है, `structured_content` code के लिए। structured वाला हिस्सा कहाँ से आता है, और उसे कैसे नियंत्रित करें, यह **[Structured output](../servers/structured-output.md)** page पर है।
### `is_error`: tool fail हुआ या नहीं {#is_error-whether-the-tool-failed}
जो tool raise करता है वह आपके client में raise **नहीं** होता। वह `is_error=True` के साथ एक साधारण result के रूप में लौटता है।
!!! check
`lookup_book` से `"Solaris"` माँगें (ऐसा title जो catalog में नहीं है) और function
`ValueError` raise करता है। call फिर भी सामान्य रूप से लौटता है:
```python
result.is_error # True
result.content # [TextContent(type='text', text="Error executing tool lookup_book: No book titled 'Solaris' in the catalog.")]
result.structured_content # None
```
exception का message `content` में पहुँचा, जहाँ **model** उसे पढ़कर दोबारा कोशिश कर सकता है। यह
जानबूझकर है: tool error बातचीत का हिस्सा है, crash नहीं। `structured_content` पर भरोसा करने से
पहले हमेशा `is_error` देखें।
!!! warning
`is_error=True` सिर्फ़ आपके अपने `raise` तक सीमित नहीं है। ऐसा tool माँगें जो server के पास है ही नहीं
(`call_tool("does_not_exist", {})`) और कुछ raise नहीं होता। आपको वही shape वापस मिलता है,
`is_error=True` और `content` में `Unknown tool: does_not_exist`। `Client` का कोई method
`MCPError` तभी raise करता है जब server result की जगह JSON-RPC **error** से जवाब दे, और
server कब क्या भेजता है यह **[errors संभालना](../servers/handling-errors.md)** में बताया गया है।
## Resources {#resources}
resource verbs जोड़ियों में आते हैं: सूची लेने के दो तरीके, पढ़ने का एक।
```python title="client.py" hl_lines="22-31"
--8<-- "docs_src/client/tutorial004.py"
```
* `list_resources()` **concrete** resources लौटाता है, जिनका URI तय है। यहाँ: `['catalog://genres']`।
* `list_resource_templates()` **parameterised** वाले लौटाता है। यहाँ: `['catalog://genres/{genre}']`। ये दो अलग सूचियाँ हैं क्योंकि template तब तक पढ़ा नहीं जा सकता जब तक आप उसे भर न दें।
* `read_resource(uri)` एक सादा `str` URI लेता है और दोनों पर काम करता है: `"catalog://genres/poetry"` दें और server उसे template से match कर लेता है।
`read_resource` `contents` लौटाता है, `TextResourceContents` या `BlobResourceContents` की सूची। वही तरीका जो tool content का है: `isinstance` से narrow करें, फिर `.text` (या `.blob`) पढ़ें।
client को यह भी बताया जा सकता है कि कोई resource कब बदला। 2025 पीढ़ी के connections पर यह `subscribe_resource(uri)` / `unsubscribe_resource(uri)` है - methods की ऐसी जोड़ी जिसे `MCPServer` implement नहीं करता, इसलिए 2026-07-28 wire पर (जहाँ ये verbs अब मौजूद नहीं हैं) request का जवाब `-32601`, *Method not found* आता है। 2026 में इसकी जगह `subscriptions/listen` stream है, जिसे `MCPServer` serve **करता है** - वहाँ `server_capabilities.resources.subscribe` `True` है - और उसे `client.listen(...)` से consume करना इस section का **[Subscriptions](subscriptions.md)** page है।
## Prompts {#prompts}
```python title="client.py" hl_lines="15-20"
--8<-- "docs_src/client/tutorial005.py"
```
`list_prompts()` बताता है कि server क्या देता है और हर prompt को क्या चाहिए:
```python
prompt.name # 'recommend'
prompt.title # 'Recommend a book'
prompt.arguments # [PromptArgument(name='genre', required=True)]
```
`get_prompt(name, arguments)` उसे render करता है। arguments dict `str -> str` है: prompt arguments हमेशा strings होते हैं। result `messages` है, `PromptMessage` की सूची, जिनमें हर एक का `role` और एक `content` block है:
```python
message.role # 'user'
message.content # TextContent(type='text', text='Recommend one poetry book from the catalog and say why.')
```
host ये messages सीधे model को दे देता है। पूरा feature बस इतना ही है।
## Completions {#completions}
जिस server में completion handler हो वह user के type करते-करते prompt और resource-template arguments autocomplete कर सकता है।
```python title="client.py" hl_lines="27-31"
--8<-- "docs_src/client/tutorial006.py"
```
* `ref` बताता है कि आप **कौन-सा** prompt या template भर रहे हैं: `PromptReference` या `ResourceTemplateReference`।
* `argument` `{"name": ..., "value": ...}` है: argument और user ने अब तक जो type किया है।
जवाब `result.completion.values` में है। `"p"` type करें और server `['poetry']` लौटाता है। server वाला पक्ष, और handler पहले से भरे **बाकी** arguments का इस्तेमाल अपने सुझाव कम करने के लिए कैसे करता है, यह **[Completions](../servers/completions.md)** page पर है।
## Pagination {#pagination}
हर `list_*` method एक `cursor=` keyword लेता है और हर result में `next_cursor` होता है। जब `next_cursor` `None` हो, आपके पास सब कुछ है।
```python title="client.py" hl_lines="22-30"
--8<-- "docs_src/client/tutorial007.py"
```
यह loop हर server के साथ सही है। `MCPServer` सब कुछ एक ही page में लौटाता है, इसलिए `next_cursor` `None` होता है और loop एक बार चलता है, यही वजह है कि ज़्यादातर code इसे कभी लिखता ही नहीं। जो servers सच में page करते हैं, और cursors जिन नियमों का पालन करते हैं, वे **[Pagination](../advanced/pagination.md)** में हैं।
## tests में {#in-tests}
बिना process और बिना port वाला `Client(mcp)` अपने आप में server के लिए test harness है।
इसी के लिए एक constructor flag बना है: `Client(mcp, raise_exceptions=True)`। इसका असर सिर्फ़ in-memory connections पर होता है, और **[Testing](../get-started/testing.md)** वह page है जो इसे समझाता है और इसके चारों ओर पूरा pattern बनाता है।
## सारांश {#recap}
* `Client(x)` server object से in-memory connect होता है, URL string से Streamable HTTP पर, और बाकी किसी भी चीज़ से transport के ज़रिए।
* `async with` ही पूरा lifecycle है। इसके अंदर `server_capabilities` और `protocol_version` पहले से भरे होते हैं; server दे तो `server_info` और `instructions` भी।
* `list_tools()` आपको हर tool का `name`, `title`, `description` और `input_schema` देता है।
* `call_tool()` model के लिए `content`, आपके code के लिए `structured_content`, और `is_error` लौटाता है। raise करने वाला tool एक result है, exception नहीं।
* `content` block types का union है; पढ़ने से पहले `isinstance` से narrow करें।
* `list_resources` / `list_resource_templates` / `read_resource`, `list_prompts` / `get_prompt`, और `complete` बाकी verbs पूरे करते हैं।
* हर `list_*` `cursor=` लेता है; `next_cursor` के `None` होने तक loop करें।
server *client* से जो चीज़ें माँग सकता है, और आप उनका जवाब कैसे देते हैं, वह **[Client callbacks](callbacks.md)** है।

View file

@ -0,0 +1,152 @@
---
translation:
sections: [c6899d3892bd9fa0, 79372cff3cc48a88, 63878d29e87c3e73, 13175843d3588af4, e7e2b9fd516f77de, 758f06399b513c1f, a05d7278487d610b]
tool: 1
---
# OAuth clients {#oauth-clients}
कुछ MCP servers सुरक्षित होते हैं। उन्हें बिना token के request भेजें तो जवाब `401 Unauthorized` आता है।
token पाने का तरीका **`OAuthClientProvider`** है। यह कोई MCP object है ही नहीं। यह `httpx2.Auth` है, "हर request के साथ कुछ करो" वाला httpx2 का standard hook। इसे `httpx2.AsyncClient` पर लगाएँ, वह client Streamable HTTP transport को दें, और इसके बारे में सोचना बंद कर दें।
यह page client वाला हिस्सा है। अपने server से token की माँग करवाना **[Authorization](../run/authorization.md)** में है।
## Provider {#the-provider}
```python title="client.py" hl_lines="44-54"
--8<-- "docs_src/oauth_clients/tutorial001.py"
```
आप इसे चार चीज़ें देते हैं:
* `server_url`: वह MCP endpoint जिससे आप जुड़ रहे हैं। बाकी सब कुछ provider इसी से खोज लेता है।
* `client_metadata`: वही जो आप किसी authorization server के "register an application" form में भरते।
* `storage`: जहाँ दो runs के बीच tokens रहते हैं।
* `redirect_handler` और `callback_handler`: वे दो पल जिनमें कोई इंसान शामिल होता है।
file में और कहीं OAuth का ज़िक्र नहीं है। `main()` को कभी कोई token दिखता ही नहीं।
### Client metadata {#client-metadata}
`OAuthClientMetadata` असली [RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591) registration document है, Pydantic model के रूप में।
आप तीन fields भरते हैं। बाकी defaults भर देते हैं: `grant_types` पहले से `["authorization_code", "refresh_token"]` है और `response_types` पहले से `["code"]` है, ठीक वही flow जो यह provider चलाता है।
!!! check
चूँकि यह Pydantic model है, यह **network पर एक भी byte जाने से पहले** validate करता है।
`redirect_uris` छोड़ दें तो construction वहीं के वहीं `ValidationError` के साथ fail हो जाता है,
जो field का नाम बताता है:
```text
redirect_uris
Field required [type=missing, input_value={'client_name': 'Bookshop Agent'}, input_type=dict]
```
न कोई browser खुला, न authorization server पर कोई अधूरी registration पीछे छूटी।
### Token storage {#token-storage}
**`TokenStorage`** चार async methods वाला `Protocol` है। आपको किसी से inherit नहीं करना; methods लिख दें और कोई भी class token store बन जाती है:
* `get_tokens` / `set_tokens` `OAuthToken` रखते हैं: access token, refresh token, expiry, scope।
* `get_client_info` / `set_client_info` वह `OAuthClientInformationFull` रखते हैं जो authorization server ने तब जारी किया था जब provider ने आपको register किया, आपके `client_id` समेत।
ऊपर वाला in-memory version काम करता है। यह process के ख़त्म होते ही सब कुछ भूल भी जाता है, इसलिए अगला run पूरी प्रक्रिया फिर से दोहराता है। इसे किसी file या अपने platform के keyring में persist करें और अगला run चुपचाप चल जाता है।
!!! tip
सिर्फ़ tokens नहीं, `client_info` भी store करें। provider पहली बार तब dynamically register करता है
जब उसे कोई stored `client_info` नहीं मिलता। इसे फेंक दें तो हर run पर एक नई registration बनती है।
### दो handlers {#the-two-handlers}
authorization code flow को इंसान की ज़रूरत ठीक एक बार पड़ती है: किसी को sign in करके "allow" पर click करना होता है।
* **`redirect_handler`** को पूरी तरह बने हुए authorization URL के साथ await किया जाता है। `client_id`, `redirect_uri`, `state` और PKCE challenge उसमें पहले से मौजूद हैं। आपका काम बस इतना है कि browser को वहाँ पहुँचाएँ। desktop app `webbrowser.open` call करता है; यह file उसे print कर देती है।
* उसके बाद **`callback_handler`** await होता है। यह तब तक इंतज़ार करता है जब तक user वापस आपके `redirect_uri` पर नहीं पहुँच जाता, और उस redirect के query parameters को `AuthorizationCodeResult` के रूप में लौटाता है।
असली client `input()` call करने के बजाय redirect URI पर एक छोटा local HTTP server चलाता है। आकार बिल्कुल वही है: redirect हों, और `code`, `state` व `iss` वापस दें।
!!! warning
`state` और `iss` को ठीक वैसे ही आगे दें जैसे वे आए थे। provider `state` की तुलना उससे करता है
जो उसने खुद बनाया था, और `iss` की तुलना खोजे गए issuer से, और मेल न खाने पर मना कर देता है।
यही CSRF और server-mix-up से बचाव हैं।
### `Client` में {#into-the-client}
`main()` देखें। provider **httpx2 client** पर जाता है, httpx2 client `streamable_http_client(url, http_client=...)` में जाता है, और वह transport `Client` में जाता है।
`streamable_http_client` में `auth=` keyword नहीं है। HTTP स्तर की हर चीज़ (auth, headers, timeouts, proxies) उस `httpx2.AsyncClient` पर होती है जो आप लाते हैं। यह layering **[Client transports](transports.md)** में है।
## Provider आपके लिए क्या करता है {#what-the-provider-does-for-you}
जब `Client` पहली बार request भेजता है, server `401` लौटाता है। provider कमान संभाल लेता है:
1. **Discovery.** यह `WWW-Authenticate` header पढ़ता है, `/.well-known/oauth-protected-resource` से server का Protected Resource Metadata लाता है, पता करता है कि कौन सा authorization server इस resource की रक्षा करता है, और **उस** server का metadata लाता है।
2. **Registration.** storage में कुछ नहीं है? यह आपके `OAuthClientMetadata` के साथ आपको dynamically register करता है और नतीजा store कर लेता है।
3. **Authorization.** यह PKCE pair और `state` बनाता है, authorization URL तैयार करता है, आपके `redirect_handler` को await करता है, फिर code के लिए आपके `callback_handler` को await करता है।
4. **Exchange.** यह code के बदले `OAuthToken` लेता है, उसे store करता है, और आपकी मूल request को `Authorization: Bearer ...` के साथ दोबारा भेजता है।
उसके बाद यह शांत रहता है। tokens storage से आते हैं, expire हुआ access token refresh token से refresh हो जाता है, और सिर्फ़ तब जब इनमें से कुछ काम नहीं करता, यह flow फिर से चलाता है।
आपने इसमें से कुछ नहीं लिखा। दो keyword arguments बचते हैं (`client_metadata_url` और `validate_resource_url`), और इस file को दोनों में से किसी की ज़रूरत नहीं। `client_metadata_url` जानने लायक है; इसका अपना section नीचे है।
### इसे आज़माएँ {#try-it}
इन docs के ज़्यादातर उदाहरण आप in-memory `Client(server)` से जाँच सकते हैं। यह नहीं: इस flow का पूरा मतलब ही HTTP `401` है, और in-memory client व उसके server के बीच कोई HTTP होता ही नहीं।
repository में live version मौजूद है। `examples/servers/simple-auth/` एक standalone authorization server और एक protected MCP server चलाता है; `examples/clients/simple-auth-client/` इसी page का client है, छोटी CLI में बढ़ा हुआ। उसकी README में दो commands हैं: servers शुरू करें, client को उनके सामने चलाएँ, और चारों चरण अपनी आँखों के सामने होते देखें।
## Client ID Metadata Documents {#client-id-metadata-documents}
spec का 2026-07-28 revision dynamic client registration को **Client ID Metadata Documents** (CIMD) के पक्ष में deprecated कर देता है। जिस भी authorization server से मिले उस पर नई registration POST करने के बजाय, आपका client अपने बारे में एक JSON document किसी स्थिर HTTPS URL पर publish करता है, और वही URL उसका `client_id` **है**। authorization server वह document लाता है; provider उसे कभी छूता तक नहीं।
SDK पहले से इसे समझता है: provider बनाते समय URL को `client_metadata_url=` के रूप में दें। जब authorization server का metadata `client_id_metadata_document_supported: true` बताता है, provider `/register` request को पूरी तरह छोड़ देता है: URL `client_id` बनकर flow में जाता है, और कोई `client_secret` नहीं होता। जब server इसे नहीं बताता (ज़्यादातर अभी नहीं बताते), या आप URL देते ही नहीं, तो provider **चुपचाप** dynamic registration पर लौट आता है, और ऊपर बताया सब कुछ ठीक वैसे ही काम करता है। stored `client_info` अब भी दोनों पर भारी पड़ती है।
URL HTTPS होना चाहिए और उसका path root न हो; इसके अलावा कुछ भी construction पर `ValueError` है, किसी network गतिविधि से पहले। साथ आने वाला `examples/clients/simple-auth-client/` इसे `MCP_CLIENT_METADATA_URL` environment variable के रूप में लेता है।
## Machine to machine {#machine-to-machine}
रात को चलने वाला job, CI का कोई step, कोई दूसरी service। न browser है, न "allow" पर click करने वाला कोई। यही **client credentials** grant है: आपके पास पहले से `client_id` और `client_secret` हैं, और token endpoint ही पूरा flow है।
`ClientCredentialsOAuthProvider` वही `httpx2.Auth` है, बस इंसान के बिना:
```python title="client.py" hl_lines="4 27-33"
--8<-- "docs_src/oauth_clients/tutorial002.py"
```
क्या बदला:
* न `OAuthClientMetadata`, न handlers। आप `client_id` और `client_secret` देते हैं; provider उनके इर्द-गिर्द एक न्यूनतम `client_credentials` registration बनाता है और dynamic registration पूरी तरह छोड़ देता है।
* `scope` space से अलग की गई string है, OAuth का wire format।
* आगे का सब कुछ बिल्कुल वही है: वही `TokenStorage`, वही `httpx2.AsyncClient(auth=...)`, वही `streamable_http_client`।
default रूप से secret token request पर HTTP Basic auth के रूप में जाता है (`client_secret_basic`)। उसे form body में डालने के लिए `token_endpoint_auth_method="client_secret_post"` दें। कुछ authorization servers दोनों में से सिर्फ़ एक ही स्वीकार करते हैं।
!!! tip
`client_secret` को environment या किसी secret manager से पढ़ें, source control से कभी नहीं।
!!! info
एक और provider `mcp.client.auth.extensions.client_credentials` में रहता है:
**`PrivateKeyJWTOAuthProvider`**, उन clients के लिए जो shared secret के बजाय JWT से
authenticate करते हैं (`private_key_jwt`, key-pair और workload-identity वाला रूप)। यह उसी
pattern पर चलता है: एक बनाएँ, `auth=` पर लगाएँ। उसी module में
`SignedJWTParameters` और `static_assertion_provider` भी हैं, दो helpers जो इसका assertion बनाते हैं।
बिना इंसान वाली एक और स्थिति है: client किसी enterprise का है जिसका identity provider, न कि user, तय करता है कि वह किन MCP servers तक पहुँच सकता है। वह अलग grant है, अपने trust model और अपने page के साथ, **[Identity assertion](identity-assertion.md)**।
## जब यह fail होता है {#when-it-fails}
जब OAuth flow में गड़बड़ होती है, provider `mcp.client.auth` से `OAuthFlowError` raise करता है। इसके दो subclasses हैं। `OAuthRegistrationError` का मतलब है कि registration से ऐसा client नहीं मिला जिसे आप इस्तेमाल कर सकें: authorization server ने आपको register करने से मना कर दिया, या register तो किया लेकिन ऐसे credentials के साथ जो यह flow इस्तेमाल नहीं कर सकता (उदाहरण के लिए कोई authentication method जिसे यह implement नहीं करता)। `OAuthTokenError` का मतलब है कि token नहीं मिल सका: token endpoint ने मना कर दिया, या किसी stored client record में ऐसा authentication method है जिसे यह client लागू नहीं कर सकता, जिसकी report token request बनाते समय होती है, भेजी नहीं जाती। एक `except OAuthFlowError:` discovery, registration, authorization और exchange, सबको cover करता है।
हर चीज़ flow error नहीं होती। network अब भी fail हो सकता है; वे साधारण `httpx2` exceptions हैं और बिना छुए आगे निकल जाते हैं।
## सारांश {#recap}
* `OAuthClientProvider` `httpx2.Auth` है। इसे `httpx2.AsyncClient` पर लगाएँ, उसे `streamable_http_client(url, http_client=...)` को दें, और `Client` को कभी पता नहीं चलता कि OAuth हुआ।
* आप चार चीज़ें देते हैं: server URL, एक `OAuthClientMetadata`, एक `TokenStorage`, और redirect/callback handler की जोड़ी।
* `TokenStorage` `Protocol` है: चार async methods, कोई base class नहीं। tokens के साथ-साथ `client_info` भी persist करें।
* discovery, registration (dynamic, या **Client ID Metadata Document** के ज़रिए), PKCE, `state` और `iss` की जाँच, और token refresh provider का काम हैं, आपका नहीं।
* `ClientCredentialsOAuthProvider` बिना इंसान वाला version है: `client_id` + `client_secret`, न handlers, न browser।
* हर OAuth failure `OAuthFlowError` है; `OAuthRegistrationError` और `OAuthTokenError` इसके subclasses हैं।
इस handshake का दूसरा हिस्सा, अपने **server** से token की माँग करवाना, **[Authorization](../run/authorization.md)** में है।

View file

@ -0,0 +1,87 @@
---
translation:
sections: [09c857a25a9dc37a, 43bc6a76a243a50e, 0a716022a88768df, 4b7f78042bfcfff7, c112662e61b03315, 58974ba1f489a8b4, d18adbdbb835ea73]
tool: 1
---
# Session groups {#session-groups}
`Client` सिर्फ़ एक server से जुड़ता है। असली applications को अक्सर कई servers चाहिए होते हैं (search server, database server, कोई internal API) और फिर हर एक के लिए अलग connection और अलग tools की सूची संभालनी पड़ती है।
**`ClientSessionGroup`** एक अकेला object है जिसमें कई connections रहते हैं और जो उन सबकी expose की गई हर चीज़ को एक ही view में मिला देता है।
## दो servers {#two-servers}
दो साधारण servers से शुरू करें। इनका आपस में कोई लेना-देना नहीं है, इसलिए स्वाभाविक रूप से दोनों ने अपने tool का नाम `search` रखा:
```python title="library_server.py" hl_lines="7"
--8<-- "docs_src/session_groups/tutorial001.py"
```
```python title="web_server.py" hl_lines="7"
--8<-- "docs_src/session_groups/tutorial002.py"
```
## एक group {#one-group}
`ClientSessionGroup` बनाएँ और हर server के लिए एक बार **`connect_to_server`** call करें:
```python title="client.py" hl_lines="10-12"
--8<-- "docs_src/session_groups/tutorial003.py"
```
* `connect_to_server` transport parameters लेता है, server object नहीं: subprocess शुरू करने के लिए `StdioServerParameters` (`mcp` से), या पहले से किसी URL पर सुन रहे server के लिए `StreamableHttpParameters` / `SseServerParameters` (`mcp.client.session_group` से)।
* `group.tools` हर जुड़े हुए server के tools का `dict[str, Tool]` है। `group.resources` और `group.prompts` का आकार भी यही है।
* `group.call_tool(name, arguments)` नाम खोजता है, वह session ढूँढता है जिसका यह tool है, और call आगे भेज देता है। आपको कभी बताना नहीं पड़ता कि कौन सा server।
!!! check
`client.py` को दोनों servers के साथ रखें और चलाएँ। दूसरा `connect_to_server` मना कर देता है:
```text
mcp.shared.exceptions.MCPError: {'search'} already exist in group tools.
```
यह `MCPError` है, जो दूसरे server से कुछ भी register होने से पहले raise होता है। नाम **पूरे**
group में unique होना ज़रूरी है, और जिन दो servers पर आपका नियंत्रण नहीं है वे कभी न कभी टकराएँगे ही।
## `component_name_hook` {#component_name_hook}
इसे servers पर नहीं, group पर ठीक किया जाता है। `(name, server_info)` लेने वाला function pass करें, और group उसे हर उस नाम पर चलाता है जिसे वह register करता है:
```python title="client.py" hl_lines="7-8 15"
--8<-- "docs_src/session_groups/tutorial004.py"
```
इसे फिर चलाएँ। `print(sorted(group.tools))` अब दोनों दिखाता है:
```text
['Library.search', 'Web.search']
```
* **key** आपकी है। `by_server` ने इसे `server_info.name` से बनाया, यानी वह नाम जिससे हर `MCPServer(...)` बनाया गया था।
* अंदर का `Tool` जस का तस है: `group.tools["Web.search"].name` अब भी `"search"` है, और `call_tool` wire पर यही नाम भेजता है। prefix आपके process से बाहर कभी नहीं जाता।
* बात सिर्फ़ tools की नहीं है। library का `hours` resource `Library.hours` नाम से register होता है।
!!! tip
hook **हर** server के **हर** नाम पर चलता है, सिर्फ़ टकराव पर नहीं: सिर्फ़-टकराव-पर-prefix
जैसा कोई mode नहीं है। एक scheme चुनें और उसे हर जगह लागू होने दें।
## servers जोड़ना और हटाना {#adding-and-removing-servers}
`connect_to_server` वह `ClientSession` लौटाता है जो उसने खोला। अगर कभी उस server को हटाना हो तो इसे संभालकर रखें: `await group.disconnect_from_server(session)` उसके tools, resources और prompts group से हटा देता है।
अगर आपके पास पहले से जुड़ा हुआ `ClientSession` है (`Client.session` ऐसा ही एक है), तो नया transport खोलने के बजाय उसे `await group.connect_with_session(server_info, session)` को सौंप दें। यह उसी तरह aggregate करता है। group कभी ऐसा session बंद नहीं करता जो उसने खुद नहीं खोला। `server_info` component prefixes के लिए server का नाम देता है; 2026 पीढ़ी के connection पर `client.server_info` `None` हो सकता है (identity वैकल्पिक है), इसलिए उस स्थिति में अपना `Implementation(name=..., version=...)` pass करें।
## Classic handshake {#the-classic-handshake}
`ClientSessionGroup` `Client` पर नहीं, `ClientSession` पर बना है। हर `connect_to_server` classic `initialize` handshake चलाता है। यह **[Protocol versions](../protocol-versions.md)** में बताया गया `server/discover` probe कभी नहीं भेजता। हर MCP server वह handshake समझता है, इसलिए इससे किसी के साथ भी compatibility नहीं खोती; इसका मतलब बस इतना है कि group ऐसे server तक भी पुराने, धीमे रास्ते से पहुँचता है जो बेहतर कर सकता था।
## सारांश {#recap}
* `ClientSessionGroup` कई server connections रखता है और उनके tools, resources और prompts को एक-एक `dict` में मिला देता है।
* हर server के लिए `connect_to_server(params)`। यह transport parameters लेता है, कभी वह server object या URL नहीं जो `Client` लेता है।
* `group.call_tool(name, arguments)` आपके लिए call को उस server तक पहुँचाता है जिसका वह tool है।
* नाम पूरे group में unique होने ज़रूरी हैं; `search` tool वाले दो servers अपने आप साथ नहीं रह सकते।
* `component_name_hook=` हर register किए गए नाम को फिर से लिखता है। dict key बदलती है, wire पर जाने वाला नाम नहीं।
* `connect_with_session` पहले से आपके पास मौजूद session जोड़ता है; `disconnect_from_server` एक session हटाता है।
group कौन सा handshake बोलता है (और `Client` किस तेज़ handshake को तरजीह देता है), यही **[Protocol versions](../protocol-versions.md)** का विषय है।

View file

@ -0,0 +1,91 @@
---
translation:
sections: [8f9558e57f29eee1, a88c587739e0465c, 46ebfd5b325ed041, 4d10b00b57ce4bd9, 2cdb0edd1f59b3e2]
tool: 1
---
# Subscriptions {#subscriptions}
server का catalog स्थिर नहीं होता। tools runtime पर आते हैं, और किसी resource URI के पीछे का content बदलता रहता है। client को इसकी ख़बर `client.listen(...)` से मिलती है: एक `subscriptions/listen` request, जिसका response ही stream **है**। यह खुला रहता है और वे change notifications लाता है जो client ने माँगे थे।
यह page client वाला सिरा है: stream खोलना, उस पर अपने main flow के साथ-साथ नज़र रखना, और उसके ख़त्म होने को संभालना। बदलाव publish करना, filter करना और method को serve करना server की तरफ़ की कहानी है, जो **आपके handler के अंदर** section के **[Subscriptions](../handlers/subscriptions.md)** page में बताई गई है। यहाँ के उदाहरण वहीं बनाए गए sprint-board server से बात करते हैं।
## stream पर नज़र रखना {#watching-the-stream}
subscription बस एक context manager है। इसमें enter करते ही request भेजी जाती है, जिसमें आपके keyword arguments subscription filter बनते हैं, और server के acknowledgment का इंतज़ार होता है, इसलिए block शुरू होने तक stream live हो चुका होता है।
```python title="client.py" hl_lines="15 18 28"
--8<-- "docs_src/subscriptions/tutorial003.py"
```
iterate करने पर चार typed events मिलते हैं: `ToolsListChanged`, `PromptsListChanged`, `ResourcesListChanged`, और `ResourceUpdated(uri=...)`।
event यह बताता है कि **क्या** बदला, कभी यह नहीं कि **कैसे**। इसीलिए `follow_board` `read_resource` और `list_tools` को call करता है: event दोबारा fetch करने का इशारा है। कौन-सा resource बदला, यह मान लेने के बजाय `event.uri` पढ़ें: filter में कई URI हो सकते हैं, और server उनमें से किसी एक के sub-resource पर बदलाव की ख़बर दे सकता है।
consume होने का इंतज़ार कर रहे duplicate events मिलकर एक हो जाते हैं, और दोबारा fetch करने पर आपको फिर भी मौजूदा state ही मिलता है। सिर्फ़ एक जैसे events ही मिलते हैं: अलग-अलग URI के दो `ResourceUpdated` दो events हैं।
handle की दो और properties:
* `sub.honored` वह filter है जिसे server ने acknowledge किया: एक `SubscriptionFilter` जिसमें आपके दिए fields हैं, जिन्हें attributes की तरह पढ़ा जाता है (`sub.honored.prompts_list_changed`)। `MCPServer` आपके माँगे हर kind को honor करता है, इसलिए वह आपकी request ज्यों की त्यों वापस लौटा देता है। कम kinds support करने वाला server कम acknowledge करता है, और honor किया गया kind फिर भी शायद कभी fire न हो। server पूरी request को acknowledge करने के बजाय उसे ठुकरा भी सकता है (server page पर [कौन देख सकता है, यह तय करना](../handlers/subscriptions.md#deciding-who-may-watch) देखें), जो request के error के रूप में सामने आता है।
* `sub.subscription_id` listen request की id है, वही जो इस stream के हर frame पर लगी होती है। एक साथ कई subscriptions खुले हो सकते हैं, और हर एक अपनी id से demultiplex होता है।
## बिना block किए नज़र रखना {#watching-without-blocking}
`follow_board` तब तक चलता है जब तक server stream बंद न कर दे, जो शायद कभी न हो, इसलिए अकेले चलाने पर यह आपके पूरे program पर क़ब्ज़ा कर लेता है। असली clients को watcher main flow के **साथ-साथ** चाहिए: agent tools call करता रहे और watcher cache या UI को ताज़ा रखे।
पहले subscription खोलें, फिर watcher शुरू करें और अपने काम में लग जाएँ।
=== "asyncio"
```python title="app.py" hl_lines="18 20"
--8<-- "docs_src/subscriptions/tutorial004_asyncio.py"
```
=== "trio"
```python title="app.py" hl_lines="18 21"
--8<-- "docs_src/subscriptions/tutorial004_trio.py"
```
=== "anyio"
```python title="app.py" hl_lines="18 21"
--8<-- "docs_src/subscriptions/tutorial004_anyio.py"
```
!!! note
`app.py` पहले उदाहरण से `BOARD` और `read_board` import करता है, जिसे यह repo
`tutorial003.py` नाम से रखता है। अगर आप rendered files को साथ-साथ `client.py` और `app.py` नाम से save करते हैं,
तो इसके बजाय `from client import BOARD, read_board` लिखें। नीचे दिया `watch.py` उदाहरण
भी `read_board` को इसी तरह import करता है।
असली बात क्रम की है। कुछ भी replay नहीं होता, इसलिए stream बनने से पहले publish हुआ event छूट जाता है। `client.listen(...)` में enter करना acknowledgment का इंतज़ार करता है, इसलिए उस पल के बाद का हर बदलाव आपके watcher तक पहुँचता है, और block के अंदर लिया गया snapshot एक भी बदलाव नहीं चूक सकता।
खुले stream के साथ-साथ उसी client पर requests बेरोक चलती हैं, चाहे watcher task से हों या किसी और से। चूँकि consume न हुए **duplicate** events मिलकर एक हो जाते हैं, इसलिए व्यस्त main flow में तीन के बजाय शायद एक ही refetch हो। अलग-अलग events नहीं मिलते: कई URI वाला filter हर URI के लिए एक pending event queue में रखता है।
नज़र रखना बंद करने के लिए block से बाहर निकलें: कोई `unsubscribe` call नहीं है। block वाले task को cancel करने से यह अपने आप हो जाता है, और SDK listen request को उसी तरह cancel करता है जैसा transport चाहता है: Streamable HTTP पर, उस request का stream बंद करके। app के पूरे जीवनकाल तक चलने वाला watcher कभी अपने आप नहीं लौटता, इसलिए shutdown पर उसे, या उसके task group के scope को, cancel करें।
## streams का ख़त्म होना {#streams-end}
stream दो में से किसी एक तरह से ख़त्म होता है, और दोनों साधारण control flow हैं। server का graceful close `async for` को ख़त्म कर देता है; अचानक टूटने पर `SubscriptionLost` raise होता है।
यह फ़र्क़ सिर्फ़ diagnosis के काम का है, आगे क्या करना है उसमें कोई फ़र्क़ नहीं: stream जा चुका है, कुछ replay नहीं हुआ, और जिस watcher को अब भी परवाह है वह दोबारा listen करता है और दोबारा fetch करता है।
```python title="watch.py" hl_lines="16 20"
--8<-- "docs_src/subscriptions/tutorial005.py"
```
servers अपने कारणों से streams को gracefully बंद करते हैं, जिनमें ऐसे subscriber को हटाना भी शामिल है जिसका backlog बहुत बड़ा हो गया हो, इसलिए साफ़ अंत नज़र रखना बंद करने का इशारा नहीं है। दोबारा listen करने से पहले back off करें।
`SubscriptionLost` का एक local कारण भी है। client ज़्यादा से ज़्यादा 1024 बिना consume हुए events रखता है, और जो consumer इतना पीछे रह जाए वह बिना सीमा के बढ़ते जाने के बजाय subscription खो देता है। `async for` की body छोटी रखें और धीमा काम कहीं और करें।
`keep_following` सिर्फ़ `SubscriptionLost` को catch करता है। `listen()` में enter करने पर `MCPError` (connection fail हुआ, या server यह method serve नहीं करता), `TimeoutError` (कोई acknowledgment नहीं आया), और `ListenNotSupportedError` (2026 से पहले का connection) भी raise हो सकते हैं। तय करें कि इनमें से किन पर आपका watcher retry करे: आख़िरी वाला कभी ठीक नहीं होता।
## सारांश {#recap}
* `async with client.listen(...)` में enter करें; enter करना acknowledgment का इंतज़ार करता है, इसलिए उसके बाद publish हुआ कुछ भी नहीं छूटता।
* `async for event in sub` से iterate करें। events दोबारा fetch करने के इशारे हैं, payload कभी नहीं।
* subscription खोलें, फिर watcher को task के रूप में चलाएँ, और tool calls उसके साथ-साथ चलते रहते हैं।
* साफ़ अंत loop को रोक देता है; टूटने पर `SubscriptionLost` raise होता है। दोनों ही हालात में: दोबारा listen करें, दोबारा fetch करें, पहले back off करें।
* block से बाहर निकलना ही unsubscribe है।
इन events को publish करना, filter को सीमित करना, और एक process से आगे scale करना server की कहानी है: **[Subscriptions](../handlers/subscriptions.md)**। यही events client-side cache को भी सही बनाए रखते हैं, और अगला page **[Caching](caching.md)** है।

View file

@ -0,0 +1,128 @@
---
translation:
sections: [9cac816674181eb0, 0700f337babcd4dd, 2bde0dd58cdf00f5, ff7401df479af877, 3d0832f39b0d7059, d4bf7e4479637768, 05e20c0a798860e7]
tool: 1
---
# Client transports {#client-transports}
हर `Client` अपने server से एक **transport** के ज़रिए बात करता है: वही चीज़ जो असल में messages ले जाती है।
आप कभी transport को अलग से configure नहीं करते। `Client` सिर्फ़ एक positional argument लेता है और उसके type से transport तय कर लेता है।
हर transport का **server** वाला पक्ष (`mcp.run()` क्या करता है और आप क्या deploy करते हैं) **[अपना server चलाना](../run/index.md)** में है।
## Memory में {#in-memory}
server object ही पास करें:
```python title="client.py" hl_lines="14"
--8<-- "docs_src/client_transports/tutorial001.py"
```
न कोई subprocess, न कोई port, न wire पर कोई bytes। client और server एक ही process में दो objects हैं, और call फिर भी असली protocol layer से होकर जाती है: `search_books` ठीक वैसे ही list, validate और invoke होता है जैसे HTTP पर होता।
इससे यह एक साथ दो काम करता है:
* **Test harness।** इस documentation का हर उदाहरण इसी तरीके से चलाया जाता है, और **[Testing](../get-started/testing.md)** page पूरा pattern इसी के इर्द-गिर्द बनाता है।
* **Embedding API।** जो application खुद server बनाता है, उसे उसके tools call करने के लिए network hop की ज़रूरत नहीं।
## Streamable HTTP {#streamable-http}
URL string पास करें और आपको **Streamable HTTP** मिलता है, वह transport जिसके पीछे आप deploy करते हैं:
```python title="client.py" hl_lines="5"
--8<-- "docs_src/client_transports/tutorial002.py"
```
पूरा production client बस इतना ही है। `Client` आपके लिए URL को `streamable_http_client(...)` में लपेट देता है, एक `httpx2.AsyncClient` के ऊपर जो MCP की ज़रूरत के हिसाब से configure किया गया है: `follow_redirects=True`, connect/write/pool के लिए 30 सेकंड का timeout, और 300 सेकंड का read timeout, क्योंकि server response stream को खुला रख सकता है।
!!! check
जो `Client` आपने बनाया है वह connected **नहीं** है। बनाने से सिर्फ़ transport चुना जाता है;
उसे खोलता `async with` है। enter करने से पहले connection तक पहुँचने की कोशिश करें तो SDK साफ़ बता देता है:
```text
RuntimeError: Client must be used within an async context manager
```
जब आपने `Client("http://...")` लिखा, तब न कुछ resolve हुआ, न fetch, न spawn। वह line मुफ़्त है।
### अपना `httpx2.AsyncClient` लाएँ {#bring-your-own-httpx2asyncclient}
जैसे ही आपको `Authorization` header, cookie, proxy, mTLS या कोई अलग timeout चाहिए, `httpx2.AsyncClient` खुद बनाएँ और उसे `streamable_http_client` को दें:
```python title="client.py" hl_lines="8-14"
--8<-- "docs_src/client_transports/tutorial003.py"
```
दो बातें ध्यान देने लायक हैं:
* `httpx2.AsyncClient` आपका है, इसलिए उसे enter और exit भी **आप** ही करते हैं। SDK कभी ऐसे client को बंद नहीं करता जो उसने नहीं बनाया।
* `streamable_http_client(url, http_client=...)` एक transport लौटाता है, और `Client(transport)` उसे किसी भी दूसरी चीज़ की तरह स्वीकार करता है।
TLS पर एक बात: `httpx2` certificates को operating system के trust store (
[`truststore`](https://pypi.org/project/truststore/) के ज़रिए) से verify करता है, किसी bundled CA list से नहीं। ऐसे environment में जहाँ
काम का system CA store न हो (कुछ minimal containers), standard `SSL_CERT_FILE`/`SSL_CERT_DIR`
environment variables set करें या अपने `httpx2.AsyncClient` को explicit `verify=ssl_context` पास करें
(पृष्ठभूमि
[`httpx` and `httpx-sse` replaced by `httpx2`](../migration.md#httpx-and-httpx-sse-replaced-by-httpx2) में है)।
!!! warning
`streamable_http_client` पहले `headers=` और `timeout=` सीधे लेता था। अब नहीं लेता:
इसके parameters सिर्फ़ `url`, `http_client` और `terminate_on_close` हैं। आदत से `headers=`
लिख दें तो यह मिलता है:
```text
TypeError: streamable_http_client() got an unexpected keyword argument 'headers'
```
HTTP से जुड़ी हर चीज़ अब उसी एक `httpx2.AsyncClient` पर रहती है जो आप पास करते हैं।
!!! info
`httpx2` जाना-पहचाना `httpx` API ही रखता है, इसलिए अगर आप `httpx` जानते हैं तो यहाँ auth,
proxies, event hooks, retries और connection limits कैसे करने हैं, यह आप पहले से जानते हैं। SDK न ऊपर से कुछ जोड़ता है, न कुछ
हटाता है। OAuth भी यहीं जुड़ता है:
`httpx2.AsyncClient(auth=OAuthClientProvider(...))`। वह पूरा flow **[OAuth clients](oauth-clients.md)** में है।
## stdio {#stdio}
**stdio** server एक subprocess है। client उसे launch करता है, उसके stdin पर JSON-RPC लिखता है और उसके stdout से JSON-RPC पढ़ता है। desktop host आपकी machine पर server इसी तरह चलाता है: host यही code **है**, बस ऊपर एक UI के साथ, और **[असली host से जुड़ें](../get-started/real-host.md)** यही रिश्ता host की तरफ़ से, एक config file के रूप में दिखाता है।
process को `StdioServerParameters` से बताएँ, `stdio_client` से उसे transport में बदलें, और **वही** `Client` को दें:
```python title="client.py" hl_lines="4-8 12"
--8<-- "docs_src/client_transports/tutorial004.py"
```
`Client` अकेले parameters object को स्वीकार नहीं करता। `StdioServerParameters` configuration है; `stdio_client(server)` वह transport है जो उससे process spawn करना जानता है। हमेशा wrap करें।
`async with` block से बाहर निकलने पर subprocess भी बंद हो जाता है: stdin बंद, इंतज़ार, और अटका रहे तो kill। आपको उसे खुद कभी साफ़ नहीं करना पड़ता।
!!! warning
child आपका environment inherit **नहीं** करता। उसे एक minimal allow-list मिलती है (POSIX पर `HOME`, `LOGNAME`,
`PATH`, `SHELL`, `TERM` और `USER`) ताकि ऐसे process में कुछ भी संवेदनशील leak न हो जिसे शायद
आपने लिखा ही न हो।
जिस server को API key चाहिए, उसे वह वहाँ नहीं मिलेगी। उसे `env=` से explicitly पास करें; वे
variables allow-list के ऊपर merge हो जाते हैं। ऊपर `BOOKSHOP_API_KEY` यही कर रहा है।
## SSE {#sse}
`mcp.client.sse` का `sse_client(url)` वह HTTP transport है जिसकी जगह Streamable HTTP ने ली। जो server अब भी इसे बोलता है, उससे बात करने के लिए इसे उसी तरह wrap करें, `Client(sse_client("http://localhost:8000/sse"))`, और इस पर कुछ नया न बनाएँ।
## `Transport` protocol {#the-transport-protocol}
`Client` के लिए ऊपर की सभी चीज़ें एक ही हैं।
**transport** कोई भी async context manager है जो message streams का `(read, write)` जोड़ा yield करता है: औपचारिक रूप से, `mcp.client` का `Transport` protocol। `Client` अपने argument को type से resolve करता है: server object in-process जुड़ता है, `str` `streamable_http_client(url)` बन जाता है, और बाकी सब कुछ सीधे transport के रूप में enter किया जाता है। यही आख़िरी नियम वजह है कि `stdio_client(...)`, `streamable_http_client(...)` और `sse_client(...)` सब उसी एक slot में बैठते हैं, और यही वजह है कि आप अपना खुद का भी लिख सकते हैं।
## सारांश {#recap}
* `Client(mcp)` (server object) memory में जुड़ता है। इसे tests और embedding के लिए इस्तेमाल करें।
* `Client("http://.../mcp")` (URL) Streamable HTTP पर जुड़ता है, जो production transport है।
* Headers, auth, proxies और timeouts उस `httpx2.AsyncClient` पर होने चाहिए जो आप `streamable_http_client(url, http_client=...)` को पास करते हैं। कोई `headers=` keyword नहीं है।
* stdio है `Client(stdio_client(StdioServerParameters(...)))`, अकेला parameters object कभी नहीं।
* subprocess को allow-list वाला environment मिलता है, आपका नहीं; `env=` उसमें जोड़ता है।
* transport वह हर चीज़ है जिस पर आप `async with x as (read, write)` कर सकें। जो कुछ server object या URL नहीं है, `Client` उसे सीधे उसी protocol को सौंप देता है।
* `Client` बनाने से transport चुना जाता है। `async with` उसे खोलता है।
transport खुल जाने के बाद दोनों पक्षों को protocol version पर सहमत होना होता है। आम तौर पर आपको इस बारे में सोचना ही नहीं पड़ता; जब पड़े, तो **[Protocol versions](../protocol-versions.md)** वह page है।