docs: publish translated docs in twelve languages and the tool that maintains them (#3280)
This commit is contained in:
parent
14aa889992
commit
52ad0a8876
6 changed files with 89737 additions and 54 deletions
145
i18n/hi/pages/run/asgi.md
Normal file
145
i18n/hi/pages/run/asgi.md
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
---
|
||||
translation:
|
||||
sections: [1062ef792791488a, 4be2b831547184a9, 374b049e770385f2, b72f6947089e6de0, b172c9db7831bb31, 70b9ece244ca1b0c, cba78e052898c3f6, f06bdb541cb0b469, fb82d526320b7cc3]
|
||||
tool: 1
|
||||
---
|
||||
# मौजूदा app में जोड़ना {#add-to-an-existing-app}
|
||||
|
||||
`mcp.run("streamable-http")` आपके लिए web server शुरू कर देता है। कभी-कभी आप यह नहीं चाहते: आपका MCP server किसी बड़ी web application का एक हिस्सा है, या आपके पास पहले से ASGI deployment है।
|
||||
|
||||
इसके लिए `mcp.streamable_http_app()` एक **Starlette application** लौटाता है।
|
||||
|
||||
Starlette app एक ASGI app है, इसलिए जो कुछ भी ASGI host कर सकता है (uvicorn, Hypercorn, कोई दूसरा Starlette, FastAPI), वह आपका MCP server host कर सकता है।
|
||||
|
||||
## app {#the-app}
|
||||
|
||||
```python title="server.py" hl_lines="12"
|
||||
--8<-- "docs_src/asgi/tutorial001.py"
|
||||
```
|
||||
|
||||
`app` एक साधारण ASGI application है। इसे किसी भी ASGI server को सौंप दें:
|
||||
|
||||
```console
|
||||
uvicorn server:app
|
||||
```
|
||||
|
||||
MCP endpoint `/mcp` पर है, इसलिए client `http://127.0.0.1:8000/mcp` से जुड़ता है।
|
||||
|
||||
app में दो चीज़ें पहले से मौजूद हैं:
|
||||
|
||||
* एक route, `/mcp`: Streamable HTTP endpoint।
|
||||
* एक **lifespan**, जो `mcp.session_manager` को शुरू करता है, यानी वह object जो हर live session के background काम का मालिक है।
|
||||
|
||||
app को अकेले चलाएँ (`uvicorn server:app`) तो आपको दोनों में से किसी के बारे में सोचना नहीं पड़ता।
|
||||
|
||||
!!! tip
|
||||
`streamable_http_app()` वही keyword arguments लेता है जो `mcp.run("streamable-http", ...)`
|
||||
लेता है, बस `port` को छोड़कर: port उसका है जो app को serve करता है। `host` अभी भी स्वीकार
|
||||
होता है लेकिन यहाँ कुछ bind नहीं करता; **[Deploy & scale](deploy.md)** बताता है कि वह असल
|
||||
में क्या नियंत्रित करता है। खुद options की जानकारी **[अपना server चलाना](index.md)** में है।
|
||||
|
||||
`mcp.sse_app()` पुराने पड़ चुके SSE transport के लिए यही करता है।
|
||||
|
||||
## सिर्फ़ localhost, जब तक आप कुछ और न कहें {#localhost-only-until-you-say-otherwise}
|
||||
|
||||
बिना कुछ configure किए app **सिर्फ़** उन्हीं requests का जवाब देता है जो localhost को भेजी गई हों।
|
||||
`streamable_http_app()` यह नहीं जान सकता कि उसे किस hostname के पीछे serve किया जाएगा, इसलिए वह
|
||||
सबसे सुरक्षित allowlist के साथ DNS-rebinding protection चालू कर देता है; आपकी मशीन पर यह बिल्कुल
|
||||
सही है। असली hostname के पीछे deploy होने पर इसका मतलब है कि **हर request `421 Misdirected Request`
|
||||
के साथ reject होती है**, जब तक आप `transport_security=` में वह allowlist नहीं देते जो आप असल में
|
||||
serve करते हैं। आपने जो कुछ बनाया है, उससे पहले पूछा तक नहीं जाता। वह allowlist, और काम करते app
|
||||
से असली hostname तक के बीच की बाकी हर चीज़, **[Deploy & scale](deploy.md)** में है।
|
||||
|
||||
## इसे mount करना {#mounting-it}
|
||||
|
||||
जैसे ही MCP server किसी बड़ी application का **हिस्सा** बनता है, आप app को `Mount` के अंदर रखते हैं। और जैसे ही आप ऐसा करते हैं, lifespan आपकी ज़िम्मेदारी बन जाता है:
|
||||
|
||||
```python title="server.py" hl_lines="18-21 25-26"
|
||||
--8<-- "docs_src/asgi/tutorial002.py"
|
||||
```
|
||||
|
||||
* `Mount("/", ...)` और default `/mcp` path मिलकर endpoint को `/mcp` पर ही रखते हैं। Starlette routes को क्रम से आज़माता है और `Mount("/")` **हर** path से match करता है, इसलिए आपके अपने routes सूची में इससे **पहले** जाते हैं। इसके बाद जो कुछ भी है, वहाँ तक पहुँचा नहीं जा सकता।
|
||||
* `lifespan` function **host** app के पूरे जीवनकाल के लिए `mcp.session_manager.run()` में प्रवेश करता है। यही वह line है जिसे सब भूल जाते हैं।
|
||||
* `mcp.session_manager` तभी मौजूद होता है जब `streamable_http_app()` call हो चुका हो। इसीलिए routes module level पर बनते हैं और manager को सिर्फ़ lifespan के अंदर छुआ जाता है।
|
||||
|
||||
Starlette का `Host` route इसी तरह काम करता है: path के बजाय hostname से route करने के लिए `Mount("/", ...)` की जगह `Host("mcp.example.com", ...)` रखें। lifespan का नियम नहीं बदलता, और transport-security का भी नहीं। `Host("mcp.example.com", ...)` route को सिर्फ़ वही requests मिलती हैं जो उस hostname को भेजी गई हों, लेकिन transport की अपनी Host allowlist (**[Deploy & scale](deploy.md)**) फिर भी पहले चलती है। उसमें `"mcp.example.com"` न हो तो वह route उनमें से हर एक का जवाब `421` से देता है।
|
||||
|
||||
!!! warning "lifespan का मालिक host app है"
|
||||
`streamable_http_app()` जो Starlette लौटाता है, उसके lifespan में `session_manager.run()`
|
||||
जोड़ देता है, लेकिन **mount की गई sub-application का lifespan कभी नहीं चलता**। app को mount
|
||||
करें और वह built-in lifespan dead code बन जाता है। आपके ASGI stack में सबसे ऊपर जो भी app
|
||||
है, उसे अपने lifespan में `mcp.session_manager.run()` में प्रवेश करना होगा।
|
||||
|
||||
!!! check
|
||||
`lifespan=lifespan` वाली line हटाएँ और server शुरू करें। वह शुरू होता है। route resolve
|
||||
होता है। फिर `/mcp` पर पहली request इस error के साथ fail होती है:
|
||||
|
||||
```text
|
||||
RuntimeError: Task group is not initialized. Make sure to use run().
|
||||
```
|
||||
|
||||
session manager को उसके `run()` के अलावा कुछ शुरू नहीं करता।
|
||||
|
||||
## दो servers, एक app {#two-servers-one-app}
|
||||
|
||||
हर `MCPServer` अपने session manager के साथ अपना अलग app है। जितने चाहें mount करें; हर manager में उसी एक host lifespan से प्रवेश करें:
|
||||
|
||||
```python title="server.py" hl_lines="27-30 35-36"
|
||||
--8<-- "docs_src/asgi/tutorial003.py"
|
||||
```
|
||||
|
||||
* `AsyncExitStack` दोनों managers में प्रवेश करता है; वे साथ शुरू होते हैं और उल्टे क्रम में बंद होते हैं।
|
||||
* endpoints `/notes/mcp` और `/tasks/mcp` हैं: mount prefix और default path मिलाकर।
|
||||
|
||||
## path बदलना {#changing-the-path}
|
||||
|
||||
अंत वाला वह `/mcp` ही `streamable_http_path` है। इसे `"/"` पर set करें और mount prefix ही पूरा public path बन जाता है:
|
||||
|
||||
```python title="server.py" hl_lines="25"
|
||||
--8<-- "docs_src/asgi/tutorial004.py"
|
||||
```
|
||||
|
||||
अब clients `/notes` से जुड़ते हैं, `/notes/mcp` से नहीं।
|
||||
|
||||
## browser clients के लिए CORS {#cors-for-browser-clients}
|
||||
|
||||
browser-based client को आपसे दो अनुमतियाँ चाहिए: अपने MCP request headers **भेजने** की, और MCP जो header वापस भेजता है उसे **पढ़ने** की। दोनों host app पर CORS configuration हैं, और ऊपर वाली transport-security allowlist का इससे मेल खाना ज़रूरी है:
|
||||
|
||||
```python title="server.py" hl_lines="27-30 33 35-49"
|
||||
--8<-- "docs_src/asgi/tutorial005.py"
|
||||
```
|
||||
|
||||
* `allow_headers` वह आधा हिस्सा है जिसे सब भूल जाते हैं। browser हर MCP request से पहले **preflight** करता है, क्योंकि `Content-Type: application/json` और `Mcp-*` request headers CORS safelist में नहीं हैं, और जिस header की अनुमति preflight नहीं देता, वह ऐसी request है जिसे browser कभी भेजता ही नहीं। (`allow_headers=["*"]` भी काम करता है: Starlette preflight का जवाब उसी से देता है जो उसने माँगा था।)
|
||||
* `expose_headers=["Mcp-Session-Id"]` पढ़ने वाला आधा हिस्सा है। Streamable HTTP session ID उसी response header में लौटाता है, और जब तक CORS उन्हें नाम से expose न करे, browsers response headers को JavaScript से छिपाते हैं। इसके बिना client अपनी दूसरी request कभी नहीं कर सकता।
|
||||
* `allow_origins` आपका फ़ैसला है, MCP का नहीं। सटीक रहें, और इसे ऊपर `allowed_origins=` में भी दोहराएँ: CORS browser लागू करता है, लेकिन server `Origin` खुद जाँचता है, और जिस origin पर transport भरोसा नहीं करता उसे साफ़ preflight के बाद भी `403` मिलता है।
|
||||
* `allow_methods` उन तीन methods की सूची है जो Streamable HTTP इस्तेमाल करता है: messages भेजने के लिए `POST`, server-to-client stream खोलने के लिए `GET`, session खत्म करने के लिए `DELETE`।
|
||||
|
||||
## custom routes {#custom-routes}
|
||||
|
||||
`@mcp.custom_route()` उसी app पर एक सादा HTTP endpoint register करता है, उन चीज़ों के लिए जो हर deployed service को चाहिए पर जिनका MCP से कोई लेना-देना नहीं: health check, OAuth callback।
|
||||
|
||||
```python title="server.py" hl_lines="15-17"
|
||||
--8<-- "docs_src/asgi/tutorial006.py"
|
||||
```
|
||||
|
||||
* handler सादा Starlette है: `Request` से `Response` तक का एक `async` function।
|
||||
* `streamable_http_app()` हर custom route को उठा लेता है। `app.routes` अब `/mcp` और `/health` है।
|
||||
* `GET /health` का जवाब `{"status": "ok"}` है, जिसमें MCP कहीं नहीं।
|
||||
|
||||
!!! warning
|
||||
custom routes **कभी authenticate नहीं होते**, तब भी जब बाकी server होता है। यह जानबूझकर
|
||||
है: health checks और OAuth callbacks तक किसी token के मौजूद होने से पहले पहुँचा जा सकना
|
||||
ज़रूरी है। इनके पीछे कुछ भी निजी न रखें।
|
||||
|
||||
## सारांश {#recap}
|
||||
|
||||
* `mcp.streamable_http_app()` एक route, `/mcp`, वाला Starlette app लौटाता है। कोई भी ASGI server इसे चला सकता है।
|
||||
* बिना कुछ configure किए app सिर्फ़ localhost को भेजी गई requests का जवाब देता है, और असली hostname के पीछे वह हर चीज़ को `421` से reject करता है, जब तक आप `transport_security=` में allowlist नहीं देते। यह, और production तक का बाकी रास्ता, **[Deploy & scale](deploy.md)** का विषय है।
|
||||
* `Mount` (या `Host`) इसे किसी बड़े Starlette या FastAPI app के अंदर रखता है।
|
||||
* **mount करने से built-in lifespan बंद हो जाता है।** host app के lifespan को `mcp.session_manager.run()` में प्रवेश करना होगा, वरना पहली request fail होती है।
|
||||
* एक app में कई servers का मतलब है कई mounts और एक lifespan जो हर session manager में प्रवेश करता है।
|
||||
* `streamable_http_path="/"` endpoint को खुद mount prefix पर ले जाता है।
|
||||
* browser clients को CORS चाहिए: `Mcp-*` request headers के लिए `allow_headers`, response के लिए `expose_headers=["Mcp-Session-Id"]`।
|
||||
* `@mcp.custom_route()` `/mcp` के बगल में सादे, बिना authentication वाले HTTP endpoints जोड़ता है।
|
||||
|
||||
जब server असली URL पर पहुँच में आ जाए, तो **[Client](../client/index.md)** server object के बजाय उसी URL से उससे जुड़ता है।
|
||||
130
i18n/hi/pages/run/authorization.md
Normal file
130
i18n/hi/pages/run/authorization.md
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
---
|
||||
translation:
|
||||
sections: [d62c13457fc4a534, 80e73abaca6e0652, d1dc4c54cd00ec9c, 14ad3bc7904036bb, 5225f127bc1b9c77, fe1626fdd5aad1da, 4556cb7ea1a04a31]
|
||||
tool: 1
|
||||
---
|
||||
# Authorization {#authorization}
|
||||
|
||||
Streamable HTTP पर आपका MCP server साधारण web service ही है, और आप इसे उसी तरह सुरक्षित करते हैं जैसे किसी भी web service को: OAuth 2.1 bearer tokens से।
|
||||
|
||||
OAuth की भाषा में, आपका server **resource server** है। यह न किसी को sign in कराता है, न कभी कोई token जारी करता है। यह बस एक काम करता है: हर request पर `Authorization` header देखता है और तय करता है कि उसमें रखा token सही है या नहीं।
|
||||
|
||||
यह page server side के बारे में है। जो client आपके authorization server को खोजता है और token लाता है, उसकी जानकारी **[OAuth clients](../client/oauth-clients.md)** में है।
|
||||
|
||||
## तीन पक्ष {#the-three-parties}
|
||||
|
||||
* **authorization server** लोगों को sign in कराता है और access tokens जारी करता है। इसे आप नहीं लिखते। यह आपका identity provider है (Auth0, Keycloak, Entra, या आपका अपना)।
|
||||
* **resource server** आपका MCP server है। यह हर request पर token verify करता है।
|
||||
* **client** पता लगाता है कि आप किस authorization server पर भरोसा करते हैं, उससे token लेता है, और उसे `Authorization: Bearer <token>` के रूप में आपको वापस भेजता है।
|
||||
|
||||
पूरा त्रिकोण बस इतना ही है। इस page पर जो कुछ है, वह बीच वाला bullet है।
|
||||
|
||||
## Token verifier {#a-token-verifier}
|
||||
|
||||
valid token कैसा दिखता है, इस बारे में SDK की कोई राय नहीं है। यह आप बताते हैं, **`TokenVerifier`** implement करके:
|
||||
|
||||
```python title="server.py" hl_lines="12-14 19-24"
|
||||
--8<-- "docs_src/authorization/tutorial001.py"
|
||||
```
|
||||
|
||||
* `TokenVerifier` एक async method वाला protocol है। `verify_token` को `Authorization` header से raw token मिलता है, और token valid हो तो यह **`AccessToken`** लौटाता है, न हो तो `None`। इसके अलावा implement करने को कुछ नहीं है।
|
||||
* यह वाला token को एक table में ढूँढता है। असली verifier JWT signature verify करता है या authorization server के token-introspection endpoint को call करता है। वह code आपका है; SDK उसे सिर्फ़ call करता है।
|
||||
* `token_verifier=` और `auth=` हमेशा साथ चलते हैं। एक को दूसरे के बिना pass करें तो `MCPServer(...)` कोई request serve करने से पहले ही `ValueError` raise कर देता है।
|
||||
|
||||
`AuthSettings` आपके resource server का सार्वजनिक चेहरा है:
|
||||
|
||||
* `issuer_url`: वह authorization server जो आपके tokens जारी करता है।
|
||||
* `resource_server_url`: इस MCP endpoint का public URL। यह बताता है कि token **किस** resource के लिए है, और discovery document भी यहीं रहता है।
|
||||
* `required_scopes`: हर token में ये सभी होने ही चाहिए।
|
||||
|
||||
!!! tip
|
||||
SDK repository में `examples/servers/simple-auth/` के अंदर एक `IntrospectionTokenVerifier` है जो
|
||||
असली authorization server के [RFC 7662](https://datatracker.ietf.org/doc/html/rfc7662) endpoint को call करता है। ज़्यादातर production verifiers का आकार यही होता है।
|
||||
|
||||
## HTTP पर आपको क्या मिलता है {#what-you-get-over-http}
|
||||
|
||||
authorization HTTP headers में रहता है, इसलिए यह सिर्फ़ HTTP transports पर मौजूद है। इसे उसी transport पर चलाएँ जिसे आप deploy करते हैं: `mcp.run(transport="streamable-http")` इसे `http://127.0.0.1:8000/mcp` पर रखता है, और बाकी जानकारी **[अपना server चलाना](index.md)** में है। app के पास अब दो routes हैं:
|
||||
|
||||
```text
|
||||
/mcp
|
||||
/.well-known/oauth-protected-resource/mcp
|
||||
```
|
||||
|
||||
आपने एक tool register किया था। दूसरा route SDK का है।
|
||||
|
||||
### Discovery {#discovery}
|
||||
|
||||
उस well-known path पर `GET` करें और आपको **[RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) Protected Resource Metadata** मिलता है, जो सीधे आपके `AuthSettings` से बना है:
|
||||
|
||||
```json
|
||||
{
|
||||
"resource": "http://127.0.0.1:8000/mcp",
|
||||
"authorization_servers": ["https://auth.example.com/"],
|
||||
"scopes_supported": ["notes:read"],
|
||||
"bearer_methods_supported": ["header"]
|
||||
}
|
||||
```
|
||||
|
||||
जिस client ने आपके server के बारे में कभी सुना भी नहीं, वह इसी document के सहारे अंदर का रास्ता ढूँढता है: वह `authorization_servers` पढ़ता है और token के लिए वहाँ जाता है। इसमें से कुछ भी आपने नहीं लिखा।
|
||||
|
||||
!!! check
|
||||
`/mcp` को बिना token के call करें (या ऐसे token के साथ जिसके लिए आपके verifier ने `None` लौटाया) और request
|
||||
दरवाज़े पर ही रोक दी जाती है:
|
||||
|
||||
```text
|
||||
HTTP/1.1 401 Unauthorized
|
||||
WWW-Authenticate: Bearer error="invalid_token", error_description="Authentication required", resource_metadata="http://127.0.0.1:8000/.well-known/oauth-protected-resource/mcp"
|
||||
|
||||
{"error": "invalid_token", "error_description": "Authentication required"}
|
||||
```
|
||||
|
||||
न कुछ parse हुआ, न कोई tool चला। और `WWW-Authenticate` में जो `resource_metadata` pointer है, वही
|
||||
discovery को automatic बनाता है: 401 -> metadata document -> authorization server -> token -> retry।
|
||||
|
||||
!!! warning
|
||||
इनमें से कुछ भी `stdio` को सुरक्षित नहीं करता। pipe में कोई `Authorization` header नहीं होता, इसलिए वहाँ `token_verifier` से कभी
|
||||
पूछा ही नहीं जाता। `stdio` server की सुरक्षा सीमा वह process है जिसने उसे शुरू किया। यही बात
|
||||
tests में इस्तेमाल होने वाले in-memory `Client(mcp)` पर भी लागू होती है: वह सीधे server object से जुड़ता है
|
||||
और HTTP layer को, authorization समेत, छोड़ देता है।
|
||||
|
||||
## caller की पहचान {#the-callers-identity}
|
||||
|
||||
किसी भी handler के अंदर, **`get_access_token()`** वही `AccessToken` है जो आपके verifier ने मौजूदा request के लिए लौटाया था:
|
||||
|
||||
```python title="server.py" hl_lines="4 32-35"
|
||||
--8<-- "docs_src/authorization/tutorial002.py"
|
||||
```
|
||||
|
||||
* यह tools, resources और prompts में काम करता है, और कुछ इधर-उधर pass करने की ज़रूरत नहीं: auth middleware इसे हर request के लिए एक context variable में रखता है।
|
||||
* आपको **वही object वापस मिलता है जो आपके verifier ने बनाया था**: `client_id`, `scopes`, `subject`, `expires_at`, और जो भी अतिरिक्त `claims` आपने जोड़े। per-tool नियमों के लिए यही hook है: scopes पढ़ें और मना कर दें।
|
||||
* authenticated HTTP request के बाहर यह `None` लौटाता है। in-memory और `stdio` पर यह हमेशा `None` है।
|
||||
|
||||
`Authorization: Bearer alice-token` के साथ `whoami` call करें और model को यह पढ़ने को मिलता है:
|
||||
|
||||
```text
|
||||
alice (scopes: notes:read)
|
||||
```
|
||||
|
||||
## वह आधा हिस्सा जो SDK नहीं करता {#the-half-the-sdk-doesnt-do}
|
||||
|
||||
SDK आपको resource-server वाला आधा हिस्सा देता है: verify करना, advertise करना, मना करना। यह आपको न login page देता है, न consent screen, न token।
|
||||
|
||||
तीनों पक्षों को काम करते देखना हो तो SDK repository से `examples/servers/simple-auth/` चलाएँ (एक छोटा authorization server और ठीक इस page की तरह set up किया गया resource server) और फिर discovery-और-token के पूरे क्रम के लिए `examples/clients/simple-auth-client/` को उसकी ओर point करें।
|
||||
|
||||
!!! info
|
||||
constructor का एक दूसरा argument भी है, `auth_server_provider=`, जो आपके MCP server के अंदर पूरा authorization
|
||||
server embed कर देता है। यह उस AS/RS अलगाव से पहले का है जिसके इर्द-गिर्द MCP authorization spec
|
||||
बना है। नए servers को इसकी ओर हाथ नहीं बढ़ाना चाहिए।
|
||||
|
||||
authorization server, user के consent screen पर click करने की जगह, किसी enterprise identity provider का signed assertion भी स्वीकार कर सकता है, और SDK उस आदान-प्रदान के दोनों पक्षों को support करता है। वह grant, और उसे पेश करने वाला client, **[Identity assertion](../client/identity-assertion.md)** में है।
|
||||
|
||||
## सारांश {#recap}
|
||||
|
||||
* Streamable HTTP पर आपका server OAuth 2.1 **resource server** है: यह tokens verify करता है, जारी कभी नहीं करता।
|
||||
* पूरा integration surface बस `TokenVerifier` है: एक async method, token अंदर, `AccessToken | None` बाहर।
|
||||
* `token_verifier=` और `auth=AuthSettings(issuer_url=..., resource_server_url=..., required_scopes=[...])` हमेशा साथ चलते हैं।
|
||||
* SDK `/.well-known/oauth-protected-resource/...` पर [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) Protected Resource Metadata publish करता है और unauthenticated requests का जवाब 401 से देता है, जिसका `WWW-Authenticate` header उसी की ओर इशारा करता है। discovery की पूरी कहानी बस इतनी ही है।
|
||||
* किसी भी handler में `get_access_token()` बताता है कि call कौन कर रहा है।
|
||||
* authorization HTTP का मामला है। `stdio` और in-memory client इसे कभी नहीं देखते।
|
||||
|
||||
client वाला आधा हिस्सा (आपके authorization server को खोजना और आपके लिए token लाना) **[OAuth clients](../client/oauth-clients.md)** में है। और जो client user से पहचान पूछने के बजाय खुद कोई पहचान **assert** करता है, वह **[Identity assertion](../client/identity-assertion.md)** में है।
|
||||
179
i18n/hi/pages/run/deploy.md
Normal file
179
i18n/hi/pages/run/deploy.md
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
---
|
||||
translation:
|
||||
sections: [28221886b198784f, f88ea1f1614f3a1d, ce926d686730b6d0, 3be24f8ad8bb5ab9, 3fad24032b2224ff, f25a7f860e579ecb, e758745df6fb7b0a]
|
||||
tool: 1
|
||||
---
|
||||
# Deploy और scale करना {#deploy-scale}
|
||||
|
||||
आपका server काम कर रहा है। अब इसे असली hostname चाहिए, और उसके पीछे एक से ज़्यादा worker।
|
||||
|
||||
इसमें से लगभग कुछ भी MCP का काम नहीं है। ASGI server, process manager, load balancer — ये आप लाते हैं। इस page पर उन थोड़ी-सी चीज़ों की छोटी सूची है जो सच में MCP का काम **हैं**: एक setting जो हर deployment का रास्ता रोकती है, और वे दो जगहें जहाँ "एक से ज़्यादा worker" होने पर SDK का व्यवहार बदल जाता है।
|
||||
|
||||
## सबसे पहले: Host allowlist {#before-anything-else-the-host-allowlist}
|
||||
|
||||
`streamable_http_app()` यह नहीं जान सकता कि उसे किस hostname के पीछे serve किया जाएगा, इसलिए वह सबसे सुरक्षित जवाब मान लेता है: localhost। `transport_security=` न दिया हो तो app **DNS-rebinding protection** चालू कर देता है और कोई request तभी स्वीकार करता है जब उसका `Host` header `127.0.0.1:<port>`, `localhost:<port>`, या `[::1]:<port>` हो। `Origin` header, जब मौजूद हो, तो उसी का `http://` रूप होना चाहिए। आपकी मशीन पर यह बिल्कुल सही है: यह किसी दुर्भावनापूर्ण web page को ऐसे DNS नाम के ज़रिए आपका local server चलाने से रोकता है जिसे उसने `127.0.0.1` पर rebind कर दिया हो।
|
||||
|
||||
असली hostname के पीछे deploy होने पर वही default **हर request** को ठुकरा देता है, जब तक आप कुछ और न कहें। यह जाँच MCP से जुड़ी किसी भी चीज़ से पहले चलती है, इसलिए आपने जो बनाया उससे पूछा तक नहीं जाता:
|
||||
|
||||
```text
|
||||
421 Misdirected Request Invalid Host header the Host is not in the allowlist
|
||||
403 Forbidden Invalid Origin header the Origin is not in the allowlist
|
||||
```
|
||||
|
||||
इसका इलाज `transport_security=` है। जो आप सच में serve करते हैं उसे allowlist करें:
|
||||
|
||||
```python title="server.py" hl_lines="2 13-17"
|
||||
--8<-- "docs_src/deploy/tutorial001.py"
|
||||
```
|
||||
|
||||
* `allowed_hosts` की entries हूबहू strings हैं: `"mcp.example.com"` बिना port वाले `Host` header से मेल खाती है और `"mcp.example.com:*"` किसी भी port से। दोनों लिखें।
|
||||
* `allowed_origins` सिर्फ़ browsers के लिए मायने रखती है, क्योंकि और कोई `Origin` नहीं भेजता। यह **[मौजूदा app में जोड़ना](asgi.md)** में बताई गई CORS configuration का server-side जोड़ीदार है।
|
||||
* ऐसे reverse proxy के पीछे जो पहले से `Host` header को नियंत्रित करता है, इस जाँच को बंद कर देना ही ईमानदार configuration है: `TransportSecuritySettings(enable_dns_rebinding_protection=False)`।
|
||||
* localhost से अलग `host=` देना (जैसे `host="mcp.example.com"`) उस hostname को allowlist **नहीं** करता। इससे बस इतना होता है कि localhost वाला default protection चालू नहीं करता, यानी हर Host और Origin स्वीकार हो जाता है। इसके बजाय `transport_security=` से साफ़-साफ़ कहें कि आप क्या चाहते हैं।
|
||||
|
||||
!!! check
|
||||
`transport_security=security` argument हटा दें और app को फिर भी deploy करें। वह शुरू होता है, `/mcp`
|
||||
route होता है, और हर request (सादे `curl` से भेजी गई भी) का यह जवाब आता है:
|
||||
|
||||
```text
|
||||
HTTP/1.1 421 Misdirected Request
|
||||
|
||||
Invalid Host header
|
||||
```
|
||||
|
||||
client की तरफ़ आपको ये शब्द नहीं मिलेंगे। `421` plain-text HTTP response है, JSON-RPC error नहीं,
|
||||
इसलिए MCP client एक सामान्य transport error raise करता है; जो hostname उसे पसंद नहीं आया
|
||||
वह सिर्फ़ **server** के log में दिखता है, एक अकेली warning के रूप में। नया-नया
|
||||
deploy हुआ server जो हर connection ठुकरा रहा हो, उसे Host allowlist की समस्या ही मानें जब तक कुछ और साबित न हो।
|
||||
**[Troubleshooting](../troubleshooting.md)** भी यहीं से शुरू होता है।
|
||||
|
||||
## Workers, और sticky किसे होना है {#workers-and-who-has-to-be-sticky}
|
||||
|
||||
जब hostname जवाब देने लगे, तो उसके पीछे एक से ज़्यादा worker लगाएँ। इसके लिए SDK में कोई knob नहीं है; Starlette app को वैसे ही scale किया जाता है जैसे किसी भी ASGI app को, object किसी ऐसी चीज़ को सौंपकर जो fork करना जानती है:
|
||||
|
||||
```console
|
||||
uvicorn server:app --workers 4
|
||||
```
|
||||
|
||||
चार processes, एक socket। और अब वह सवाल जिसका जवाब हर deployment को देना होता है: **क्या किसी request का उसी worker तक पहुँचना ज़रूरी है जिसने पिछली request देखी थी?**
|
||||
|
||||
**2026-07-28** protocol बोलने वाले client के लिए, नहीं। modern request अपने आप में पूरी एक POST है: उससे पहले कोई `initialize` handshake नहीं, response पर कोई `Mcp-Session-Id` नहीं, ऐसा कुछ भी नहीं जिस पर दूसरी request को लौटकर आना पड़े। इसे किसी भी worker को भेज दें।
|
||||
|
||||
यह कोई ऐसा mode नहीं जिसे आप चालू करते हैं। `stateless_http=True` देखने में ऐसा लगता है, लेकिन transport `MCP-Protocol-Version` request header देखकर route करता है, modern request को modern handler को सौंपता है, और **return कर जाता है**। `stateless_http` पढ़ने वाली line उस return के **बाद** आती है। ऐसा नहीं कि 2026-07-28 path पर flag अनदेखा होता है; वहाँ तक पहुँचा ही नहीं जाता। `stateless_http` सिर्फ़ **legacy** हिस्से का knob है, और modern path बनावट से ही sessionless है।
|
||||
|
||||
spec version 2025-11-25 या उससे पहले वाले legacy client के लिए जवाब उस flag पर निर्भर करता है:
|
||||
|
||||
| client का protocol version | Session | load balancer को क्या करना होगा |
|
||||
| --- | --- | --- |
|
||||
| **2026-07-28** | कोई नहीं। `Mcp-Session-Id` कभी set नहीं होता। | कुछ नहीं। कोई भी worker कोई भी request serve करता है। |
|
||||
| **2025-11-25 और पहले** (default) | `Mcp-Session-Id`, एक worker की memory में रखा हुआ। | **Sticky sessions।** कोई अगली request जो दूसरे worker तक पहुँचे उसे `404` *"Session not found"* मिलता है। |
|
||||
| **2025-11-25 और पहले**, `stateless_http=True` के साथ | कोई नहीं। | कुछ नहीं। कीमत है server से client वाला back-channel (sampling, push elicitation, `roots/list`) और resumability। |
|
||||
|
||||
Sticky sessions और legacy हिस्से की कीमत का अपना अलग page है, **[legacy clients को serve करना](legacy-clients.md)**; दोनों पीढ़ियाँ खुद **[Protocol versions](../protocol-versions.md)** में हैं। यहाँ मायने रखता है जवाब का आकार: **2026-07-28 पर आप पहले से stateless हैं, configure करने को कुछ नहीं।**
|
||||
|
||||
इस page का बाकी हिस्सा उन दो चीज़ों के बारे में है जो stateless होने से आपको **नहीं** मिलतीं।
|
||||
|
||||
## अलग-अलग workers के बीच `requestState` {#requeststate-across-workers}
|
||||
|
||||
**[multi-round-trip](../handlers/multi-round-trip.md)** tool को कुछ ऐसा चाहिए होता है जो client को जाकर लाना पड़ता है (एक confirmation, एक चुनाव, एक credential), इसलिए वह जवाब की जगह सवाल लौटाता है और retry पर काम पूरा करता है। दोनों rounds के बीच client के पास एक opaque `request_state` token होता है जिसे server ने बनाया था। retry पर server को वह token फिर से खोलना होता है।
|
||||
|
||||
**किस key से seal किया गया?** default रूप से, उस key से जो server ने construction के समय `os.urandom(32)` से बनाई थी। `--workers 4` में यह चार constructions हैं, चार processes में: चार अलग-अलग keys, कहीं लिखी नहीं गईं, कभी साझा नहीं हुईं, restart पर गायब।
|
||||
|
||||
यह रहा एक tool जो कुछ करने से पहले पूछता है, ऐसे server पर जो कुछ भी configure नहीं करता:
|
||||
|
||||
```python title="server.py" hl_lines="14 20"
|
||||
--8<-- "docs_src/deploy/tutorial002.py"
|
||||
```
|
||||
|
||||
पहला round worker A तक पहुँचता है। worker A `refund:120` को **अपनी** key से seal करता है और token लौटाता है। client सवाल किसी इंसान के सामने रखता है, हाँ पाता है, और retry करता है। यह retry बिल्कुल नई HTTP request है।
|
||||
|
||||
!!! check
|
||||
मान लें वह retry worker B तक पहुँचती है। B ऐसे token को unseal करने की कोशिश करता है जो उसने नहीं बनाया, कर नहीं पाता, और
|
||||
पूरा round ठुकरा देता है। `refund` कभी call नहीं होता; client को JSON-RPC error मिलता है:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": -32602,
|
||||
"message": "Invalid or expired requestState",
|
||||
"data": {"reason": "invalid_request_state"}
|
||||
}
|
||||
```
|
||||
|
||||
यह message **कभी नहीं बदलता**। Expired हो, छेड़छाड़ हुई हो, अलग arguments के साथ replay किया गया हो, या
|
||||
(असली deployment में सबसे आम कारण) किसी सहोदर worker ने seal किया हो: client को
|
||||
हर बार यही बताया जाता है, इसलिए wire पर कभी पता नहीं चलता कि कौन-सी जाँच fail हुई। असली कारण
|
||||
server के log में एक `WARNING` है:
|
||||
|
||||
```text
|
||||
requestState rejected on tools/call: unknown key
|
||||
```
|
||||
|
||||
जो multi-round-trip tool एक worker पर चलता था और दो पर **कभी-कभी** fail होने लगा,
|
||||
उसकी वजह यही है। दोनों rounds को अब भी एक ही process तक पहुँचना होता है, इसलिए यह ठीक उतनी बार fail होता है जितनी बार
|
||||
आपका load balancer उन्हें अलग कर देता है।
|
||||
|
||||
दोनों rounds दो स्वतंत्र HTTP requests हैं, और कई आम चीज़ें उन्हें अलग कर देती हैं: हर request पर balance करने वाला proxy, बीच में टूट गया connection, कोई deploy या restart, ऐसा client जिसने `request_state` सहेज रखा था और अब बिल्कुल अलग process से resume कर रहा है (**[Loop खुद चलाना](../handlers/multi-round-trip.md#driving-the-loop-yourself)**)। इनमें से कोई भी "एक अलग worker" है।
|
||||
|
||||
इलाज एक argument है। उसके **दो** हिस्से हैं।
|
||||
|
||||
```python title="server.py" hl_lines="1 12 14"
|
||||
--8<-- "docs_src/deploy/tutorial003.py"
|
||||
```
|
||||
|
||||
* **`keys=[...]`** वह हिस्सा है जो सबको मिल जाता है। हर instance को एक ही secret दें (कम से कम 32 bytes का), और हर instance वह unseal कर सकता है जो किसी भी सहोदर ने बनाया। `keys[0]` seal करती है और सूची की हर key unseal करती है, यही rotation ring है; इसे बिना downtime के कैसे घुमाएँ, यह **[Keys rotate करना](../handlers/multi-round-trip.md#rotating-keys)** में है।
|
||||
* **server का नाम** वह हिस्सा है जो लगभग किसी को नहीं मिलता, और यही कारण है कि key साझा करने के बाद भी cross-instance retries fail होती रहती हैं। हर sealed token में server का `name` एक **audience claim** के रूप में होता है, जिसे वापसी पर सख़्ती से जाँचा जाता है। एक ही code से बने दो instances का नाम एक ही होता है और उन्हें इसका कभी पता भी नहीं चलता। उन्हें अलग-अलग नाम दें (`MCPServer(f"billing-{POD}")` अच्छी observability आदत जैसा लगता है), और हर cross-instance retry ठीक ऊपर की तरह ठुकरा दी जाती है, key साझा हो या न हो। log में `unknown key` की जगह `audience` लिखा आता है; client को फ़र्क़ पता नहीं चलता।
|
||||
|
||||
secret एक बार बनाएँ और वही value हर instance को दें। अगर आप 32 bytes से कम देते हैं तो SDK का अपना error message यही command चलाने को कहता है:
|
||||
|
||||
```console
|
||||
python -c "import secrets; print(secrets.token_hex(32))"
|
||||
```
|
||||
|
||||
!!! warning "वही keys, **और** वही नाम"
|
||||
multi-instance deployment को दोनों साझा करने ही होंगे। अगर हर instance का अलग नाम आपके लिए ज़रूरी है,
|
||||
तो इसके बजाय पूरे fleet को एक स्पष्ट audience दें: `RequestStateSecurity(keys=[...], audience="billing")`।
|
||||
फिर हर instance `"billing"` के तहत बनाता और स्वीकार करता है, चाहे उसका नाम कुछ भी हो।
|
||||
|
||||
seal के बारे में बाकी सब कुछ **[`requestState` की सुरक्षा](../handlers/multi-round-trip.md#protecting-requeststate)** में है: यह क्या-क्या bind करता है, हर round का `ttl` (default रूप से 600 seconds), अपना codec लाना, और बिना configure किया default `stdio` पर बिल्कुल सही क्यों है। इस page का पूरा योगदान दो बातों की checklist है: **वही keys, वही नाम।**
|
||||
|
||||
!!! info
|
||||
भले ही आपने कभी `InputRequiredResult` न लिखा हो, आप इसी path पर हैं। जिस tool के parameters
|
||||
`Resolve(...)` इस्तेमाल करते हैं (**[Dependencies](../handlers/dependencies.md)**) वह multi-round-trip tool है,
|
||||
और SDK उसके लिए उसका `request_state` बनाता और seal करता है। वही default key, workers के बीच वही
|
||||
failure, वही इलाज।
|
||||
|
||||
## अलग-अलग replicas के बीच change notifications {#change-notifications-across-replicas}
|
||||
|
||||
client की `subscriptions/listen` stream एक लंबे समय तक चलने वाला response है, इसलिए वह अपनी पूरी ज़िंदगी एक replica से बँधी रहती है। किसी **दूसरे** replica पर publish हुआ `ctx.notify_resource_updated(...)` उस तक पहुँचना चाहिए।
|
||||
|
||||
दोनों के बीच का जोड़ `SubscriptionBus` है। आप server को जो भी bus देते हैं, हर publish उसी में जाता है और हर खुली stream उसी को सुनती है, इसलिए हर replica को वही bus दें:
|
||||
|
||||
```python title="server.py" hl_lines="2 7 9"
|
||||
--8<-- "docs_src/deploy/tutorial004.py"
|
||||
```
|
||||
|
||||
fan-out को इससे कोई मतलब नहीं कि stream किस server object से जुड़ी है। एक ही `InMemorySubscriptionBus` रखने वाले दो servers पहले से ऐसे ही बर्ताव करते हैं: एक पर listen stream खोलें, दूसरे पर `edit_note` चलाएँ, और stream को इसकी ख़बर मिल जाती है। वह in-memory bus सिर्फ़ एक process के अंदर के server objects तक फैलता है, इसलिए यह model है, deployment नहीं:
|
||||
|
||||
* असली processes के बीच, **SDK में ऐसा कोई bus नहीं आता जो आपकी मदद कर सके।** `SubscriptionBus` दो methods वाला `Protocol` है (`publish` और `subscribe`) जिसे आप अपने pub/sub backend (Redis, NATS, जो भी आप पहले से चलाते हैं) के ऊपर implement करते हैं और `MCPServer(subscriptions=...)` के रूप में देते हैं। sketch और contract **[Subscriptions](../handlers/subscriptions.md#scaling-past-one-process)** में हैं।
|
||||
* bus चार छोटे typed events ढोता है, JSON-RPC कभी नहीं। Acknowledgment, filtering, और stream lifecycle SDK में ही रहते हैं, इसलिए आपका bus protocol तोड़ नहीं सकता; वह सिर्फ़ events को processes के बीच ले जा सकता है।
|
||||
* Streams resumable **नहीं** हैं और events replay **नहीं** होते। कोई replica खो जाए तो उसकी streams गिर जाती हैं; clients फिर से listen और फिर से fetch करते हैं। साझा करने को कोई event store नहीं और configure करने को और कुछ नहीं। यह वह एक जगह है जहाँ scale out करना सच में बस वही चीज़ और ज़्यादा है।
|
||||
|
||||
## SDK आपको क्या नहीं देता {#what-the-sdk-does-not-give-you}
|
||||
|
||||
`MCPServer` एक protocol implementation है, application server नहीं। जिन deployment knobs को आप आगे ढूँढने जाएँगे वे जान-बूझकर नहीं हैं:
|
||||
|
||||
* **कोई `workers=` नहीं।** `mcp.run("streamable-http")` ठीक एक uvicorn process शुरू करता है, और वह कभी बस इतना ही शुरू करेगा। Multi-process का मतलब है `streamable_http_app()` को उसी चीज़ को सौंपना जिससे आप पहले से ASGI deploy करते हैं: `uvicorn --workers`, gunicorn, आपके platform का process manager। यह page जान-बूझकर इनमें से किसी का tutorial नहीं है; उनका documentation यहाँ उसकी नकल से बेहतर है।
|
||||
* **कोई health-check route नहीं।** `@mcp.custom_route("/health", methods=["GET"])` ही पूरा जवाब है, और इस पर कभी authentication नहीं लगता, तब भी नहीं जब बाकी server पर लगा हो। liveness probe के लिए यह सही है, किसी भी निजी चीज़ के लिए गलत। **[मौजूदा app में जोड़ना](asgi.md#custom-routes)** में एक उदाहरण है।
|
||||
* **कोई production settings object नहीं।** `MCPServer` पर timeouts, TLS, graceful shutdown, या connection limits लिखने की कोई जगह नहीं है, क्योंकि इनमें से कोई भी उसका काम नहीं। ये आपके ASGI server के हैं, और आप उन्हें वहीं configure करते हैं। constructor जो गिनी-चुनी settings **लेता है**, वे **[अपना server चलाना](index.md)** में हैं।
|
||||
* **कोई `EventStore` साथ नहीं आता, और 2026-07-28 पर उसका कोई काम भी नहीं।** Resumability legacy stateful हिस्से की feature है; modern exchange एक POST, एक response है, और resume करने को कुछ नहीं।
|
||||
|
||||
## सारांश {#recap}
|
||||
|
||||
* बिना कुछ configure किए app सिर्फ़ उन्हीं requests का जवाब देता है जो localhost को भेजी गई हों। `transport_security=TransportSecuritySettings(allowed_hosts=[...], allowed_origins=[...])` ही go-live gate है: जब तक आप इसे नहीं देते, असली hostname के पीछे हर request `421` है और कारण सिर्फ़ server के log में है।
|
||||
* 2026-07-28 पर कोई session नहीं है और load balancer के sticky होने के लिए कुछ नहीं। `stateless_http=True` सिर्फ़ legacy का knob है क्योंकि modern request उस flag के पढ़े जाने से पहले ही route होकर जवाब पा लेती है।
|
||||
* default `requestState` key `os.urandom(32)` है, हर process में अलग बनी हुई। कोई multi-round-trip retry जो दूसरे worker तक पहुँचे, `-32602` *"Invalid or expired requestState"* के साथ fail होती है।
|
||||
* इलाज है `RequestStateSecurity(keys=[...])` **और** हर instance पर एक ही server नाम। नाम ही token का default audience claim है। वही keys, वही नाम।
|
||||
* Change notifications एक साझा `SubscriptionBus` के ज़रिए replicas के पार जाते हैं। SDK का एकमात्र implementation in-process है; अपने pub/sub के ऊपर दो methods वाला `Protocol` आपको खुद लिखना है।
|
||||
* कोई `workers=` नहीं, कोई health route नहीं, कोई production settings object नहीं। अपना ASGI server खुद लाएँ।
|
||||
|
||||
असली hostname के सामने जो दूसरी चीज़ चाहिए वह है token: **[Authorization](authorization.md)**।
|
||||
156
i18n/hi/pages/run/index.md
Normal file
156
i18n/hi/pages/run/index.md
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
---
|
||||
translation:
|
||||
sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 7a53ead3e704a7f0, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0]
|
||||
tool: 1
|
||||
---
|
||||
# अपना server चलाना {#running-your-server}
|
||||
|
||||
`mcp.run()` server को शुरू करता है।
|
||||
|
||||
आपको सिर्फ़ एक फ़ैसला करना है: **transport** कौन सा हो, यानी server और उसके client के बीच bytes असल में कैसे आएँ-जाएँ।
|
||||
|
||||
## Transport चुनना {#pick-a-transport}
|
||||
|
||||
| Transport | यह क्या है | कब |
|
||||
|---|---|---|
|
||||
| `stdio` | Host आपकी file को subprocess के रूप में launch करता है और उसके stdin और stdout पर बात करता है। | Local servers के लिए। यही default है। |
|
||||
| `streamable-http` | Port पर सुनने वाला असली HTTP server। | जो कुछ भी आप deploy करें। |
|
||||
| `sse` | पुराना HTTP transport। | कभी नहीं। |
|
||||
|
||||
!!! warning
|
||||
2025-03-26 protocol revision में SSE की जगह Streamable HTTP ने ले ली।
|
||||
`mcp.run(transport="sse")` अब भी काम करता है, अपने `sse_path=` और `message_path=`
|
||||
options के साथ, लेकिन यह सिर्फ़ उन clients के लिए है जो अभी तक नहीं बदले। इस पर कुछ नया न बनाएँ।
|
||||
|
||||
## `mcp.run()` {#mcprun}
|
||||
|
||||
```python title="server.py" hl_lines="12-13"
|
||||
--8<-- "docs_src/run/tutorial001.py"
|
||||
```
|
||||
|
||||
* `run()` synchronous है। यह server के पूरे जीवनकाल तक block करता है।
|
||||
* बिना argument के transport `stdio` होता है।
|
||||
* यह `if __name__ == "__main__":` के नीचे इसलिए है क्योंकि server को load करने वाली हर चीज़ (`mcp dev`, `mcp run`, `mcp install`, आपके tests) इस file को **import** करती है। यह guard import को चलते हुए server में बदलने से रोकता है।
|
||||
|
||||
### stdio {#stdio}
|
||||
|
||||
Configure करने को कुछ नहीं है। Host आपकी file को child process के रूप में शुरू करता है, उसके stdin पर requests लिखता है, और उसके stdout से responses पढ़ता है।
|
||||
|
||||
इसे खुद चलाएँ और नतीजा दिखेगा:
|
||||
|
||||
```console
|
||||
python server.py
|
||||
```
|
||||
|
||||
कुछ print नहीं होता, और यह लौटता भी नहीं। यह stdin पर इंतज़ार कर रहा है कि कोई host पहले बोले।
|
||||
|
||||
इसका मतलब यह भी है कि stdout **ही wire है**। Serve करते समय SDK wire को एक private descriptor पर ले जाता है और जो output stdout पर **flush** होता है (कोई subprocess जो अपने inherited stdout पर लिखता है, flush किया गया `print()`), उसे stderr पर मोड़ देता है, जहाँ वह stream को बिगाड़ नहीं सकता। Serving शुरू होने से **पहले** stdout पर flush हुआ output (कोई wrapper script जो echo करे, import के समय का unbuffered print) अब भी wire पर पहुँचता है, और वैसा `print()` भी जो तब तक buffered रहता है जब तक interpreter exit पर उसे drain नहीं कर देता। जो output आप सच में चाहते हैं, उसके लिए `logging` module सही तरीका है: उसका handler हर record को उसी समय stderr पर flush करता है। वह पूरी जानकारी **[Logging](../handlers/logging.md)** में है।
|
||||
|
||||
### इसे आज़माएँ {#try-it}
|
||||
|
||||
```console
|
||||
uv run mcp dev server.py
|
||||
```
|
||||
|
||||
Inspector ठीक वही करता है जो असली host करता है: यह `server.py` को subprocess के रूप में launch करता है और stdio पर उससे जुड़ता है।
|
||||
|
||||
आपने इसे कभी port नहीं दिया। कोई port है ही नहीं।
|
||||
|
||||
## Streamable HTTP {#streamable-http}
|
||||
|
||||
इसी server को port पर रखने के लिए `run()` में transport (और उसके options) का नाम दें:
|
||||
|
||||
```python title="server.py" hl_lines="13"
|
||||
--8<-- "docs_src/run/tutorial002.py"
|
||||
```
|
||||
|
||||
वह एक line Starlette app बनाती है और उसे uvicorn से serve करती है। Clients `http://127.0.0.1:3001/mcp` से जुड़ते हैं।
|
||||
|
||||
हर transport के अपने keyword arguments हैं, सब `run()` पर:
|
||||
|
||||
* `host` / `port`: कहाँ सुनना है। Default `127.0.0.1` और `8000`।
|
||||
* `streamable_http_path`: MCP endpoint कहाँ रहता है। Default `/mcp`।
|
||||
* `json_response=True`: हर POST का जवाब SSE stream के बजाय एक अकेली JSON body से देना। उस body में सिर्फ़ response की जगह है, और कुछ नहीं, इसलिए जो tool request के बीच में client को वापस call करता है (`ctx.elicit()`, sampling), वह इस leg पर `NoBackChannelError` raise करता है, और चल रही call से जुड़े notifications (`ctx.report_progress()` का progress, per-call log messages) छोड़ दिए जाते हैं; standalone `GET` stream असंबंधित notifications अब भी ले जाती है।
|
||||
* `stateless_http=True`: हर request के लिए नया transport, कोई session tracking नहीं।
|
||||
* `max_request_body_size`: स्वीकार की जाने वाली सबसे बड़ी POST body, bytes में। Default 4 MiB है; इससे बड़ी requests
|
||||
को parsing या session बनने से पहले ही HTTP 413 मिलता है। इसे तभी बढ़ाएँ जब जायज़ MCP messages
|
||||
उस आकार से बड़े हों।
|
||||
* `event_store`, `retry_interval`, `transport_security`: resumability और DNS-rebinding से सुरक्षा। ये इंतज़ार कर सकते हैं, जब तक आप localhost के अलावा कहीं deploy न करें; `transport_security` की जानकारी **[Deploy & scale](deploy.md)** में है।
|
||||
|
||||
!!! warning
|
||||
Transport options `run()` को जाते हैं, `MCPServer(...)` को **नहीं**। Constructor बताता है कि
|
||||
आपका server **क्या है**: name, version, instructions. `run()` बताता है कि वह कैसे serve होता है। इसे
|
||||
उल्टा करेंगे तो MCP के शामिल होने से पहले ही Python जवाब दे देता है:
|
||||
|
||||
```text
|
||||
TypeError: MCPServer.__init__() got an unexpected keyword argument 'port'
|
||||
```
|
||||
|
||||
`run()` छोटा रास्ता है। जैसे ही आपको इससे ज़्यादा चाहिए (server किसी मौजूदा app के अंदर mount हो, एक process में दो servers, browser clients के लिए CORS), आप ASGI app खुद बनाते हैं और उसे किसी भी ASGI host को सौंप देते हैं। वह **[मौजूदा app में जोड़ना](asgi.md)** है।
|
||||
|
||||
## Server settings {#server-settings}
|
||||
|
||||
चलाने से जुड़ी कुछ चीज़ें transport के बारे में नहीं हैं। वे constructor arguments हैं:
|
||||
|
||||
```python title="server.py" hl_lines="3"
|
||||
--8<-- "docs_src/run/tutorial003.py"
|
||||
```
|
||||
|
||||
* `log_level`: `MCPServer(...)` बनते ही `logging.basicConfig()` को दे दिया जाता है। यह **root** logger को configure करता है, इसलिए यह सिर्फ़ SDK के नहीं, आपके अपने loggers का level भी तय करता है। Default `"INFO"`।
|
||||
* `debug`: उस Starlette app को आगे भेजा जाता है जिसे HTTP transports बनाते हैं। Default `False`।
|
||||
|
||||
दोनों `mcp.settings` पर पहुँचते हैं, जिसे आप runtime पर पढ़ सकते हैं।
|
||||
|
||||
## `mcp` command {#the-mcp-command}
|
||||
|
||||
`[cli]` extra इन सबके इर्द-गिर्द एक छोटा command-line tool install करता है।
|
||||
|
||||
`mcp dev` आपके server को **MCP Inspector** के नीचे चलाता है:
|
||||
|
||||
```console
|
||||
uv run mcp dev server.py
|
||||
uv run mcp dev server.py --with pandas --with numpy
|
||||
uv run mcp dev server.py --with-editable .
|
||||
```
|
||||
|
||||
`--with` जो environment यह बनाता है उसमें packages जोड़ता है; `--with-editable` उसमें आपका अपना package install करता है। इसे आपके `PATH` पर `npx` चाहिए: Inspector Node.js app है।
|
||||
|
||||
`mcp run` file को import करता है, server object ढूँढता है (module-level `mcp`, `server`, या `app`), और उस पर `run()` call करता है:
|
||||
|
||||
```console
|
||||
uv run mcp run server.py
|
||||
uv run mcp run server.py:bookshop
|
||||
```
|
||||
|
||||
जब object का नाम `mcp`, `server`, या `app` नहीं है, तब `:` suffix उसका नाम बताता है।
|
||||
|
||||
आपका `if __name__ == "__main__":` block यहाँ कभी नहीं चलता: `mcp run` खुद `run()` call करता है, और जो अकेला option वह आगे भेजता है वह `--transport` है।
|
||||
|
||||
`mcp install` server को **Claude Desktop** में register करता है, ताकि app उसे आपके लिए launch करे:
|
||||
|
||||
```console
|
||||
uv run mcp install server.py --name "Bookshop"
|
||||
uv run mcp install server.py -v API_KEY=abc123 -f .env
|
||||
```
|
||||
|
||||
`-v KEY=VALUE` और `-f .env` उस entry में environment variables दर्ज करते हैं। Claude Desktop आपके server को अपने process में शुरू करता है। आपके shell का environment वहाँ नहीं होता।
|
||||
|
||||
`mcp install` सिर्फ़ Claude Desktop को host के रूप में जानता है। बाकी हर host (Claude Code, Cursor, VS Code) वही launch command अपनी config file में लेता है, और हर एक की जानकारी **[असली host से जुड़ना](../get-started/real-host.md)** में है।
|
||||
|
||||
`mcp version` install किया गया SDK version print करता है।
|
||||
|
||||
!!! tip
|
||||
`mcp dev` और `mcp run` सिर्फ़ `MCPServer` समझते हैं। अगर आप low-level `Server` से बनाते हैं,
|
||||
तो उसे खुद चलाते हैं। देखें **[Low-level Server](../advanced/low-level-server.md)**।
|
||||
|
||||
## सारांश {#recap}
|
||||
|
||||
* **Transport** वह तरीका है जिससे bytes आपके server तक पहुँचते हैं: local subprocess के लिए `stdio`, port के लिए `streamable-http`। SSE की जगह ले ली गई है।
|
||||
* `mcp.run()` transport चुनता है। बिना argument के यह `stdio` है, और यह block करता है।
|
||||
* हर transport option (`host`, `port`, `streamable_http_path`, ...) `run()` का argument है, `MCPServer(...)` का कभी नहीं।
|
||||
* `run()` को `if __name__ == "__main__":` के नीचे रखें। Server को load करने वाली हर चीज़ पहले file import करती है।
|
||||
* `log_level=` और `debug=` constructor arguments हैं; वे `mcp.settings` पर पहुँचते हैं।
|
||||
* Inspector के लिए `mcp dev`, file चलाने के लिए `mcp run`, Claude Desktop के लिए `mcp install`, version के लिए `mcp version`।
|
||||
* Transport कभी नहीं बदलता कि आपका server **क्या है**: इस page की तीनों files बिल्कुल वही tool expose करती हैं।
|
||||
|
||||
जब `run()` खुद सीमा बन जाए (आपका server किसी पहले से मौजूद app के अंदर), तो वह **[मौजूदा app में जोड़ना](asgi.md)** है। असली hostname और एक से ज़्यादा worker **[Deploy & scale](deploy.md)** है। और अगर आपके कुछ clients अब भी spec version 2025-11-25 या उससे पहले पर हैं, तो अच्छी ख़बर **[Legacy clients को serve करना](legacy-clients.md)** में है।
|
||||
132
i18n/hi/pages/run/legacy-clients.md
Normal file
132
i18n/hi/pages/run/legacy-clients.md
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
---
|
||||
translation:
|
||||
sections: [3d1663c18edc824c, d4fd37009a13f03d, af9f398a5a8b679a, 470c2dd144294d69, 8e45827e6d24e8c8, 91dfd0ce98ebb03c]
|
||||
tool: 1
|
||||
---
|
||||
# legacy clients को serve करना {#serving-legacy-clients}
|
||||
|
||||
MCP में protocol की दो पीढ़ियाँ हैं: `initialize`-handshake वाली पीढ़ी, जो spec version `2025-11-25` तक चलती है, और modern पीढ़ी, `2026-07-28`। इस बँटवारे पर अलग से पूरा page **[Protocol versions](../protocol-versions.md)** है।
|
||||
|
||||
यह page उस बँटवारे के server वाले पहलू के बारे में है, और जवाब एक वाक्य में आ जाता है: **जो `streamable_http_app()` आप पहले से deploy करते हैं, वही दोनों को serve करता है।**
|
||||
|
||||
SDK हर request को उसके `MCP-Protocol-Version` header के हिसाब से route करता है। जिस request में `2026-07-28` लिखा हो, वह modern handler के पास जाती है। जिस request में handshake पीढ़ी का कोई version हो, या कोई header ही न हो (2026 से पहले के client की `initialize` इसी तरह आती है), वह उसी transport के पास जाती है जिसकी उन clients को उम्मीद होती है: `initialize` handshake, sessions, सब कुछ। यह हर request पर होता है, आपके code से पहले, उसी एक app पर।
|
||||
|
||||
इसलिए legacy client कोई ऐसी चीज़ नहीं जिसके **लिए** आप कुछ बनाएँ। वह बस उस server **से जुड़ता** है जो आप पहले ही लिख चुके हैं। configure कुछ नहीं करना।
|
||||
|
||||
!!! note
|
||||
सचमुच कुछ नहीं। न कोई `legacy=` option है, न version allowlist, न किसी पीढ़ी को reject या
|
||||
disable करने का कोई तरीका: न `streamable_http_app()` पर, न `run()` पर, न session manager पर।
|
||||
दोनों पीढ़ियाँ हमेशा चालू रहती हैं। उस signature में पीढ़ी के हिसाब से switch जैसी सबसे नज़दीकी चीज़
|
||||
`stateless_http` है, और इस page का ज़्यादातर हिस्सा उसी के बारे में है।
|
||||
|
||||
## एक handler, दोनों पीढ़ियाँ {#one-handler-both-eras}
|
||||
|
||||
यह रहा एक tool जिसे user से कुछ पूछना है, और दोनों पीढ़ियों के client जो उसे call कर रहे हैं:
|
||||
|
||||
```python title="server.py" hl_lines="24 37-38"
|
||||
--8<-- "docs_src/legacy_clients/tutorial001.py"
|
||||
```
|
||||
|
||||
`reserve` को एक चीज़ चाहिए जो model ने नहीं दी: कितनी copies। tool यह बात `Annotated[..., Resolve(ask_quantity)]` से declare करता है (पूरी जानकारी **[Dependencies](../handlers/dependencies.md)** में है)। `reserve` में कहीं भी न किसी version का नाम है, न capability की जाँच, न कोई branch।
|
||||
|
||||
दोनों clients **एक ही समय पर** खुले हैं, उसी `mcp` object पर। `mode="legacy"` `initialize` handshake चलाता है: ठीक वही connection जो 2026 से पहले का client खोलता है। दूसरा client default लेता है और `2026-07-28` पर पहुँचता है।
|
||||
|
||||
```text
|
||||
2025-11-25 {'result': "Reserved 2 of 'Dune'."}
|
||||
2026-07-28 {'result': "Reserved 2 of 'Dune'."}
|
||||
```
|
||||
|
||||
वही server, वही handler, वही जवाब। पूरा feature बस इतना ही है।
|
||||
|
||||
यह **कैसे** हुआ, इस पर थोड़ा रुकना ठीक रहेगा, क्योंकि दोनों clients से वही सवाल दो बिल्कुल अलग wires पर पूछा गया। `2026-07-28` connection में ऐसा कोई channel नहीं जिस पर server request भेज सके, इसलिए `Resolve` ने सवाल tool result के अंदर लौटाया और client ने जवाब के साथ call दोबारा किया (**[Multi-round-trip requests](../handlers/multi-round-trip.md)**)। `2025-11-25` connection में ऐसा कुछ नहीं है; वहाँ `Resolve` ने call के बीच में ही live `elicitation/create` request भेजी और इंतज़ार किया। आपने दोनों में से कुछ नहीं लिखा। `Resolve` connection का negotiated version पढ़ता है और चुनता है; आपकी tool body को दोनों सूरतों में `AcceptedElicitation` ही दिखता है।
|
||||
|
||||
!!! tip
|
||||
पीढ़ियों के बीच यही portability वह **वजह** है कि `Resolve` ही वह API है जिस पर बनाना चाहिए। इसका पुराना
|
||||
भाई `ctx.elicit()` (**[Elicitation](../handlers/elicitation.md)**) हमेशा सिर्फ़ `elicitation/create` भेजता है,
|
||||
इसलिए यह सिर्फ़ legacy connection पर ही काम करता है। `2026-07-28` connection पर call fail हो जाता है।
|
||||
अगर कोई tool अब भी इसे इस्तेमाल करता है, तो उसका हल वही है जो ऊपर दिखा, version check नहीं।
|
||||
|
||||
## legacy session की कीमत क्या है {#what-a-legacy-session-costs-you}
|
||||
|
||||
routing मुफ़्त है। session नहीं।
|
||||
|
||||
`2026-07-28` connection **sessionless** होता है: हर request अपने आप में पूरी होती है, और modern handler कभी `Mcp-Session-Id` जारी नहीं करता। legacy connection इसका उल्टा है। जैसे ही 2026 से पहले का client `initialize` भेजता है, SDK एक `Mcp-Session-Id` बनाता है, उसे response header में लौटाता है, और उसके पीछे एक live record रखता है ताकि client की बाद की requests उसे ढूँढ सकें: negotiated version, खुले streams, session को चलाने वाला background task।
|
||||
|
||||
वह record बस **सादा in-process `dict`** है। कोई distributed session store नहीं है, और न कोई जोड़ने का तरीका।
|
||||
|
||||
एक worker पर यह दिखता ही नहीं। दो पर, पूरी समस्या यही है: जो request `Mcp-Session-Id` लेकर आए और ऐसे worker पर पहुँचे जिसने वह ID नहीं बनाई थी, उसे उस dict में कुछ नहीं मिलता, और जवाब `404` (`Session not found`) होता है, tool result नहीं। इसलिए जैसे ही आप एक से ज़्यादा worker चलाते हैं, **legacy clients को sticky routing चाहिए**: session की हर request को उसी process तक पहुँचना होगा जिसने उसे शुरू किया था। modern clients को कभी नहीं; उनके पास कोई session ही नहीं जिससे चिपका जाए। stickiness और एक से ज़्यादा worker चलाने से जुड़ी बाकी सारी बातें **[Deploy और scale](deploy.md)** में हैं।
|
||||
|
||||
!!! warning
|
||||
`event_store=` हल जैसा दिखता है पर है नहीं। यह **resumability** है (**उसी** session से दोबारा जुड़ रहे
|
||||
client को छूटे हुए SSE events फिर से भेजना), session store नहीं। यह कभी किसी session को
|
||||
दूसरे process से पहुँच लायक नहीं बनाता।
|
||||
|
||||
## इकलौता switch: `stateless_http` {#the-one-knob-stateless_http}
|
||||
|
||||
अगर stickiness ऐसी कीमत है जो आप चुकाना नहीं चाहते, तो ठीक एक चीज़ है जो आप बदल सकते हैं।
|
||||
|
||||
```python title="server.py" hl_lines="28"
|
||||
--8<-- "docs_src/legacy_clients/tutorial002.py"
|
||||
```
|
||||
|
||||
यह page के ऊपर वाला server ही है, बस एक keyword ज़्यादा। `stateless_http=True` से legacy हिस्सा इसके बजाय हर request के लिए अलग, अस्थायी session बनाता है: कोई `Mcp-Session-Id` जारी नहीं होती, requests के बीच कुछ याद नहीं रखा जाता, इसलिए कोई भी worker कोई भी request serve कर सकता है और load balancer जो चाहे कर सकता है।
|
||||
|
||||
इसके बारे में दो बातें इससे ज़्यादा मायने रखती हैं कि यह करता क्या है।
|
||||
|
||||
**यह सिर्फ़ legacy हिस्से को छूता है।** requests version header के हिसाब से `stateless_http` पढ़े जाने से **पहले** route हो जाती हैं, इसलिए modern path इसे कभी देखता ही नहीं। `2026-07-28` connection पहले से sessionless है और दोनों values पर बिल्कुल एक जैसा रहता है।
|
||||
|
||||
**उस हिस्से पर इसकी कीमत server से client जाने वाले दोनों channels हैं।** जो session सिर्फ़ एक `POST` तक जीता है, उसके पास न ऐसा stream है जिस पर server request भेज सके, न ऐसा standalone stream जिस पर वह notifications भेज सके। server की तरफ़ से शुरू हुई हर request `NoBackChannelError` raise करती है: `ctx.elicit()`, retire हो चुके sampling और roots calls (**[Deprecated features](../deprecated.md)**), और, हाँ, `Resolve` का किसी **legacy** client से अपना सवाल पूछना भी। notifications को तो error भी नहीं मिलता; वे चुपचाप गिरा दिए जाते हैं।
|
||||
|
||||
!!! note
|
||||
`json_response=True` वह switch नहीं है, पर **हर** legacy session पर वही कीमत आधी वसूलता है:
|
||||
जिस `POST` का जवाब एक JSON body से दिया जाए, उसके पास request-scoped channel के लिए कोई stream
|
||||
नहीं होता, इसलिए request के बीच में किया गया `ctx.elicit()` वही `NoBackChannelError` raise करता है और
|
||||
request से जुड़े notifications गिरा दिए जाते हैं। session का standalone stream अछूता रहता है: असंबंधित
|
||||
notifications अब भी पहुँचते हैं।
|
||||
|
||||
!!! check
|
||||
जानबूझकर गलत काम करें। `reserve` ठीक वही tool है जिसने अभी दोनों clients को serve किया। इसे
|
||||
`stateless_http=True` के साथ deploy करें, वही दो clients HTTP पर जोड़ें, और हर एक से इसे call करें।
|
||||
|
||||
modern client को अब भी `Reserved 2 of 'Dune'.` मिलता है। modern हिस्सा नहीं बदला।
|
||||
|
||||
legacy client का call ऐसे `is_error` result के रूप में वापस नहीं आता जिसे model पढ़ सके।
|
||||
पूरी request fail होती है, top-level protocol error के रूप में:
|
||||
|
||||
```text
|
||||
mcp.shared.exceptions.MCPError: Cannot send 'elicitation/create': this transport context has no back-channel for server-initiated requests.
|
||||
```
|
||||
|
||||
`Resolve` ने आपको नहीं बचाया। `2025-11-25` connection पर इसे `elicitation/create` भेजना ही **पड़ता** है,
|
||||
और जो channel इसे चाहिए वह ठीक वही चीज़ है जो `stateless_http=True` ने गँवा दी। पीढ़ियों के बीच
|
||||
portable code का मतलब back-channel से मुक्त code नहीं है।
|
||||
|
||||
तो यह सचमुच का सौदा है, और यह सिर्फ़ legacy हिस्से पर मौजूद है: **session वाला और sticky, या stateless और एकतरफ़ा।** अगर आपके tools कभी client में वापस call नहीं करते, तो `stateless_http=True` मुफ़्त है और आपको इसे ले लेना चाहिए। अगर करते हैं, तो sessions रखें और routing sticky रखें।
|
||||
|
||||
## आपका code असल में कहाँ बँटता है {#where-your-code-actually-forks}
|
||||
|
||||
लगभग कहीं नहीं।
|
||||
|
||||
tools, resources, prompts, structured output, progress, errors: इनमें से किसी को फ़र्क नहीं पड़ता कि किस पीढ़ी ने call किया। `initialize` handshake, `Mcp-Session-Id`, standalone stream, session खत्म करने वाला `DELETE`: यह सब SDK के ज़िम्मे है, और handler को इनमें से कुछ कभी नहीं दिखता। interactive input **वही एक** जगह है जहाँ wire पर पीढ़ियाँ सच में अलग हैं, और `Resolve` इसीलिए है कि यह आपकी समस्या न बने: आपने अभी एक ही tool को दोनों को serve करते देखा।
|
||||
|
||||
ठीक एक चीज़ बचती है, और वह है **change notifications**, क्योंकि दोनों पीढ़ियाँ अलग-अलग pipes पर सुनती हैं:
|
||||
|
||||
* `2026-07-28` client `subscriptions/listen` stream खोलता है और subscriptions bus पढ़ता है। `ctx.notify_resource_updated()` (और `notify_tools_changed()`, `notify_prompts_changed()`, `notify_resources_changed()`) वहीं publish करते हैं, और **सिर्फ़** वहीं। वह page **[Subscriptions](../handlers/subscriptions.md)** है।
|
||||
* legacy client वह standalone stream पढ़ता है जो उसका session खुला रखता है। `ctx.session.send_resource_updated()` (और `send_tool_list_changed()` व उसके साथी) उस **connection** पर लिखते हैं जिस पर request आई थी: legacy session के लिए वह उसका standalone stream है। modern connection में इसके लिए कोई जगह नहीं: HTTP पर ऐसा कोई channel है ही नहीं, और stdio पर चारों तरह के change notifications सिर्फ़ `subscriptions/listen` streams पर चलते हैं, इसलिए modern connection पर notification चुपचाप गिरा दिया जाता है।
|
||||
|
||||
HTTP पर, दोनों में से कोई call दूसरी पीढ़ी के clients तक नहीं पहुँचता। सबको बताने के लिए, दोनों call करें:
|
||||
|
||||
```python title="server.py" hl_lines="19-20"
|
||||
--8<-- "docs_src/legacy_clients/tutorial003.py"
|
||||
```
|
||||
|
||||
दो lines, कोई `if` नहीं, कोई version check नहीं, और काम पूरा। legacy client के होने की वजह से handler जो कुछ अलग करता है, उसकी पूरी सूची बस इतनी ही है।
|
||||
|
||||
## सारांश {#recap}
|
||||
|
||||
* एक ही `streamable_http_app()` protocol की दोनों पीढ़ियों को serve करता है। SDK हर request को उसके `MCP-Protocol-Version` header के हिसाब से route करता है; configure करने को कुछ नहीं है और पीढ़ी का कोई switch ढूँढने को नहीं है।
|
||||
* legacy client की कीमत एक session है: in-process `Mcp-Session-Id` record जिसके पीछे कोई distributed store नहीं। एक से ज़्यादा worker का मतलब **sticky routing** है, वरना गलत worker `404 Session not found` जवाब देता है। कई workers वाली पूरी जानकारी **[Deploy और scale](deploy.md)** में है।
|
||||
* `stateless_http=True` इकलौता switch है, और यह **सिर्फ़ legacy हिस्से पर** असर करता है। यह legacy clients के लिए बेरोक load balancing दिलाता है, पर बदले में उस हिस्से के server से client जाने वाले दोनों channels जाते हैं: server की तरफ़ से शुरू हुई requests `NoBackChannelError` raise करती हैं (client पर top-level error, `is_error` result नहीं), और notifications गिरा दिए जाते हैं।
|
||||
* `2026-07-28` connection हर हाल में sessionless है। `stateless_http` इसे कभी नहीं छूता।
|
||||
* आपका handler code पीढ़ी के हिसाब से ठीक एक जगह बँटता है: change notifications। `ctx.notify_*` `subscriptions/listen` clients तक पहुँचता है; `ctx.session.send_*` legacy sessions तक। दोनों call करें।
|
||||
* बाकी सब कुछ (`Resolve` के ज़रिए user से input माँगना भी) बनावट से ही पीढ़ियों के बीच portable है। modern तरीका एक बार लिखें।
|
||||
85
i18n/hi/pages/run/opentelemetry.md
Normal file
85
i18n/hi/pages/run/opentelemetry.md
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
---
|
||||
translation:
|
||||
sections: [bc0227014724fa49, 15738c2f7fd67d86, a2c17bbe3f707e2f, d0d853376f162c06, b6368643fcc1c8d8, 902e33e17564a607]
|
||||
tool: 1
|
||||
---
|
||||
# OpenTelemetry {#opentelemetry}
|
||||
|
||||
आपका server पहले से trace हो रहा है। आपको कुछ जोड़ने की ज़रूरत नहीं।
|
||||
|
||||
आप जो भी server बनाते हैं, वह अपने संभाले हर message के लिए एक [OpenTelemetry](https://opentelemetry.io/) span emit करता है। यह आपने नहीं लिखा, और न आप इसे import करते हैं। जिस पल आप `MCPServer(...)` call करते हैं, यह मौजूद होता है।
|
||||
|
||||
```python title="server.py"
|
||||
--8<-- "docs_src/opentelemetry/tutorial001.py"
|
||||
```
|
||||
|
||||
यह पूरा, traced server है। `search_books` को call करें और उसके लिए span बन जाता है। low-level `Server` के लिए भी यही सच है: tracing दोनों में मौजूद है।
|
||||
|
||||
## आपको क्या मिलता है {#what-you-get}
|
||||
|
||||
हर inbound message एक `SERVER` span बन जाता है, जिसका नाम method और उसके target पर रखा जाता है। तो `search_books` के लिए `tools/call` का span `tools/call search_books` होता है, और सिर्फ़ `tools/list` बस `tools/list` रहता है।
|
||||
|
||||
हर span में कुछ attributes होते हैं:
|
||||
|
||||
* `mcp.method.name` और `mcp.protocol.version`, हर span पर।
|
||||
* `jsonrpc.request.id`, request पर (notification का कोई नहीं होता)।
|
||||
* जो handler raise करता है, वह span status को error पर set कर देता है। `is_error=True` वाला tool result भी यही करता है।
|
||||
|
||||
और क्योंकि tool call को trace करना बहुत आम ज़रूरत है, `tools/call` spans OpenTelemetry की [GenAI semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/) का पालन करते हैं:
|
||||
|
||||
* `gen_ai.operation.name`, जो `"execute_tool"` पर set होता है।
|
||||
* `gen_ai.tool.name`, जो call हो रहे tool के नाम पर set होता है।
|
||||
|
||||
इसी तर्ज़ पर `prompts/get` span को `gen_ai.prompt.name` मिलता है। list methods में कोई `gen_ai.*` keys नहीं होतीं, क्योंकि वहाँ नाम देने के लिए कुछ है ही नहीं।
|
||||
|
||||
!!! tip
|
||||
इन्हीं GenAI attributes की वजह से tracing UI आपके tool calls को उसी तरह group करती है जैसे किसी भी दूसरे agent के। यह grouping आपको मुफ़्त मिलती है, बिना किसी अतिरिक्त code के।
|
||||
|
||||
## जब तक आप न चाहें, इसकी कोई कीमत नहीं {#it-costs-nothing-until-you-want-it}
|
||||
|
||||
यही वह हिस्सा है जो "default रूप से चालू" को एक सहज default बनाता है।
|
||||
|
||||
SDK सिर्फ़ `opentelemetry-api` पर depend करता है, जो OpenTelemetry का हल्का आधा हिस्सा है। जब कोई SDK और कोई exporter install न हो, तो span बनाना no-op है। इसलिए आपका server अभी जो spans emit कर रहा है, उनकी कीमत लगभग कुछ भी नहीं, और कोई उन्हें इकट्ठा नहीं कर रहा।
|
||||
|
||||
जिस दिन आप उन्हें **देखना** चाहें, दूसरा आधा हिस्सा install करें और उसे कहीं point करें:
|
||||
|
||||
```console
|
||||
uv add opentelemetry-sdk opentelemetry-exporter-otlp
|
||||
```
|
||||
|
||||
exporter को OpenTelemetry के सामान्य तरीके से configure करें, और SDK जो spans चुपचाप बनाता आ रहा था, वे सब दिखने लगते हैं। आपका server code नहीं बदलता। एक line भी नहीं।
|
||||
|
||||
!!! info
|
||||
[Pydantic Logfire](https://logfire.pydantic.dev/) ऐसा ही एक backend है, और यह configuration आपके लिए कर देता है: `pip install logfire`, `logfire.configure()`, और आपके MCP spans live view में दिखने लगते हैं। यह OpenTelemetry पर बना है, इसलिए नीचे लिखी हर बात इस पर भी लागू होती है।
|
||||
|
||||
## wire पार करने वाले traces {#traces-that-cross-the-wire}
|
||||
|
||||
trace सबसे ज़्यादा काम का तब होता है जब वह request को client से लेकर server के अंदर तक, एक जुड़ी हुई तस्वीर में follow करे।
|
||||
|
||||
जब client और server दोनों SDK चला रहे हों, तो यह जुड़ाव अपने आप होता है। client request में [W3C trace context](https://www.w3.org/TR/trace-context/) inject करता है, और server उसे वापस पढ़ लेता है, इसलिए server span उसी trace में client span के नीचे nest हो जाता है। यही [SEP-414](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/414) है, और यह आपको बिना माँगे मिलता है।
|
||||
|
||||
अगर inbound message में कोई trace context नहीं है, जैसे ऐसे client से आई request जो SDK नहीं है, तो server span बिल्कुल नया orphan trace शुरू करने के बजाय server पर जो भी span पहले से current है उसी का child बन जाता है।
|
||||
|
||||
## इसे बंद करना {#turning-it-off}
|
||||
|
||||
tracing एक middleware है, आपके server की सूची में पहला। अगर आप सच में ऐसा server चाहते हैं जो कोई span emit न करे, तो इसे हटा दें:
|
||||
|
||||
```python
|
||||
from mcp.server._otel import OpenTelemetryMiddleware
|
||||
|
||||
mcp._lowlevel_server.middleware[:] = [
|
||||
m for m in mcp._lowlevel_server.middleware if not isinstance(m, OpenTelemetryMiddleware)
|
||||
]
|
||||
```
|
||||
|
||||
!!! warning
|
||||
उस import में आगे underscore है, और यह जानबूझकर है। class provisional है, ठीक वैसे ही जैसे [`Server.middleware`](../advanced/middleware.md) provisional है, इसलिए import path के बदलने की उम्मीद रखें। आपको इसकी ज़रूरत लगभग कभी नहीं पड़ती: जब कोई exporter install न हो तो spans मुफ़्त हैं, इसलिए आम जवाब यही है कि उन्हें चालू रहने दें और exporter install न करें।
|
||||
|
||||
## सारांश {#recap}
|
||||
|
||||
* हर `MCPServer` और हर low-level `Server` बिना कुछ configure किए हर inbound message पर एक `SERVER` span emit करता है। आप कुछ नहीं लिखते।
|
||||
* spans में `mcp.method.name` और `mcp.protocol.version` होते हैं; `tools/call` और `prompts/get` में GenAI attributes भी होते हैं ताकि आपके tool calls किसी भी दूसरे agent की तरह group हों।
|
||||
* जब तक आप OpenTelemetry SDK और exporter install नहीं करते, इसकी कोई कीमत नहीं, और फिर यह आपके server में बिना किसी बदलाव के दिखने लगता है।
|
||||
* जब दोनों तरफ़ SDK चल रहा हो, तो client से server तक trace context अपने आप propagate होता है।
|
||||
|
||||
कोई request चलेगी भी या नहीं, यह **[Authorization](authorization.md)** तय करता है।
|
||||
Loading…
Reference in a new issue