From 33e9d44075067836cf2db424b03e655cccf588b3 Mon Sep 17 00:00:00 2001 From: m0hamed-ux Date: Fri, 9 Oct 2026 14:57:49 +0000 Subject: [PATCH] docs: add Arabic documentation edition --- docs/translations.md | 2 +- i18n/ar/glossary.json | 294 ++++++++++++ i18n/ar/instructions.md | 82 ++++ i18n/ar/notices.md | 20 + i18n/ar/pages/advanced/apps.md | 186 ++++++++ i18n/ar/pages/advanced/extensions.md | 285 ++++++++++++ i18n/ar/pages/advanced/header-parameters.md | 65 +++ i18n/ar/pages/advanced/index.md | 36 ++ i18n/ar/pages/advanced/low-level-server.md | 225 ++++++++++ i18n/ar/pages/advanced/middleware.md | 146 ++++++ i18n/ar/pages/advanced/pagination.md | 89 ++++ i18n/ar/pages/client/caching.md | 136 ++++++ i18n/ar/pages/client/callbacks.md | 154 +++++++ i18n/ar/pages/client/identity-assertion.md | 152 +++++++ i18n/ar/pages/client/index.md | 231 ++++++++++ i18n/ar/pages/client/oauth-clients.md | 155 +++++++ i18n/ar/pages/client/session-groups.md | 87 ++++ i18n/ar/pages/client/subscriptions.md | 91 ++++ i18n/ar/pages/client/transports.md | 166 +++++++ i18n/ar/pages/deprecated.md | 157 +++++++ i18n/ar/pages/get-started/first-steps.md | 143 ++++++ i18n/ar/pages/get-started/index.md | 57 +++ i18n/ar/pages/get-started/installation.md | 47 ++ i18n/ar/pages/get-started/real-host.md | 182 ++++++++ i18n/ar/pages/get-started/testing.md | 114 +++++ i18n/ar/pages/handlers/cancellation.md | 60 +++ i18n/ar/pages/handlers/context.md | 134 ++++++ i18n/ar/pages/handlers/dependencies.md | 163 +++++++ i18n/ar/pages/handlers/elicitation.md | 191 ++++++++ i18n/ar/pages/handlers/index.md | 38 ++ i18n/ar/pages/handlers/lifespan.md | 94 ++++ i18n/ar/pages/handlers/logging.md | 88 ++++ i18n/ar/pages/handlers/multi-round-trip.md | 191 ++++++++ i18n/ar/pages/handlers/progress.md | 123 +++++ i18n/ar/pages/handlers/sampling-and-roots.md | 51 +++ i18n/ar/pages/handlers/subscriptions.md | 192 ++++++++ i18n/ar/pages/index.md | 102 +++++ i18n/ar/pages/protocol-versions.md | 141 ++++++ i18n/ar/pages/run/asgi.md | 145 ++++++ i18n/ar/pages/run/authorization.md | 134 ++++++ i18n/ar/pages/run/deploy.md | 197 +++++++++ i18n/ar/pages/run/index.md | 161 +++++++ i18n/ar/pages/run/legacy-clients.md | 176 ++++++++ i18n/ar/pages/run/opentelemetry.md | 112 +++++ i18n/ar/pages/servers/completions.md | 130 ++++++ i18n/ar/pages/servers/handling-errors.md | 162 +++++++ i18n/ar/pages/servers/index.md | 35 ++ i18n/ar/pages/servers/media.md | 141 ++++++ i18n/ar/pages/servers/prompts.md | 202 +++++++++ i18n/ar/pages/servers/resources.md | 146 ++++++ i18n/ar/pages/servers/structured-output.md | 257 +++++++++++ i18n/ar/pages/servers/tools.md | 179 ++++++++ i18n/ar/pages/servers/uri-templates.md | 274 ++++++++++++ i18n/ar/pages/translations.md | 30 ++ i18n/ar/pages/troubleshooting.md | 443 +++++++++++++++++++ i18n/ar/pages/whats-new.md | 218 +++++++++ i18n/languages.yml | 4 + 57 files changed, 8015 insertions(+), 1 deletion(-) create mode 100644 i18n/ar/glossary.json create mode 100644 i18n/ar/instructions.md create mode 100644 i18n/ar/notices.md create mode 100644 i18n/ar/pages/advanced/apps.md create mode 100644 i18n/ar/pages/advanced/extensions.md create mode 100644 i18n/ar/pages/advanced/header-parameters.md create mode 100644 i18n/ar/pages/advanced/index.md create mode 100644 i18n/ar/pages/advanced/low-level-server.md create mode 100644 i18n/ar/pages/advanced/middleware.md create mode 100644 i18n/ar/pages/advanced/pagination.md create mode 100644 i18n/ar/pages/client/caching.md create mode 100644 i18n/ar/pages/client/callbacks.md create mode 100644 i18n/ar/pages/client/identity-assertion.md create mode 100644 i18n/ar/pages/client/index.md create mode 100644 i18n/ar/pages/client/oauth-clients.md create mode 100644 i18n/ar/pages/client/session-groups.md create mode 100644 i18n/ar/pages/client/subscriptions.md create mode 100644 i18n/ar/pages/client/transports.md create mode 100644 i18n/ar/pages/deprecated.md create mode 100644 i18n/ar/pages/get-started/first-steps.md create mode 100644 i18n/ar/pages/get-started/index.md create mode 100644 i18n/ar/pages/get-started/installation.md create mode 100644 i18n/ar/pages/get-started/real-host.md create mode 100644 i18n/ar/pages/get-started/testing.md create mode 100644 i18n/ar/pages/handlers/cancellation.md create mode 100644 i18n/ar/pages/handlers/context.md create mode 100644 i18n/ar/pages/handlers/dependencies.md create mode 100644 i18n/ar/pages/handlers/elicitation.md create mode 100644 i18n/ar/pages/handlers/index.md create mode 100644 i18n/ar/pages/handlers/lifespan.md create mode 100644 i18n/ar/pages/handlers/logging.md create mode 100644 i18n/ar/pages/handlers/multi-round-trip.md create mode 100644 i18n/ar/pages/handlers/progress.md create mode 100644 i18n/ar/pages/handlers/sampling-and-roots.md create mode 100644 i18n/ar/pages/handlers/subscriptions.md create mode 100644 i18n/ar/pages/index.md create mode 100644 i18n/ar/pages/protocol-versions.md create mode 100644 i18n/ar/pages/run/asgi.md create mode 100644 i18n/ar/pages/run/authorization.md create mode 100644 i18n/ar/pages/run/deploy.md create mode 100644 i18n/ar/pages/run/index.md create mode 100644 i18n/ar/pages/run/legacy-clients.md create mode 100644 i18n/ar/pages/run/opentelemetry.md create mode 100644 i18n/ar/pages/servers/completions.md create mode 100644 i18n/ar/pages/servers/handling-errors.md create mode 100644 i18n/ar/pages/servers/index.md create mode 100644 i18n/ar/pages/servers/media.md create mode 100644 i18n/ar/pages/servers/prompts.md create mode 100644 i18n/ar/pages/servers/resources.md create mode 100644 i18n/ar/pages/servers/structured-output.md create mode 100644 i18n/ar/pages/servers/tools.md create mode 100644 i18n/ar/pages/servers/uri-templates.md create mode 100644 i18n/ar/pages/translations.md create mode 100644 i18n/ar/pages/troubleshooting.md create mode 100644 i18n/ar/pages/whats-new.md diff --git a/docs/translations.md b/docs/translations.md index 1061cf67e2..2c9ee9d532 100644 --- a/docs/translations.md +++ b/docs/translations.md @@ -4,7 +4,7 @@ This documentation is written in English. To make it useful to more people, we a ## What's available -Translated documentation is currently a **preview** in twelve languages: Deutsch, español, français, हिन्दी, 日本語, 한국어, português (Brasil), русский язык, Türkçe, українська мова, 简体中文 and 繁體中文. Pick one from the language switcher at the top of any page. More languages may follow once these have proved themselves. +Translated documentation is currently a **preview** in thirteen languages: العربية, Deutsch, español, français, हिन्दी, 日本語, 한국어, português (Brasil), русский язык, Türkçe, українська мова, 简体中文 and 繁體中文. Pick one from the language switcher at the top of any page. More languages may follow once these have proved themselves. The API reference is not translated: the translated site links to the single English one. diff --git a/i18n/ar/glossary.json b/i18n/ar/glossary.json new file mode 100644 index 0000000000..21d981c2fa --- /dev/null +++ b/i18n/ar/glossary.json @@ -0,0 +1,294 @@ +{ + "keep": [ + "MCP", + "Model Context Protocol", + "MCPServer", + "FastMCP", + "ClientSession", + "Context", + "ctx", + "stdio", + "Streamable HTTP", + "SSE", + "JSON-RPC", + "JSON", + "OAuth", + "PKCE", + "JWT", + "CIMD", + "HTTP", + "HTTPS", + "TLS", + "CORS", + "URI", + "URL", + "ASGI", + "WebSocket", + "API", + "SDK", + "CLI", + "IDE", + "LLM", + "SEP", + "RFC", + "Python", + "TypeScript", + "Node.js", + "PyPI", + "Pydantic", + "Starlette", + "FastAPI", + "uvicorn", + "httpx", + "anyio", + "asyncio", + "trio", + "pytest", + "OpenTelemetry", + "Inspector", + "Claude", + "GitHub", + "VS Code", + "Windows", + "macOS", + "Linux", + "llms.txt", + "2026-07-28", + "2025-11-25", + "2025-06-18", + "2025-03-26" + ], + "terms": [ + { + "source": "tool", + "target": "أداة", + "note": "MCP callable primitive; plural أدوات. Keep Tools when naming the Inspector UI tab, and all code identifiers unchanged." + }, + { + "source": "resource", + "target": "مورد", + "note": "Readable MCP data; plural موارد. A resource template is قالب مورد. Keep Resources and Resource Templates when naming actual UI tabs." + }, + { + "source": "prompt", + "target": "قالب توجيه", + "note": "Reusable MCP message template, plural قوالب توجيه. First prose mention on a page may include (prompt). A general instruction to an LLM is توجيه; a UI confirmation prompt is مطالبة. Keep Prompts as a literal UI label." + }, + { + "source": "sampling", + "target": "أخذ العينات", + "note": "MCP model-generation feature; first prose occurrence takes (sampling). Do not suggest it merely samples existing data. Wire identifiers remain unchanged." + }, + { + "source": "roots", + "target": "المجلدات الجذرية", + "note": "Client-exposed directory roots; first prose mention takes (roots). Not mathematical roots or administrator privileges." + }, + { + "source": "elicitation", + "target": "استقاء المعلومات", + "note": "Server requests user input through the client; first prose mention takes (elicitation). Preserve form/URL and push/pull distinctions." + }, + { + "source": "capability", + "target": "قدرة", + "note": "Declared protocol capability; plural قدرات. A feature is ميزة, not necessarily a declared capability." + }, + { + "source": "transport", + "target": "وسيلة نقل", + "note": "Protocol connection mechanism; plural وسائل نقل. stdio, Streamable HTTP, and SSE stay unchanged." + }, + { + "source": "session", + "target": "جلسة", + "note": "Plural جلسات. Session identifiers and class names stay unchanged." + }, + { + "source": "handler", + "target": "دالة معالجة", + "note": "Registered handler function; plural دوال معالجة. A handler body is جسم دالة المعالجة; avoid the hardware sense of processor." + }, + { + "source": "dependency", + "target": "اعتمادية", + "note": "Plural اعتماديات, for packages and injected dependencies. Dependency injection is حقن الاعتماديات." + }, + { + "source": "resolver", + "target": "دالة حل الاعتمادية", + "note": "Function supplying an injected parameter. Shorten to دالة الحل after the role is clear; preserve Resolve identifiers." + }, + { + "source": "client", + "target": "عميل", + "note": "Plural عملاء; the MCP component inside a host, distinct from the user and host application." + }, + { + "source": "server", + "target": "خادم", + "note": "Plural خوادم. Class and module names remain unchanged." + }, + { + "source": "host", + "target": "تطبيق مضيف", + "note": "The user-facing MCP application containing clients, not the MCP server or a hosting machine. Shorten to المضيف when unambiguous." + }, + { + "source": "request", + "target": "طلب", + "note": "Protocol/HTTP request; plural طلبات. A response is استجابة, a result is نتيجة." + }, + { + "source": "token", + "target": "رمز", + "note": "OAuth: access token رمز وصول, refresh token رمز تحديث, bearer token رمز حامل. LLM token: وحدة نصية (token) on first use, then وحدة نصية. Do not conflate the two." + }, + { + "source": "lifespan", + "target": "دورة الحياة", + "note": "Server startup/shutdown feature. Preserve lifespan= and other identifiers." + }, + { + "source": "callback", + "target": "دالة رد نداء", + "note": "Plural دوال رد نداء. OAuth callback URL is عنوان URL لرد النداء. First prose mention may take (callback)." + }, + { + "source": "deploy", + "target": "نشر", + "note": "Deploy a server: انشر الخادم. Deployment نشر; distinct from running locally, تشغيل." + }, + { + "source": "library", + "target": "مكتبة", + "note": "Software library; plural مكتبات." + }, + { + "source": "back-channel", + "target": "قناة عكسية", + "note": "Server-to-client calls during a request; first prose mention takes (back-channel). Preserve NoBackChannelError." + }, + { "source": "file", "target": "ملف", "note": "Plural ملفات." }, + { + "source": "user", + "target": "مستخدم", + "note": "Plural مستخدمون / مستخدمين according to case. Distinct from client." + }, + { + "source": "escape hatch", + "target": "منفذ للتحكم المباشر", + "note": "Lower-level API mechanism for bypassing convenience-layer restrictions. Translate metaphor by its function." + }, + { + "source": "type hint", + "target": "تلميح نوع", + "note": "Python typing hint; plural تلميحات الأنواع. Type annotation is تعليق نوع when distinction matters; do not translate the identifier." + }, + { + "source": "Get started", + "target": "ابدأ هنا", + "note": "Guide section/index title; distinct from First steps." + }, + { + "source": "First steps", + "target": "الخطوات الأولى", + "note": "Tutorial page inside Get started." + }, + { + "source": "authentication", + "target": "مصادقة", + "note": "Verifying identity, distinct from authorization." + }, + { + "source": "authorization", + "target": "تفويض", + "note": "Granting/checking permission, distinct from authentication." + }, + { + "source": "middleware", + "target": "برمجيات وسيطة", + "note": "Request-processing middleware. A single middleware component is مكوّن وسيط." + }, + { + "source": "schema", + "target": "مخطط", + "note": "Data/type schema; JSON Schema remains the name JSON Schema." + }, + { + "source": "structured output", + "target": "مخرجات منظّمة", + "note": "Tool output matching a schema; structured content is محتوى منظّم." + }, + { + "source": "pagination", + "target": "تقسيم النتائج إلى صفحات", + "note": "Protocol list pagination, not printed-page numbering." + }, + { + "source": "subscription", + "target": "اشتراك", + "note": "Plural اشتراكات. Subscribe اشترك; unsubscribe ألغِ الاشتراك." + }, + { + "source": "notification", + "target": "إشعار", + "note": "Protocol notification distinct from request and response." + }, + { + "source": "cancellation", + "target": "إلغاء", + "note": "Cancelling a request/task; preserve cooperative cancellation semantics." + }, + { + "source": "stateless", + "target": "عديم الحالة", + "note": "No server-side state required between requests; statelessness انعدام الحالة." + }, + { + "source": "stateful", + "target": "ذو حالة", + "note": "Preserves state across requests; inflect for gender and number." + }, + { + "source": "identity assertion", + "target": "إفادة الهوية", + "note": "Signed assertion carrying authenticated identity; first prose occurrence may take (identity assertion)." + }, + { + "source": "completion", + "target": "إكمال", + "note": "MCP argument autocomplete إكمال تلقائي للوسائط; LLM completion استكمال يولّده النموذج. Preserve protocol/class identifiers." + }, + { + "source": "in-memory", + "target": "داخل الذاكرة", + "note": "Direct in-process test connection, without network transport or subprocess." + }, + { + "source": "legacy", + "target": "قديم", + "note": "Older protocol path, not a judgement about quality. Legacy client عميل قديم. Preserve mode=\"legacy\"." + }, + { + "source": "pull", + "target": "سحب", + "note": "New protocol where clients fetch outstanding requests; contrast with server-initiated دفع (push)." + }, + { + "source": "argument", + "target": "وسيطة", + "note": "Passed call value; plural وسائط. Parameter is مَعلمة, plural مَعلمات; preserve identifiers." + }, + { + "source": "stream", + "target": "تدفّق", + "note": "Streaming تَدَفّق / بث according to context; never translate the name Streamable HTTP." + }, + { + "source": "cache", + "target": "ذاكرة تخزين مؤقت", + "note": "Caching تخزين مؤقت; cached result نتيجة مخزّنة مؤقتًا." + } + ] +} diff --git a/i18n/ar/instructions.md b/i18n/ar/instructions.md new file mode 100644 index 0000000000..e4082ba5f1 --- /dev/null +++ b/i18n/ar/instructions.md @@ -0,0 +1,82 @@ +# Arabic (ar) — translation instructions + +Target language: Modern Standard Arabic (العربية الفصحى), directory and URL +code `ar`, page language tag `ar`. These instructions accompany the shared +rules in `../general-prompt.md`; `glossary.json` wins terminology conflicts. + +## 1. Register + +Write clear Modern Standard Arabic for software developers across the Arabic-speaking +world. Use neither regional dialect nor ornate literary or bureaucratic language. +Address the reader with direct singular imperatives: "ثبّت", "أنشئ", "شغّل", "مرّر". +Prefer verbs to nominal constructions: "شغّل الخادم", not "قم بعملية تشغيل الخادم". +Use "يمكنك" for can, "يجب" for must, "ينبغي" for should, and "قد" for may +when it expresses possibility; never weaken a requirement or turn an option into one. +Do not add "يرجى" to instructions that are direct in English. + +## 2. Voice + +Sound like an experienced Arabic-speaking developer explaining the SDK to a colleague: +direct, practical, and approachable. Prefer short sentences and familiar connectors +such as "ثم", "لذلك", and "أي". Preserve every technical claim, caveat, condition, +negation, example, and step, including those in a friendly aside. Recast clause order +when Arabic needs it without rearranging blocks or changing emphasis. + +Avoid inflated introductions such as "تجدر الإشارة إلى" and "من الجدير بالذكر", +mechanical English word order, excessive passive voice, and transliterated verbs. +"Returns" is "يعيد" in a function description, not "يرجع إلى". "Expose" means +"يتيح" in MCP prose, not "يفضح". "Argument" means a passed value, never a dispute. +Distinguish the MCP host application from the client inside it and the server it +connects to. Distinguish authentication (المصادقة) from authorization (التفويض). + +## 3. Humour and idioms + +Translate the meaning of an idiom rather than its literal image. "Out of the box" +is "افتراضيًا"; "under the hood" is "داخليًا"; "the whole story" is "التفاصيل كاملة". +"That's it. It's just Python." is "هذا كل شيء. إنها Python فحسب.". +Keep short payoff sentences short. Preserve the source's emojis and punctuation +emphasis without adding new ones. Do not omit an aside or invent explanatory notes. + +## 4. Typography + +Arabic prose reads right to left; Latin identifiers and code retain their original +left-to-right spelling. Do not reverse text, insert invisible bidi control characters, +or wrap identifiers in added HTML or Markdown. Direction is the site's responsibility. +Use the Arabic comma "،", semicolon "؛", and question mark "؟" in prose; retain +ordinary colons, parentheses, straight quotes, and the source's Markdown syntax. +Do not translate punctuation inside code, URLs, or pinned heading anchors. + +Use ASCII digits throughout, including quantities, ports, versions, dates, HTTP +status codes, percentages, RFCs, and SEPs. Preserve decimal separators and protocol +revision identifiers exactly. Avoid decorative elongation (tatweel) and full vowel +marks; use an occasional mark only to resolve ambiguity. Spell hamza and final +letters correctly (إعداد، إنشاء، استدعاء، واجهة، مكتبة). + +Translate headings, table cells, admonition titles, tab labels, link text, and image +alt text. Product/package names used as labels (uv, pip, Claude Desktop) stay as named. +When referring to an actual English UI tab, keep its displayed label (Tools, +Resources, Resource Templates, Prompts) so the reader can find it. Preserve bold and +italic emphasis on the corresponding meaning and never add code spans. + +## 5. Terminology pointer + +Follow `glossary.json` consistently, allowing normal Arabic inflection, definiteness, +agreement, and plural forms (أداة / الأدوات، مورد / الموارد، عميل / العملاء). +The listed targets name concepts; do not force the singular into a plural sentence. +Everything in a code span or fenced block stays byte-identical, including comments, +docstrings, strings, snippet includes, annotation markers, and error messages. +API identifiers, classes, functions, parameters, modules, headers, environment +variables, commands, and protocol methods stay unchanged even outside code font. + +On a page's first prose use of an unfamiliar MCP concept, include its English term +in parentheses where the glossary asks for it. Subsequent uses use Arabic alone. +Do not add a gloss to a code identifier or to a heading when the concept is explained +in the body. Acronyms and proper names in `keep` remain exactly as in the source. +Translate all other reader-visible English; do not leave whole sentences in English. + +## 6. Provisional note + +These choices require review by native Arabic-speaking developers. Propose durable +corrections here or in `glossary.json`, then regenerate the affected pages; do not +patch generated `pages/` or `notices.md`. The normal command is +`translate --lang ar --pages …`; the English documentation remains authoritative. diff --git a/i18n/ar/notices.md b/i18n/ar/notices.md new file mode 100644 index 0000000000..688d5dfc76 --- /dev/null +++ b/i18n/ar/notices.md @@ -0,0 +1,20 @@ +--- +translation: + sections: [aff1b3e872b7876a, 4d80558ad052d586, 0bb81f1e62062d26, d5c35dcec50156bc] + tool: 1 +--- +# ملاحظات الترجمة {#translation-notices} + +تظهر إحدى هذه الملاحظات أعلى كل صفحة من موقع التوثيق المترجم. + +## ترجمة آلية {#translated} + +تُرجمت هذه الصفحة آليًا من التوثيق الإنجليزي، وتظل [الصفحة الإنجليزية](ENGLISH_PAGE) النسخة المعتمدة. إذا وجدت صياغة غير صحيحة، فتوضح [الترجمات](TRANSLATIONS_PAGE) كيفية الإبلاغ عنها. + +## الترجمة متأخرة عن الصفحة الإنجليزية {#outdated} + +تغيّرت الصفحة الإنجليزية بعد إعداد هذه الترجمة، لذا قد تكون بعض أجزائها غير محدّثة. عند الشك، اقرأ [الصفحة الإنجليزية](ENGLISH_PAGE)؛ وتوضح [الترجمات](TRANSLATIONS_PAGE) كيفية عمل التوثيق المترجم. + +## معروضة بالإنجليزية {#english} + +لا توجد ترجمة محدّثة لهذه الصفحة، لذا تقرؤها بالإنجليزية. توضح [الترجمات](TRANSLATIONS_PAGE) كيفية عمل التوثيق المترجم. diff --git a/i18n/ar/pages/advanced/apps.md b/i18n/ar/pages/advanced/apps.md new file mode 100644 index 0000000000..a5ae714792 --- /dev/null +++ b/i18n/ar/pages/advanced/apps.md @@ -0,0 +1,186 @@ +--- +translation: + sections: [0355618e5f4d5fe4, 0fefa31fb7c7b585, 5c53e7487c9c70cc, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487] + tool: 1 +--- +# MCP Apps {#mcp-apps} + +**تطبيق MCP** أداة لها واجهة: إلى جانب بياناتها، تشير الأداة إلى وثيقة HTML +يعرضها التطبيق المضيف كواجهة تفاعلية. + +يتألف دائمًا من جزأين: + +1. **أداة** تنفّذ العمل وتعيد بيانات، مثل أي أداة أخرى. +2. **مورد `ui://`** يحتوي على HTML الذي يعرضه التطبيق المضيف لها. + +تحمل الأداة مرجع `_meta.ui.resourceUri` إلى المورد. يجلبه التطبيق المضيف +باستخدام `resources/read`، ويعرضه في **iframe داخل بيئة معزولة**، ويدفع نتيجة +الأداة إلى ذلك الإطار عبر `postMessage`. لا يرسل خادمك أي رسائل +`ui/*` ولا يتلقاها: فهذه الحركة بين التطبيق المضيف والإطار. أنت تخدم أداة +ووثيقة HTML؛ ويتولى التطبيق المضيف العرض. + +تقدّم SDK ذلك كامتداد `Apps` مدمج (`io.modelcontextprotocol/ui`). +إذا لم تكن تعرف [الامتدادات](extensions.md)، فاقرأ تلك الصفحة سريعًا أولًا. دقيقة واحدة، +ثم عد إلى هنا. + +## ساعة بواجهة {#a-clock-with-a-face} + +```python title="server.py" hl_lines="17 20 28 30" +--8<-- "docs_src/apps/tutorial001.py" +``` + +أربع خطوات: + +* `Apps()`: نسخة واحدة تحمل أدواتك المرتبطة بالواجهة ومواردها. +* `@apps.tool(resource_uri="ui://clock/app.html")`: أداة عادية، مع + وسم `_meta.ui.resourceUri`. يُمرّر كل ما تقبله `@mcp.tool()` (الاسم والعنوان + والوصف وغيرها). +* `apps.add_html_resource("ui://clock/app.html", CLOCK_HTML)`: المورد + المطابق، ويُقدّم بالنوع `text/html;profile=mcp-app`. نوع MIME هذا تحديدًا + هو ما يخبر التطبيق المضيف: «هذا تطبيق، اعرضه». +* `MCPServer("clock", extensions=[apps])`: تفعيل اختياري. يعلن الخادم الآن + `io.modelcontextprotocol/ui` تحت `capabilities.extensions`. + +تستمع HTML نفسها إلى `postMessage` من التطبيق المضيف وتعرض النتيجة. للتطبيقات الفعلية، +استخدم SDK المتصفح الرسمية [`@modelcontextprotocol/ext-apps`](https://github.com/modelcontextprotocol/ext-apps) +داخل HTML. توفر لك `ontoolresult` و`callServerTool` +و`getHostContext` و`onhostcontextchanged` بدلًا من أحداث الرسائل الخام. + +## تراجع سلس إلى النص {#graceful-degradation} + +لا تعرض كل العملاء التطبيقات. المواصفة واضحة بشأن ما يعنيه ذلك لك: + +> **يجب** أن تعيد الأدوات مصفوفة `content` ذات معنى حتى عندما تتوفر واجهة مستخدم. + +يقرأ النموذج `content`؛ أما iframe فللبشر. يظل التطبيق المضيف القادر على عرض الواجهات يمرّر +النتيجة النصية إلى النموذج، ويحصل العميل النصي على ذلك *فقط*. ولذلك +فالنمط المعتمد هو أداة واحدة وإجابتان. انظر إلى `get_time` مجددًا: + +```python title="server.py" hl_lines="21-25" +--8<-- "docs_src/apps/tutorial001.py" +``` + +لا تكون `client_supports_apps(ctx)` مساوية لـ`True` إلا عندما يعلن العميل +امتداد `io.modelcontextprotocol/ui` **و** يُدرج `text/html;profile=mcp-app` +في إعدادات `mimeTypes`. الحقل مطلوب، ولذلك لا يُعد العميل الذي يحذفه +داعمًا. إليك جانب العميل من التفاوض: + +```python title="client.py" hl_lines="8 12" +--8<-- "docs_src/apps/tutorial001_client.py" +``` + +شغّل `server.py` عبر HTTP، ثم شغّل العميل في طرفية ثانية: + +```console +uv run mcp run server.py --transport streamable-http +``` + +```console +python client.py +``` + +```text +2026-06-26T12:00:00Z +``` + +عادت الإجابة الغنية. احذف `extensions=[APPS_SUPPORT]` من استدعاء `Client`، +وسيَطبع البرنامج نفسه `The time is 2026-06-26T12:00:00Z.` بدلًا منها، وهو +كل ما يراه العميل النصي. + +!!! warning + لا تعِد عنصرًا نائبًا مثل `"[Rendered UI]"` بوصفه المحتوى الوحيد مطلقًا. إذا كان + النص البديل غير مفيد، فالأداة غير مفيدة لكل عميل نصي وللنموذج + نفسه. اكتب الجملة الفعلية. + +## تقييد iframe {#locking-the-iframe-down} + +يحمل جانب المورد البيانات الوصفية الأمنية: ما يجوز للإطار تحميله، وأذونات +المتصفح التي يطلبها، وكيف يريد تضمينه: + +```python title="server.py" hl_lines="9 19-22" +--8<-- "docs_src/apps/tutorial002.py" +``` + +تمثل `csp` و`permissions` **طلبات إلى التطبيق المضيف**، وليستا سلوكًا للخادم. يبني التطبيق المضيف +سياسَتي Content-Security-Policy وPermissions-Policy للإطار منهما، وقد +يرفض. تحقّق من توفر الميزات في JavaScript بدلًا من افتراض منحها. + +حقول `ResourceCsp` واحدًا واحدًا (اسم Python، والمفتاح على الشبكة، وما يفعله التطبيق المضيف به): + +| Python | على الشبكة (`_meta.ui.csp`) | ما يتحكم فيه | +|---|---|---| +| `connect_domains` | `connectDomains` | `connect-src`: وجهات `fetch`/XHR المسموح بها | +| `resource_domains` | `resourceDomains` | `img-src` و`style-src` وغيرها: الملفات الثابتة | +| `frame_domains` | `frameDomains` | `frame-src`: إطارات iframe المتداخلة | +| `base_uri_domains` | `baseUriDomains` | `base-uri`: الوجهات التي يجوز أن يشير إليها `` | + +`ResourcePermissions`: يطلب كل حقل إذنًا من المتصفح للإطار. + +| Python | على الشبكة (`_meta.ui.permissions`) | +|---|---| +| `camera` | `camera` | +| `microphone` | `microphone` | +| `geolocation` | `geolocation` | +| `clipboard_write` | `clipboardWrite` | + +!!! note + توجد CSP والأذونات في **المورد**، وليس في الأداة مطلقًا. لا تتضمن البيانات الوصفية للأداة + في المواصفة موضعًا لها، وتتجاهلها التطبيقات المضيفة هناك. تمنع SDK تمثيل + هذا الخطأ: لا تحتوي `@apps.tool()` على مَعلمة `csp` أصلًا. + +### الظهور {#visibility} + +تعني `visibility=["app"]` في الأداة: «هذه موجودة للإطار، وليس للنموذج»: + +* `"model"`: يستطيع النموذج استدعاءها. +* `"app"`: يستطيع الإطار استدعاءها (عبر `callServerTool`). +* عند الحذف: كلاهما، وهو الافتراضي. + +الترشيح مهمة **التطبيق المضيف**. يدرج خادمك الأدوات الخاصة بالتطبيق فقط في `tools/list` +مثل غيرها؛ ويخفيها التطبيق المضيف عن النموذج. لا ترشّحها على جانب الخادم. + +## القواعد التي تفرضها SDK {#the-rules-the-sdk-enforces} + +تفشل كل الحالات التالية عند بدء التشغيل، وليس في الإنتاج: + +* تؤدي `resource_uri` أو URI مورد لا تتبع `ui://...` إلى `ValueError` عند + تطبيق المزخرف أو التسجيل. +* تؤدي الأداة المرتبطة بعنوان URI **دون مورد مسجّل مطابق** إلى `ValueError` + عندما تستهلك `MCPServer(extensions=[apps])` الامتداد. الأداة التي تعلن + HTML تعيد 404 عند `resources/read` إعداد خاطئ، ولذلك يرفض + المُنشئ إكمال الإنشاء. +* تؤدي `meta={"ui": ...}` في `@apps.tool()` إلى `ValueError`. يتولى المزخرف + `_meta["ui"]`؛ عبّر عنها باستخدام `resource_uri=` و`visibility=`. تُدمج مفاتيح `meta=` الأخرى + بصورة طبيعية إلى جانبها. + +لا تكتشف SDK ext-apps في TypeScript ولا FastMCP أيًا من هذه الحالات حاليًا؛ ويفضّل أن +تعرفها قبل أن يصادفها التطبيق المضيف. + +## ما بعد HTML المضمّنة {#beyond-inline-html} + +تغطي `add_html_resource` الحالة الشائعة: نص HTML. أما في غير ذلك، +مثل HTML على القرص أو محتوى مولّد، فابنِ المورد بنفسك ومرّره: + +```python title="server.py" hl_lines="12 18" +--8<-- "docs_src/apps/tutorial003.py" +``` + +تضع `add_resource` نوع MIME `text/html;profile=mcp-app` عندما لا يعيّن المورد +نوعًا صريحًا، وترفض النوع الصريح المخالف: لن يعرض أي تطبيق مضيف مورد `ui://` +تحت نوع MIME آخر. + +!!! tip + هل تستهدف تطبيقًا مضيفًا يسبق الإتاحة العامة وما زال يقرأ المفتاح المسطح المهمل + `_meta["ui/resourceUri"]`؟ ادمجه بنفسك: + `@apps.tool(resource_uri="ui://x", meta={"ui/resourceUri": "ui://x"})`. + كائن `ui` المتداخل هو شكل المواصفة؛ أما المفتاح المسطح فسيُستغنى عنه. + +## شاهده يعمل {#see-it-run} + +قصة `apps` في `examples/stories/` هي هذه الصفحة في زوج قابل للتشغيل: خادم +بأداة ساعة مرتبطة بواجهة، وعميل يتفاوض على Apps ويقرأ +`_meta.ui.resourceUri` الخاصة بالأداة ويجلب HTML ويستدعي الأداة. + +```bash +uv run python -m stories.apps.client +``` diff --git a/i18n/ar/pages/advanced/extensions.md b/i18n/ar/pages/advanced/extensions.md new file mode 100644 index 0000000000..dbbd496f53 --- /dev/null +++ b/i18n/ar/pages/advanced/extensions.md @@ -0,0 +1,285 @@ +--- +translation: + sections: [05891e7cc1938a13, b3c01a6af28c51ee, 6eba6ce094f4417e, 125f28588c85dd7b, ddd8969b698ca11c, ed6af2df4b656dff] + tool: 1 +--- +# الامتدادات {#extensions} + +**الامتداد** حزمة اختيارية من سلوك MCP تحت معرّف واحد. + +يمكنه في الخادم إضافة أدوات وموارد وطرائق طلب جديدة، وتغليف +`tools/call`. ويمكنه في العميل تعريف أشكال إضافية لنتائج `tools/call` ومراقبة إشعارات +المورّد. يعلن كل طرف الامتداد في `capabilities.extensions` الخاصة به، ولا يتغير شيء +لمن لم يطلبه. هذا هو العقد ([SEP-2133](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2133))، +وله قاعدة أساسية واحدة: **الامتدادات معطّلة افتراضيًا**. + +## استخدام امتداد {#using-an-extension} + +مرّر النسخ عند الإنشاء: + +```python title="server.py" +--8<-- "docs_src/extensions/tutorial001.py" +``` + +اكتمل الإعداد. يعلن الخادم الآن `io.modelcontextprotocol/ui` تحت +`capabilities.extensions`، ويخدم كل ما يضيفه الامتداد. + +يمثل `Apps` الامتداد المرجعي المدمج، وله صفحة مستقلة: **[MCP Apps](apps.md)**. + +!!! note + تُثبت الامتدادات عند الإنشاء. لا توجد `add_extension` لاستدعائها لاحقًا: + يجب ألا تتغير خريطة قدرات الخادم أثناء اتصال العملاء به. + +تُنقل خريطة القدرات عبر `server/discover`، وهو مسار **2026-07-28**. لا يوجد موضع لها +في مصافحة `initialize` القديمة، ولذلك لا يرى العميل القديم +الامتداد. صمّم مع مراعاة ذلك: الامتداد *يعزّز* الخادم، ويجب ألا يكون +الطريقة الوحيدة لاستخدامه. + +## كتابة امتدادك {#writing-your-own} + +اشتق صنفًا من `Extension` وتجاوز ما تحتاجه فقط. لكل دالة تنفيذ افتراضي. + +### المعرّف {#the-identifier} + +```python +--8<-- "docs_src/extensions/tutorial002.py" +``` + +المعرّف نص بصيغة `vendor-prefix/name` يتبع قواعد مفاتيح `_meta` +في المواصفة: مقاطع تفصلها نقاط (يبدأ كل منها بحرف وينتهي بحرف أو +رقم)، ثم شرطة مائلة، ثم الاسم. يُتحقق منه **عند تعريف الصنف**، فلا ينتظر +الخطأ الكتابي بدء تشغيل الخادم: + +```text +TypeError: Stamps.identifier must be a `vendor-prefix/name` string +(reverse-DNS prefix required), got 'stamps' +``` + +استخدم نطاقًا تملكه للبادئة. خُصص `io.modelcontextprotocol/*` للامتدادات +التي يحددها مشروع MCP نفسه. + +### إضافة أدوات {#contributing-tools} + +أبسط امتداد مفيد هو أداة واحدة وخريطة إعدادات: + +```python title="server.py" hl_lines="16 18-19 21-22 25" +--8<-- "docs_src/extensions/tutorial003.py" +``` + +* تعيد `tools()` قيم `ToolBinding`. يسجّل الخادم كل واحدة كما لو أنك + استدعيت `mcp.add_tool(...)` بنفسك: توليد المخطط نفسه، وحقن `Context` + نفسه، وكل السلوك نفسه. +* تمثل `settings()` القيمة المعلنة في `capabilities.extensions["com.example/stamps"]`. + أعِد `{}` (الافتراضي) للإعلان عن الامتداد دون إعدادات. +* لا يتلقى الامتداد الخادم مطلقًا. يعلن إضافاته كبيانات؛ + ويستهلكها `MCPServer`. لا توجد `self.server` لتعديلها. + +شغّله عبر HTTP، وسيوضح العميل النتيجة: + +```console +uv run mcp run server.py --transport streamable-http +``` + +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/extensions/tutorial003_client.py" +``` + +يُشغّل كل `server.py` في هذه الصفحة بهذا الأمر، ويُشغّل كل `client.py` +إلى جانبه باستخدام `python client.py` في طرفية ثانية. + +### خدمة طرائقك الخاصة {#serving-your-own-methods} + +يمكن للامتداد تسجيل **طرائق طلب جديدة**: عملياته الخاصة، التي تُخدم إلى جانب +طرائق المواصفة: + +```python title="server.py" hl_lines="14-20 24 33-41" +--8<-- "docs_src/extensions/tutorial004.py" +``` + +* يشتق `SearchParams` من `RequestParams`، فيُحلّل غلاف `_meta` لعام 2026 + باتساق، وتتلقى دالتك مَعلمات متحققًا منها، وليس قاموسًا خامًا. قيّد ما + يتحكم فيه العميل: ترفض `Field(ge=1, le=100)` قيمة `limit` غير المعقولة قبل + أن تخصص شيفرتك أي موارد لها. +* تمثل `require_client_extension(ctx, EXTENSION_ID)` شرط الدخول: يحصل العميل الذي لم + يعلن الامتداد على الخطأ `-32021` (قدرة عميل مطلوبة مفقودة)، + مع حمولة `requiredCapabilities` القابلة للقراءة آليًا التي تطلبها المواصفة. +* تثبّت `protocol_versions=frozenset({"2026-07-28"})` الطريقة على إصدار نقل واحد. + في أي إصدار آخر، يحصل العميل على `METHOD_NOT_FOUND`، كما لو أن الطريقة + غير موجودة هناك. وهي غير موجودة بالفعل بالنسبة إلى ذلك العميل. + +الطرائق **إضافية حصرًا**. تفرض SDK ذلك عند الإنشاء، وليس أثناء +التشغيل: + +* ترفع `MethodBinding` لطريقة تحددها المواصفة (`tools/list` أو `completion/complete` أو غيرهما) + الاستثناء `ValueError` عند إنشاء الربط. الطرائق الأساسية من اختصاص الخادم. +* إذا ربط امتدادان الطريقة نفسها، يُرفع استثناء عند تسجيل الثاني. + السماح لآخر كتابة بالتغلب على السابقة يتيح للإضافات إفساد بعضها؛ ولذلك لا يُسمح به. +* تثير مجموعة `protocol_versions` الفارغة استثناءً أيضًا: الطريقة التي لا يمكن خدمتها مطلقًا + خطأ برمجي، وليست إعدادًا. + +### جانب العميل {#the-client-side} + +العميل برنامج مستقل، ويحمل جزأي العمل الخاصين به: + +```python title="client.py" hl_lines="21-23 27-30" +--8<-- "docs_src/extensions/tutorial004_client.py" +``` + +* تعلن `Client(..., extensions=[advertise(EXTENSION_ID)])` الامتداد. تصبح + الإعلانات `ClientCapabilities.extensions`: في اتصال 2026-07-28، + تُنقل الخريطة في غلاف `_meta` لكل طلب، فيراها الخادم في + **كل** طلب؛ وفي الاتصال القديم، تُنقل في مصافحة `initialize`. + لا تحتاج شيفرة الخادم إلى التمييز: تقرأ `require_client_extension(ctx, ...)` و + `ctx.session.check_client_capability(...)` المصدر الصحيح في المسارين. +* تنزل طرائق المورّد مستوى واحدًا إلى `client.session.send_request(...)`؛ لا يضيف `Client` + طرائق مباشرة إلا لعمليات المواصفة. تقبل `send_request` أي + صنف مشتق من `Request`، ولذلك يمر طلب المورّد كما هو. +* يمثل `SearchRequest` والنموذجان اللذان يحملهما عقد الامتداد على الشبكة، + ولذلك يعرّفها العميل لنفسه. أما الامتداد المنشور فيقدّمها في + حزمة يستوردها الطرفان. + +### اعتراض `tools/call` {#intercepting-toolscall} + +هذه هي دالة الاعتراض الوحيدة. تجاوز `intercept_tool_call` لمراقبة +استدعاء أداة أو اختصار مساره أو رفضه: + +```python title="server.py" hl_lines="17-24" +--8<-- "docs_src/extensions/tutorial005.py" +``` + +* `params` هي `CallToolRequestParams` المتحقق منها: تحصل على `params.name` و + `params.arguments` دون التعامل مع JSON الخام. وهي أيضًا ما يحدد + استدعاء الأداة الذي يُنفّذ: يغيّر تمرير سياق معدّل عبر `call_next` ما + تراه الدالة في `ctx`، وليس استدعاء الأداة. إعادة كتابة الطلب على مستوى النقل + من اختصاص [البرمجيات الوسيطة](middleware.md). +* تنفّذ `call_next(ctx)` بقية السلسلة وتعيد نتيجة دالة المعالجة. + أعِدها دون تغيير (للمراقبة)، أو أعِد شيئًا آخر (للاستبدال)، أو ارفع + `MCPError` (للرفض). تُسلسل كل قيمة تعيدها كأي نتيجة دالة معالجة، + بما في ذلك وسم هوية `serverInfo` من جيل 2026، ولذلك لا ينتج + المعترض الذي يختصر المسار ردًا مجهول الهوية أو مخالفًا + للمخطط. +* مع امتدادات متعددة، تتداخل المعترضات بترتيب التسجيل: يكون الامتداد الأول + في `extensions=[...]` هو الخارجي. +* يمرّر التنفيذ الافتراضي الاستدعاء مباشرة، ويحتفظ الخادم الذي لا تتجاوز امتداداته + هذه الدالة بدالة `tools/call` الأصلية دون تغيير. لا تتحمل + تكلفة ما لا تستخدمه. + +تغلّف الدالة `tools/call` فقط. للشؤون التي تخص كل رسالة، استخدم +[البرمجيات الوسيطة](middleware.md). فهذا دورها. + +## استخدام امتداد عميل {#using-a-client-extension} + +**امتداد العميل** هو العقد نفسه من جانب المستهلك: حزمة من +سلوك العميل تحت معرّف واحد. يجيب الخادم هنا عن `buy` بإيصال +يمكن استبداله بالبضاعة بدلًا من البضاعة نفسها، وفقط للعميل الذي أعلن +الامتداد: + +```python title="server.py" hl_lines="22-25" +--8<-- "docs_src/extensions/tutorial006.py" +``` + +في العميل، مرّر النسخ إلى `Client(extensions=[...])` واستدعِ الأدوات كالمعتاد: + +```python title="client.py" hl_lines="33-35" +--8<-- "docs_src/extensions/tutorial006_client.py" +``` + +تعيد `call_tool("buy", ...)` قيمة `CallToolResult` عادية، كأي استدعاء آخر. ما +غيّره الامتداد هو أن الخادم يستطيع الآن الإجابة عن `buy` بـ**شكل نتيجة** من نوع `receipt` +بدلًا من نتيجة نهائية، وتُكمل `Receipts` العملية (هنا باستبدال +الإيصال في استدعاء لاحق) قبل عودة `call_tool`. لا يتغير شيء في موضع +الاستدعاء. + +احذف الامتداد، وسيختفي هذا كله: يرفض شرط الخادم العميل +الذي لم يعلنه (الخطأ -32021)، ويفشل تحقق الشكل الذي يعلنه خادم +يتجاوز هذا الشرط، كما تفرض المواصفة تمامًا عند وجود +`resultType` غير معروف. الامتدادات معطّلة افتراضيًا في طرفي الاتصال. + +للإعلان عن معرّف **دون** سلوك على جانب العميل (يشترط الخادم +القدرة، ولا يفعل العميل شيئًا، كما في عميل البحث أعلاه)، استخدم +`advertise()`: + +```python +from mcp.client import advertise + +client = Client("http://localhost:8000/mcp", extensions=[advertise("com.example/search")]) +``` + +## كتابة امتداد عميل {#writing-a-client-extension} + +اشتق من `ClientExtension` وتجاوز ما تحتاجه فقط. توجد ثلاثة أنواع +من الإضافات، ولكل منها افتراضي: `settings()` و`claims()` و`notifications()`. + +```python title="client.py" hl_lines="16-17 25-26 28-29" +--8<-- "docs_src/extensions/tutorial006_client.py" +``` + +* يتبع المعرّف قواعد معرّف الخادم نفسها، ويُتحقق منه عند تعريف + الصنف. +* تعيد `claims()` قيم `ResultClaim`: وسمًا على الشبكة، ونموذجًا يحلّله، + ودالة حل تُكمله. يجب أن يثبّت النموذج الوسم باستخدام + `result_type: Literal["receipt"]`، وألا يشتق من أنواع النتائج الأساسية + للطريقة؛ ويُفرض الشرطان عند إنشاء التعريف. تُنقل حقول المورّد مثل + `receipt_token` كما هي: يصل الشكل البديل إلى العميل + حرفيًا. +* تتلقى دالة الحل النموذج المحلّل و`ClaimContext`؛ وتمثل `ctx.session` + الواجهة العامة نفسها التي تمثلها `client.session`، فالمتابعات استدعاءات جلسة عادية. + وتعيد `CallToolResult` المعتادة للطريقة. +* تمثل `settings()` القيمة المعلنة في `ClientCapabilities.extensions[identifier]`، + وتُقرأ مرة واحدة عند إنشاء `Client`. + +تعلن `notifications()` إشعارات خادم خاصة بالمورّد لمراقبتها: + +```python +def notifications(self) -> Sequence[NotificationBinding[Any]]: + return [NotificationBinding(method="notifications/receipts", params_type=ReceiptEvent, handler=self.on_receipt)] +``` + +تتلقى الدالة مَعلمات متحققًا منها، واحدة في كل مرة، بترتيب التوزيع. وهي تراقب فقط؛ لا تستطيع الرفض +أو الرد. + +قاعدتان إضافيتان. لا تنشط تعريفات أشكال النتائج إلا في اتصالات 2026-07-28، ويتبعها إعلان +القدرات: في الاتصال القديم، تُلغى التعريفات ويختفي المعرّف +من الإعلان معها، فلا يعلن العميل مطلقًا امتدادًا سيرفض +أشكاله. وعندما تريد الشكل المعلن نفسه بدلًا من دالة الحل، +استدعِ `client.session.call_tool(..., allow_claimed=True)`؛ دون هذا الخيار، +يرفع الشكل المعلن الذي يصل إلى مستدعٍ على مستوى الجلسة `UnexpectedClaimedResult`. + +### طرائق الامتداد {#extension-verbs} + +لا تحتاج طرائق طلب الامتداد الخاصة إلى تسجيل على جانب العميل. يشتق نوع طلب المورّد +من `mcp.types.Request` ويمر عبر `client.session.send_request`، +كما في [خدمة طرائقك الخاصة](#serving-your-own-methods). لنأخذ خادمًا يخدم +امتداده طريقة واحدة تتعلق بمهمة مسماة: + +```python title="server.py" hl_lines="12-13 30" +--8<-- "docs_src/extensions/tutorial007.py" +``` + +إضافة واحدة في العميل: عندما يجب أن يُنقل مفتاح مَعلمة في ترويسة `Mcp-Name` +(تتطلب مواصفات امتدادات مثل المهام ذلك لطرائقها)، يعلن نوع الطلب +`name_param`: + +```python title="client.py" hl_lines="20-23 28-29" +--8<-- "docs_src/extensions/tutorial007_client.py" +``` + +تعكس الجلسة `params["jobId"]` في `Mcp-Name` على كل مسار إرسال، +وتؤدي القيمة المفقودة إلى إخفاق واضح بدلًا من حذف ترويسة مطلوبة بصمت. + +## ما لا يستطيع الامتداد فعله {#what-an-extension-cannot-do} + +واجهة الإضافات **مغلقة** عن قصد. في الخادم: إعدادات وأدوات +وموارد وطرائق ومعترض واحد لـ`tools/call`. في العميل: إعدادات وتعريفات +أشكال نتائج وروابط إشعارات. لا يستطيع الامتداد: + +* **الوصول إلى داخل التطبيق المضيف.** يعلن بيانات؛ ولا يملك مرجعًا إلى الخادم أو العميل. +* **استبدال السلوك الأساسي.** تُرفض طرائق المواصفة ووسوم النتائج الأساسية عند + الإنشاء (يحجز مشغّل الاتصال `initialize` بالكامل)؛ أما ربط إشعار + تحجبه مفردات البروتوكول الأساسية، فيُعطّل مع تحذير بدلًا من ذلك. +* **التسجيل لاحقًا.** بعد عودة `MCPServer(...)` أو `Client(...)`، + تصبح مجموعة الامتدادات ثابتة. + +إذا حاولت تجاوز هذه الحدود، فأنت لا تكتب امتدادًا، بل نسخة مشتقة +من المشروع. الحدود هي الميزة: يعرف المستخدم الذي يقرأ `extensions=[Apps(), Stamps()]` +*كل* ما كان يمكن أن يؤثر فيه هذان الامتدادان. diff --git a/i18n/ar/pages/advanced/header-parameters.md b/i18n/ar/pages/advanced/header-parameters.md new file mode 100644 index 0000000000..12cb4cc339 --- /dev/null +++ b/i18n/ar/pages/advanced/header-parameters.md @@ -0,0 +1,65 @@ +--- +translation: + sections: [81862a209b483d27, b76e8073487afa03, bc214b2fc2bcdae4, d5835477c0b60163, a5d9786f902ad8e1] + tool: 1 +--- +# مَعلمات الترويسات {#header-parameters} + +لا تحتاج معظم الخوادم إلى هذه الميزة مطلقًا. + +لا تستطيع البوابة أو موازن الأحمال أمام خادمك توجيه الطلب إلا وفق ما تقرؤه دون تحليل الجسم. علّم وسيطة أداة بـ`x-mcp-header`، وسترسل العملاء على **[إصدار البروتوكول](../protocol-versions.md)** `2026-07-28` قيمتها في ترويسة HTTP أيضًا. + +## تعليم وسيطة {#mark-an-argument} + +الوسم مفتاح إضافي واحد في JSON Schema الخاص بالوسيطة. في `MCPServer`، تضعه `Field` هناك: + +```python title="server.py" hl_lines="13" +--8<-- "docs_src/header_parameters/tutorial001.py" +``` + +* عبر Streamable HTTP على `2026-07-28`، يرسل العميل `Mcp-Param-Region` إلى جانب الجسم، ويرفض الخادم الاستدعاء إذا اختلفا. +* العميل الذي لم يجلب قائمة الأدوات لم ير الوسم: لا يرسل ترويسة، فيُرفض الاستدعاء. يجلب `Client` في SDK هذه قائمة الأدوات حينها ويعيد الاستدعاء مرة واحدة، ولذلك فإن جلب القائمة أولًا يوفّر جولة طلب ورد فقط. +* تتجاهل كل الاتصالات الأخرى التعليق. + +لا تتغير دالتك: تظل `region` تصل كوسيطة. + +## ما يمكن تعليمه {#what-can-be-marked} + +وسيطات `str` و`int` و`bool`. يُرفض أي نوع آخر عند تسجيل الأداة باستخدام `InvalidSignature`. + +يشمل ذلك `str | None`، الذي ليس له نوع واحد. تحتاج الوسيطة الاختيارية إلى كتابة مخططها صراحةً باستخدام `WithJsonSchema` في pydantic: + +```python +region: Annotated[str | None, WithJsonSchema({"type": "string", "x-mcp-header": "Region"})] = None +``` + +## في `Server` منخفض المستوى {#on-the-low-level-server} + +هناك تكتب `input_schema` يدويًا، فيُضاف المفتاح مباشرة: + +```python title="server.py" hl_lines="18" +--8<-- "docs_src/header_parameters/tutorial002.py" +``` + +* لا يتحقق شيء من التعليق نيابة عنك: يُقدّم التعليق غير الصالح، وتستبعد العملاء على `2026-07-28` الأداة من قائمتها. + +### المخططات حسب الاسم {#schemas-by-name} + +للتحقق من الترويسة، تحتاج SDK إلى مخطط إدخال الأداة قبل توجيه الاستدعاء. دون `get_tool_input_schema`، تحصل عليه بتشغيل دالة المعالجة `on_list_tools` في كل استدعاء يحمل وسيطات، سواء أكانت أي أداة معلّمة أم لا. + +```python title="server.py" hl_lines="26 39-41 48" +--8<-- "docs_src/header_parameters/tutorial003.py" +``` + +* مرّر الدالة لتجيب باستخدام ما تملكه بالفعل. +* أعِد `None` لأداة ليس لديها ما يحتاج إلى فحص. + +## مراجعة {#recap} + +* يؤدي وضع `x-mcp-header` على وسيطة أداة إلى تكرار قيمتها في ترويسة HTTP باسم `Mcp-Param-*` لدى عملاء `2026-07-28`. +* يرفض الخادم الاستدعاء إذا اختلفت الترويسة عن الجسم. +* لا يمكن تعليم سوى وسيطات `str` و`int` و`bool`. يرفع `MCPServer` الاستثناء `InvalidSignature` لأي نوع آخر. +* لا يفحص `Server` منخفض المستوى أي شيء، وتستبعد العملاء الأداة ذات التعليق غير الصالح. +* تمنع `get_tool_input_schema` تشغيل `on_list_tools` في `Server` منخفض المستوى عند كل استدعاء. + +تشرح صفحة **[الخادم منخفض المستوى](low-level-server.md)** بقية واجهة `Server` المكتوبة يدويًا. diff --git a/i18n/ar/pages/advanced/index.md b/i18n/ar/pages/advanced/index.md new file mode 100644 index 0000000000..24d05799bd --- /dev/null +++ b/i18n/ar/pages/advanced/index.md @@ -0,0 +1,36 @@ +--- +translation: + sections: [348f8697c6b12cd0] + tool: 1 +--- +# موضوعات متقدمة {#advanced} + +تغطي الأقسام السابقة كل ما يحتاجه خادم أو عميل عادي في موضعه المناسب. +يضم هذا القسم منافذ التحكم المباشر التي تلجأ إليها عندما تعيقك طبقة التسهيل +في `MCPServer`: + +* **[الخادم منخفض المستوى](low-level-server.md)**: الصنف الذي يُبنى عليه `MCPServer`. + مخططات مكتوبة يدويًا، ودوال معالجة `on_*`، دون فحوص نيابة عنك، وطرائق JSON-RPC + مخصصة من تصميمك. +* **[تقسيم النتائج إلى صفحات](pagination.md)** و**[البرمجيات الوسيطة](middleware.md)**: ميزتان لا يمكنك + استخدامهما *إلا* في `Server` منخفض المستوى. +* **[مَعلمات الترويسات](header-parameters.md)**: دع بوابة توجّه استدعاء أداة وفق قيمة + إحدى وسيطاته. +* **[الامتدادات](extensions.md)** و**[MCP Apps](apps.md)**: واجهة امتداد + البروتوكول. أضف حزم امتدادات إلى خادم، أو اكتب امتداداتك الخاصة. + +توجد بعض الموضوعات التي قد تتوقع العثور عليها هنا في المواضع التي تستخدمها +فيها فعليًا: + +* يوجد **التفويض** ضمن **[تشغيل خادمك](../run/index.md)** لأنك تحمي + الخادم في موضع نشره. +* توجد **OAuth** و**إفادة الهوية** والاتصال بـ**خوادم متعددة** و**ذاكرة التخزين + المؤقت** للردود ضمن **[العملاء](../client/index.md)**. +* توجد **الطلبات متعددة جولات الطلب والرد** و**الاشتراكات** ضمن + **[داخل دالة المعالجة](../handlers/index.md)** لأن كلتيهما سلوك تنفّذه + دالة المعالجة. +* توجد **قوالب URI** ضمن **[الخوادم](../servers/index.md)** إلى جانب الموارد. +* لكل من **[إصدارات البروتوكول](../protocol-versions.md)** و + **[الميزات المهملة](../deprecated.md)** صفحة مستقلة في المستوى الأعلى. + +إذا لم تكن متأكدًا من حاجتك إلى هذا القسم، فلست بحاجة إليه. diff --git a/i18n/ar/pages/advanced/low-level-server.md b/i18n/ar/pages/advanced/low-level-server.md new file mode 100644 index 0000000000..54f8b36245 --- /dev/null +++ b/i18n/ar/pages/advanced/low-level-server.md @@ -0,0 +1,225 @@ +--- +translation: + sections: [2c79b6338e09b7ac, 9d5d10a5f0405d0a, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, 2090d99b355bc2c7, 0fde3bcea081ba3a] + tool: 1 +--- +# الخادم منخفض المستوى {#the-low-level-server} + +تمثل `@mcp.tool()` طبقة. وتحتها صنف خادم آخر، هو `Server`، يتعامل مباشرة مع MCP: تعطيه كائنات البروتوكول، فيرسلها دون تغيير. + +يُبنى `MCPServer` فوقه. تلجأ إلى المستوى الأدنى عندما تعيقك طبقة التسهيل: + +* تحتاج إلى إصدار مخطط **مطابق تمامًا** (محمّل من ملف أو مولّد من قاعدة بيانات)، لا مخطط مشتق من توقيع Python. +* تحتاج إلى تحكم كامل في النتيجة: `_meta` و`is_error` وكل مفتاح في `structured_content`. +* تحتاج إلى معالجة طريقة لا يحددها MCP. + +في كل الحالات الأخرى، ابقَ على `MCPServer`. + +## الأداة نفسها، يدويًا {#the-same-tool-by-hand} + +هذه أداة `search_books` التي تكتبها صفحة **[الأدوات](../servers/tools.md)** في تسعة أسطر باستخدام `@mcp.tool()`، بعد إزالة التسهيلات: + +```python title="server.py" hl_lines="22 26 32" +--8<-- "docs_src/lowlevel/tutorial001.py" +``` + +تغيرت ثلاثة أمور، وهي واجهة API منخفضة المستوى بأكملها: + +* **دوال المعالجة مَعلمات للمُنشئ.** تدخل `on_list_tools=` و`on_call_tool=` في `Server(...)`. لا توجد مزخرفات هنا، وكل دالة معالجة لها الشكل نفسه: `async (ctx, params) -> result`. +* **تكتب مخطط الإدخال.** `Tool.input_schema` قاموس `dict` عادي لـJSON Schema. لا يشتقه شيء من تلميحات الأنواع، فلا توجد تلميحات أنواع يُشتق منها. +* **تبني النتيجة.** تكتب `CallToolResult(content=[TextContent(...)])` يدويًا. لا تغليف ولا تحويل ولا استنتاج من تعليق نوع الإرجاع. + +تمثل `params` الطلب المحلّل: تعطيك `CallToolRequestParams` حقلي `.name` و`.arguments`. أما `ctx` فهي `ServerRequestContext`: تتضمن `ctx.session` للتواصل مع العميل، و`ctx.lifespan_context` و`ctx.request_id` و`ctx.meta`، أي `_meta` الواردة في الطلب. + +!!! info + إذا استخدمت FastAPI، فأنت تعرف هذه العلاقة بالفعل. `MCPServer` طبقة المزخرفات وتلميحات الأنواع؛ و`Server` هو Starlette تحتها. لا يتنافسان: ينشئ `MCPServer` نسخة `Server` ويسجّل عليها دوال معالجة مثل هذه تمامًا. + +### جرّبه {#try-it} + +لا تقبل `mcp dev` و`mcp run` سوى `MCPServer`، ولذلك تشغّل هذا الخادم بنفسك. يبني السطر الأخير من `server.py` تطبيق ASGI عاديًا منه، ويشغّله uvicorn: + +```console +uvicorn server:app --port 8000 +``` + +وجّه Inspector أو أي عميل إلى `http://localhost:8000/mcp`: + +```python title="client.py" +import asyncio + +from mcp import Client + + +async def main() -> None: + async with Client("http://localhost:8000/mcp") as client: + result = await client.call_tool("search_books", {"query": "dune", "limit": 5}) + print(result.content) + + +asyncio.run(main()) +``` + +```text +[TextContent(type='text', text="Found 3 books matching 'dune' (showing up to 5).", annotations=None, meta=None)] +``` + +النص نفسه الذي أنتجته نسخة `@mcp.tool()`. مع اختلافين واضحين: + +* تكون `result.structured_content` مساوية لـ`None`. يغلّف الخادم عالي المستوى قيمة `-> str` في `{"result": ...}` نيابة عنك؛ أما هنا فلا يبني شيء ما لم تبنه أنت. +* تعيد `list_tools` المخطط الذي كتبته **أنت**، حرفًا بحرف. تضمنت النسخة عالية المستوى `"title": "Query"` في كل خاصية، و`"title": "search_booksArguments"` في الجذر: إضافات Pydantic. هنا، إذا ظهر شيء في البيانات المنقولة، فأنت وضعته هناك. + +في الاختبار، تتجاوز uvicorn والمنفذ: تقبل `Client(server)` خادم `Server` منخفض المستوى داخل العملية كما تقبل `MCPServer` تمامًا، وتعرض صفحة **[الاختبار](../get-started/testing.md)** هذا النمط. + +## لا فحوص نيابة عنك {#nothing-is-checked-for-you} + +يرفض `MCPServer` الوسيطة غير الصالحة قبل تشغيل دالتك، ويتحقق من الاستدعاء مقابل المخطط الذي ولّده (**[الأدوات](../servers/tools.md)**). + +لا يفعل `Server` ذلك. يُعلَن `input_schema` للعميل، لكنه لا يُطبّق مطلقًا على `params.arguments`. + +!!! check + استدعِ `search_books` دون `limit`، وسترفع `args["limit"]` الاستثناء `KeyError`. يرى العميل: + + ```text + MCPError: Internal server error + ``` + + خطأ JSON-RPC برمز `-32603` ورسالة عامة عمدًا: لا تسرّب SDK تتبّع استثنائك إلى مستدعٍ بعيد. لا يعرف النموذج ما أخطأ فيه، فلا يستطيع إعادة المحاولة. (في الاختبار، يعرض `raise_exceptions=True` الاستثناء الفعلي بدلًا من ذلك؛ راجع **[الاختبار](../get-started/testing.md)**.) + +تنطبق القاعدة عمومًا. الاستثناء الذي ترفعه دالة معالجة منخفضة المستوى هو **دائمًا** خطأ بروتوكول، وليس نتيجة أداة تحمل `is_error=True`. إذا أردت أن يقرأ النموذج الإخفاق ويتعافى منه، فتحقق من `params.arguments` بنفسك وأعِد `CallToolResult(content=[TextContent(...)], is_error=True)`. تشرح صفحة **[معالجة الأخطاء](../servers/handling-errors.md)** نوعي الإخفاق. + +## أداتان ودالة معالجة واحدة {#two-tools-one-handler} + +تمثل `on_call_tool` نقطة الدخول الوحيدة لكل أداة في الخادم. توجّه الاستدعاء وفق `params.name`: + +```python title="server.py" hl_lines="38-43" +--8<-- "docs_src/lowlevel/tutorial002.py" +``` + +* تعلن `list_tools` كلتيهما. وتوجّه `call_tool` وفق الاسم. +* فرع `else` مهم: يمرّر `Server` طلب `tools/call` لاسم لم تدرجه مطلقًا إلى دالتك مباشرة. رفع استثناء هناك يحوّل الاستدعاء إلى `-32603` نفسه أعلاه. + +## المخرجات المنظّمة، يدويًا {#structured-output-by-hand} + +أعلن `output_schema` في `Tool` وضع `structured_content` في النتيجة. كلاهما مسؤوليتك: + +```python title="server.py" hl_lines="19-23 36" +--8<-- "docs_src/lowlevel/tutorial003.py" +``` + +استدعِ الأداة، فتحمل النتيجة التمثيلين: + +```json +{ + "content": [{"type": "text", "text": "Found 3 books matching 'dune'."}], + "structuredContent": {"matches": 3, "query": "dune"}, + "isError": false, + "resultType": "complete", + "_meta": {"io.modelcontextprotocol/serverInfo": {"name": "Bookshop", "version": "2.0.0"}} +} +``` + +كتلة `_meta` وسم هوية الخادم: تضيفها SDK إلى كل نتيجة من جيل 2026، مع `version` من المُنشئ (يعرض الخادم الذي لا يعيّنها نصًا فارغًا). يستطيع الخادم الذي يجب ألا يعرّف نفسه حذف المفتاح باستخدام برمجية وسيطة تتحكم في النتائج التي تعيدها. + +لا يقارن الخادم الحقلين مطلقًا. لكن `Client` في SDK يفعل: إذا أعدت `structured_content` لا تطابق `output_schema` الذي أعلنته، ترفع `call_tool` استثناء `RuntimeError` يبدأ بـ`Invalid structured content returned by tool search_books` ويتبعه تفاصيل إخفاق `jsonschema`. إعلان المخطط سهل؛ والوفاء به مسؤوليتك. تجد التسلسل الكامل لأنواع الإرجاع والمخططات في **[المخرجات المنظّمة](../servers/structured-output.md)**. + +## الصيغة هي JSON Schema 2020-12 {#the-dialect-is-json-schema-2020-12} + +يمثل `input_schema` و`output_schema` مخططات JSON Schema، وتحدد [مواصفة MCP](https://modelcontextprotocol.io/specification/latest/basic#json-schema-usage) الصيغة: المخطط الذي لا يتضمن مفتاح `$schema` هو **JSON Schema 2020-12**. تعتمد مخططات `MCPServer` المولّدة على هذا الافتراضي (تكتب Pydantic صيغة 2020-12 وتحذف المفتاح)، ويخضع القاموس المكتوب يدويًا له أيضًا، فتتوفر مفردات 2020-12 كاملةً: + +```python title="server.py" hl_lines="8 14-15" +--8<-- "docs_src/lowlevel/tutorial007.py" +``` + +* يجب أن يكون جذر `input_schema` هو `"type": "object"`. وإلى جانبه تصل `oneOf` و`additionalProperties` و`anyOf` و`if`/`then`/`else` و`prefixItems` و`$defs` مع مراجع `$ref` المحلية وبقية الكلمات المفتاحية في 2020-12 إلى العميل كما كُتبت تمامًا. +* لا حاجة لمفتاح `$schema`. أضفه فقط لاختيار مسودة أقدم: يختار `Client` في SDK، الذي يتحقق من `structured_content` مقابل `output_schema` للأداة، المتحقق وفق `$schema`، ويستخدم 2020-12 عند غيابه. + +## `_meta`: للتطبيق، لا للنموذج {#\_meta-for-the-application-not-the-model} + +يمثل `content` جزء الإجابة الذي يقرؤه النموذج. ويمثل `structured_content` الإجابة نفسها كبيانات ذات أنواع. أما `_meta` فهي القناة الثالثة: بيانات ترافق النتيجة من أجل **تطبيق العميل**، دون أن تكون جزءًا من الإجابة أصلًا. + +استخدمها لمعرّفات السجلات والتتبّع وأي شيء تحتاجه واجهة المستخدم ولا يحتاجه قالب التوجيه: + +```python title="server.py" hl_lines="37" +--8<-- "docs_src/lowlevel/tutorial004.py" +``` + +* تنشئها باسم `_meta=`، وهو الاسم على الشبكة. يقرؤها العميل عبر `result.meta`. +* ضع مفاتيحك في نطاق أسماء (`bookshop/record_ids`). مفاتيح `io.modelcontextprotocol/*` محجوزة للبروتوكول. + +!!! warning + `_meta` اتفاق بينك وبين تطبيق العميل، وليست ضمانًا لما يصل + إلى النموذج. يحدد التطبيق المضيف ما يعرضه. لا تضع سرًا في أي جزء من نتيجة أداة. + +## القدرات تتبع دوال المعالجة {#capabilities-follow-your-handlers} + +يعلن `Server` عائلات الطرائق التي زوّدته بدوال معالجة لها فقط. يمرّر `Bookshop` أعلاه `on_list_tools` و`on_call_tool` دون غيرهما، ولذلك يرى العميل المتصل به: + +```json +{"tools": {"listChanged": false}} +``` + +لا `resources` ولا `prompts`: لا توجد دوال تدعمهما. مرّر `on_list_prompts` فتظهر `prompts`؛ ومرّر `on_completion` فتظهر `completions`. + +يعلن `MCPServer` دائمًا الأدوات والموارد وقوالب التوجيه، سواء سجّلت أيًا منها أم لا، لأن مديريها موجودون دائمًا. في هذا المستوى، يكون الإعلان *هو* استدعاء المُنشئ. + +## النوع العام لدورة الحياة {#the-lifespan-generic} + +يستخدم `Server` نوعًا عامًا وفق النوع الذي تنتجه دورة حياته. علّق نوعه مرة واحدة، وسيُعرف نوع الكائن أينما ظهر: + +```python title="server.py" hl_lines="24-26 44-45 50" +--8<-- "docs_src/lowlevel/tutorial005.py" +``` + +* دورة الحياة من النوع `Callable[[Server[Catalog]], AbstractAsyncContextManager[Catalog]]`؛ ويعطيك تطبيق `@asynccontextmanager` على مولّد `async` هذا النوع تمامًا. +* تصبح القيمة التي ينتجها عبر `yield` هي `ctx.lifespan_context`، وبما أن دوال المعالجة معلّقة بنوع `ServerRequestContext[Catalog]`، يتوفر الإكمال التلقائي وفحص الأنواع لـ`.search(...)`. +* يُدخل سياقها مرة عند بدء الخادم ويُخرج منه مرة عند توقفه. تعرض صفحة **[دورة الحياة](../handlers/lifespan.md)** بدء التشغيل والتنظيف ونسخة `MCPServer` من الفكرة نفسها. + +دون `lifespan=`، تكون `ctx.lifespan_context` قاموس `dict` فارغًا. + +## طريقة خاصة بك {#a-method-of-your-own} + +يغطي المُنشئ الطرائق التي يحددها MCP. وتغطي `add_request_handler` كل ما عداها: + +```python title="server.py" hl_lines="35-36 39-40 43-44 48" +--8<-- "docs_src/lowlevel/tutorial006.py" +``` + +* الوسيطة الأولى هي نص الطريقة. وللإشعارات نظير هو `add_notification_handler`. تعمل دواله على stdio واتصالات HTTP من جيل المصافحة؛ أما على مسار streamable-HTTP لـ`2026-07-28`، فيُقر POST إشعار العميل برمز `202` ولا يُوزّع، لأن ذلك الإصدار لا يحدد إشعارات من العميل إلى الخادم عبر HTTP. +* `params_type` هو النموذج الذي تُفحص `params` الواردة مقابله **قبل** تشغيل دالتك، ولذلك تحصل الطرائق المخصصة على التحقق الذي لا تحصل عليه الأدوات. اشتق من `RequestParams` حتى يُحلّل حقل `_meta` كما في بقية الطرائق. +* تعيد الدالة `BaseModel` أو `dict` أو `None`. تسلسلها SDK في نتيجة JSON-RPC. + +تنبيه واضح: لا يتضمن `Client` عالي المستوى إلا طرائق MCP المحددة، ولذلك لا توجد `client.reindex()`. الطريقة الخاصة بمورّد موجّهة إلى طرف يعرف وجودها بالفعل: عميل تقدّمه أنت أيضًا، أو خدمة أخرى لك تتحدث JSON-RPC. + +طريقة واحدة لا تستطيع تولّيها: + +```text +ValueError: 'initialize' is handled by the server runner and cannot be overridden; +use Server.middleware to observe or wrap initialization +``` + +المصافحة من اختصاص مشغّل الاتصال. أما `server/discover` و`ping` وكل طريقة مدمجة أخرى، فيمكنك استبدالها. + +!!! tip + تغلّف `Server.middleware`، المذكورة في ذلك الخطأ، **كل** رسالة واردة، بما فيها `initialize`. إذا أردت مراقبة الحركة أو إعادة كتابتها بدلًا من الإجابة عن طريقة جديدة، فابدأ بـ**[البرمجيات الوسيطة](middleware.md)**. + +## دوال المعالجة الأخرى {#the-other-handlers} + +تمثل كل واحدة من هذه فكرة أصبحت تعرف مفرداتها؛ ولكل منها صفحتها. + +* يمكن أن تعيد `on_call_tool` و`on_get_prompt` و`on_read_resource` قيمة `InputRequiredResult` بدلًا من نتيجتها المعتادة لإيقاف الاستدعاء مؤقتًا وطلب إدخال من العميل؛ راجع **[الطلبات متعددة جولات الطلب والرد](../handlers/multi-round-trip.md)**. وكما يليق بهذا المستوى، لا يُثبّت شيء نيابة عنك: بينما تحمي `MCPServer` قيمة `requestState` افتراضيًا، تعبر `request_state` التي تعيّنها هنا الشبكة كما كُتبت حتى تفعّل `server.middleware.append(RequestStateBoundary(RequestStateSecurity(keys=[...]), default_audience=server.name))`: سطر واحد (يمكن استيراد الاسمين من `mcp.server.request_state`) للحماية والتحقق نفسيهما اللذين تنفّذهما `MCPServer` (**[حماية `requestState`](../handlers/multi-round-trip.md#protecting-requeststate)**). +* تستخدم `on_list_resources` و`on_read_resource` و`on_list_prompts` و`on_get_prompt` و`on_completion` الشكل نفسه `(ctx, params) -> result` لبقية العناصر الأساسية. +* تخدم `on_subscriptions_listen` تدفّق `subscriptions/listen` في 2026-07-28. مرّر `ListenHandler` مبنية على `SubscriptionBus` وانشر الأحداث إلى الناقل من دوالك الأخرى؛ راجع **[الاشتراكات](../handlers/subscriptions.md)** للتركيب الكامل. +* تُبقي `get_tool_input_schema` دالة `on_list_tools` خارج مسار الاستدعاء؛ راجع **[مَعلمات الترويسات](header-parameters.md#schemas-by-name)**. +* تعيد `server.streamable_http_app()` تطبيق Starlette نفسه الذي تعيده `MCPServer`؛ انشره كما تنشر صفحة **[تشغيل خادمك](../run/index.md)** أي تطبيق ASGI آخر. لا توجد `server.run(transport=...)` هنا: تدير `server.run(read_stream, write_stream, server.create_initialization_options())` اتصالًا واحدًا عبر زوج من التدفقات، وهذا السطر الواحد هو الآلية كاملةً. + +## مراجعة {#recap} + +* يأخذ `Server` منخفض المستوى دوال معالجته كـ**مَعلمات للمُنشئ** باسم `on_*`؛ وكل دالة معالجة هي `async (ctx, params) -> result`. +* تكتب قاموس `input_schema` وتبني `CallToolResult`. لا اشتقاق ولا تغليف ولا تحقق نيابة عنك. +* الاستثناء في دالة المعالجة خطأ بروتوكول `-32603`. أما خطأ الأداة الذي يقرؤه النموذج فهو `CallToolResult` مع `is_error=True` تعيدها **أنت**. +* تُوجّه `_meta` في النتيجة إلى تطبيق العميل، وليس إلى النموذج. +* يعتمد النوع العام `Server[T]` على ما تنتجه دورة حياته؛ وتكون `ctx.lifespan_context` من النوع `T`. +* تخدم `add_request_handler(method, params_type, handler)` أي طريقة. أما `initialize` فمحجوزة. +* تُشتق القدرات التي يعلنها `Server` من دوال المعالجة التي سجّلتها. + +عامل العميل الخادمين بالطريقة نفسها لأنهما يستخدمان البروتوكول نفسه فعلًا، وهذا هو المقصود. أما المستوى الأدنى التالي فليس صنفًا أصلًا: إنه **[البرمجيات الوسيطة](middleware.md)**. diff --git a/i18n/ar/pages/advanced/middleware.md b/i18n/ar/pages/advanced/middleware.md new file mode 100644 index 0000000000..35e24d25a4 --- /dev/null +++ b/i18n/ar/pages/advanced/middleware.md @@ -0,0 +1,146 @@ +--- +translation: + sections: [58e1103d9a323ccf, 46056f318ef205e4, 812b414557fb0c35, 4df162eea2518d38, c62422b159c6ed09, 420968f514138f43] + tool: 1 +--- +# البرمجيات الوسيطة {#middleware} + +**البرمجية الوسيطة** دالة غير متزامنة واحدة تغلّف كل رسالة يتلقاها خادمك. + +تكتبها بالشكل `async (ctx, call_next)` وتضيفها إلى `server.middleware`. وهذه هي واجهة API كاملةً. + +!!! warning + قائمة البرمجيات الوسيطة موسومة بأنها **مؤقتة** في المصدر: قد يتغير توقيعها ودلالاتها + في إصدار فرعي من 2.x. استخدمها *للمراقبة* (التوقيت والتسجيل والتتبّع) و + *لرفض* الرسائل؛ ولا تجعلها الأساس الذي يعتمد عليه خادمك. + +يأخذ `MCPServer` القائمة عند الإنشاء (`MCPServer(name, middleware=[...])`) ويعرضها عبر +`mcp.middleware`؛ ويعرض `Server` منخفض المستوى القائمة نفسها عبر `server.middleware`. تستخدم الأمثلة +أدناه `Server` منخفض المستوى؛ إذا لم تكن تعرف `Server(name, on_call_tool=...)`، فاقرأ +**[الخادم منخفض المستوى](low-level-server.md)** أولًا. + +## برمجية وسيطة لقياس الزمن {#a-timing-middleware} + +خادم واحد، وأداة واحدة، وبرمجية وسيطة واحدة تسجّل زمن معالجة كل رسالة: + +```python title="server.py" hl_lines="39-45 49" +--8<-- "docs_src/middleware/tutorial001.py" +``` + +* `ctx` هو `ServerRequestContext` نفسه الذي تتلقاه دوال المعالجة. تمثل `ctx.method` نص + الطريقة الخام؛ وتمثل `ctx.params` المَعلمات الخام **قبل** أي تحقق. +* تشغّل `call_next(ctx)` بقية السلسلة: التحقق، والعثور على دالة المعالجة، ثم دالتك. + أعِد ما تعيده، وسيبقى الرد دون تغيير. +* استخدام `try`/`finally` مقصود: تظل الدالة التي تثير استثناءً خاضعة لقياس الزمن، لأن الإخفاق + يصل إلى برمجيتك الوسيطة كاستثناء صادر عن `call_next`. +* تسجّلها `server.middleware.append(...)`. تُنفّذ القائمة بدءًا من الطبقة الخارجية، ولذلك + تكون `middleware[0]` الأقرب إلى البيانات المنقولة. + +### جرّبه {#try-it} + +صِل عميلًا، واجلب قائمة الأدوات، واستدعِ إحداها. يحتوي سجلك على **ثلاثة** أسطر: + +```text +server/discover took 18.3 ms +tools/list took 0.1 ms +tools/call took 0.1 ms +``` + +نفّذت استدعاءين وحصلت على ثلاثة أسطر. الأول هو `server/discover`: الطلب الذي أرسله +العميل لإعداد الاتصال قبل أن تطلب أي شيء. + +هذا هو المقصود. تغلّف البرمجيات الوسيطة **كل** رسالة واردة: + +* إعداد الاتصال: `server/discover`، أو `initialize` و`notifications/initialized` + في جلسة قديمة. +* كل طلب وكل إشعار يصل إلى الخادم. في الإشعار، تكون + `ctx.request_id is None`، وتعيد `call_next(ctx)` قيمة `None`، ويُهمل كل ما تعيده أنت. + (على مسار streamable-HTTP لـ`2026-07-28`، يُقر طلب POST لإشعار العميل برمز `202` في + وسيلة النقل ولا يُوزّع، ولذلك لا يصل إلى البرمجيات الوسيطة أيضًا؛ فلا يحدد هذا الإصدار + إشعارات من العميل إلى الخادم عبر HTTP.) +* حتى الطريقة التي ليس لها دالة معالجة في الخادم: ترفع `call_next` + الخطأ `MCPError(-32601, "Method not found")` *عبر* برمجيتك الوسيطة في طريقه إلى العميل. + +## حد للتزامن {#a-concurrency-cap} + +لا يلزم أن تستدعي البرمجية الوسيطة `call_next(ctx)`. ارفع `MCPError` بدلًا من ذلك، فتُرفض +تلك الرسالة **وحدها**: يبقى الاتصال قائمًا وتمر الرسالة التالية. + +لنفترض أن كل بحث يحجز اتصالًا من مجموعة من أربعة اتصالات. تسمح هذه البرمجية الوسيطة بأربعة استدعاءات أدوات +متزامنة وترفض الخامس: + +```python title="server.py" hl_lines="15-16 40-55 59" +--8<-- "docs_src/middleware/tutorial002.py" +``` + +* تُحسب `tools/call` فقط، ولذلك يواصل الخادم الإجابة عن `server/discover` و`tools/list` + أثناء رفض استدعاءات الأدوات. +* لا يحدد MCP رمز خطأ «الخادم مشغول»، ولذلك تخص `SERVER_BUSY` هذا الخادم. +* يُخبر الرفض العميل فورًا بأن الخادم محمّل فوق طاقته. إذا فضّلت جعل + المستدعين ينتظرون، فغلّف `call_next(ctx)` باستخدام `anyio.CapacityLimiter` بدلًا من ذلك. + +يصل `MCPError` المرفوع إلى تطبيق العميل، وليس إلى النموذج. إذا أردت أن يقرأ النموذج +الرسالة، فأعِد نتيجة أداة مع `is_error=True` بدلًا من ذلك: وهذا هو **الرد** أدناه. + +## ما يمكنك فعله داخلها {#what-you-can-do-inside-one} + +بالترتيب التصاعدي لمدى الحذر المطلوب: + +* **المراقبة.** قِس الزمن، وعدّ، وسجّل. مثل برمجية التوقيت الوسيطة أعلاه. +* **الرفض.** ارفع `MCPError` *بدلًا من* استدعاء `call_next(ctx)`، فتُجاب تلك الرسالة + بخطأ JSON-RPC. يبقى الاتصال قائمًا وتمر الرسالة التالية. مثل حد + التزامن أعلاه. وهذه أيضًا طريقة ضبط الوصول إلى `subscriptions/listen` لكل مستدعٍ: + يشرح قسم **[تحديد من يمكنه المراقبة](../handlers/subscriptions.md#deciding-who-may-watch)** في صفحة + الاشتراكات ذلك بالتفصيل. +* **إعادة الكتابة.** `ctx` صنف بيانات: تمرّر `await call_next(dataclasses.replace(ctx, params=...))` + إلى بقية السلسلة مَعلمات مختلفة عما أرسله العميل. لا تفعل ذلك مطلقًا مع + `initialize`: تُبنى النتيجة التي يتلقاها العميل من المَعلمات المعدّلة، لكن الخادم + يعتمد حالة اتصاله من المَعلمات الأصلية المنقولة. قد ينهي الطرفان المصافحة + دون اتفاق على ما تفاوضا عليه. +* **الرد.** أعِد نتيجة دون استدعاء `call_next(ctx)`، فتصل إلى العميل بوصفها + ردك. تعطيك `call_next` صيغة النقل النهائية، ولا يعدّل مسار المعالجة ما + تعيده، ولذلك فأنت مسؤول عن الغلاف كله: في اتصال من جيل 2026، يشمل ذلك + وسم `serverInfo` في `_meta`، الذي تضيفه SDK إلى نتائج دوال المعالجة، ولكن ليس إلى نتائجك. + +!!! check + `initialize` من الرسائل التي تغلّفها البرمجيات الوسيطة، وهي المنفذ *الوحيد* المتاح لك + للتعامل معها. حاول تولّيها باستخدام `add_request_handler`، وسترفض SDK: + + ```text + ValueError: 'initialize' is handled by the server runner and cannot be overridden; + use Server.middleware to observe or wrap initialization + ``` + +!!! warning + تُعالج `initialize` مباشرة: لا يقرأ الخادم رسائل واردة أخرى حتى تعود + سلسلة البرمجيات الوسيطة. ولذلك فإن انتظار طلب من الخادم إلى العميل (`ctx.session.send_request(...)`، + أو استقاء معلومات) أثناء معالجة `initialize` **يؤدي إلى توقف متبادل في الاتصال**: لا يمكن + قراءة الرد الذي تنتظره مطلقًا. أما الإشعارات المرسلة دون انتظار فتعمل بصورة طبيعية. + +## البرمجية الوسيطة الوحيدة المفعّلة افتراضيًا {#the-one-middleware-that-ships-on-by-default} + +تتضمن SDK برمجية وسيطة واحدة تحديدًا، وهي موجودة بالفعل في قائمة خادمك: التي +تصدر مقطع تتبّع OpenTelemetry لكل رسالة. لا تضيفها بنفسك، وغالبًا لا تحتاج +إلى التفكير فيها. لا تفعل شيئًا حتى تثبّت مُصدّرًا، ولها صفحتها الخاصة: +**[OpenTelemetry](../run/opentelemetry.md)**. + +!!! info + إذا كتبت برمجيات ASGI وسيطة، فأنت تعرف هذا الشكل بالفعل. تحولت + `(scope, receive, send)` في Starlette إلى `(ctx, call_next)`، وتعمل *بعد* وسيلة النقل، + على الرسالة المفككة بدلًا من طلب HTTP الخام. ويمكن الجمع بينهما: ترى برمجيات Starlette الوسيطة + على `streamable_http_app()` حركة HTTP؛ وترى هذه حركة MCP. + +## مراجعة {#recap} + +* البرمجية الوسيطة هي `async (ctx, call_next) -> result`، تُمرّر في `MCPServer(middleware=[...])` (أو + تُضاف إلى `mcp.middleware`)، وتُضاف إلى `server.middleware` في `Server` منخفض المستوى. +* تغلّف **كل** رسالة واردة تصل إلى الخادم (`server/discover` و`initialize` + والطلبات والإشعارات والطرائق غير المعروفة)، وتعمل بدءًا من الطبقة الخارجية. +* تميّز `ctx.request_id is None` الإشعار عن الطلب. +* ارفع استثناءً بدلًا من استدعاء `call_next` لرفض رسالة واحدة؛ ويظل الاتصال قائمًا. +* تتبّع OpenTelemetry الخاص بـSDK برمجية وسيطة أيضًا، وموجودة بالفعل في القائمة. راجع + **[OpenTelemetry](../run/opentelemetry.md)**. +* الواجهة بأكملها مؤقتة. استخدمها للمراقبة، ولا تبنِ عليها أساس خادمك. + +هذا كل ما يغلّف الطلب. أما **[التفويض](../run/authorization.md)** فهو ما يحدد ما إذا كان يُسمح للطلب +بالتنفيذ أصلًا. diff --git a/i18n/ar/pages/advanced/pagination.md b/i18n/ar/pages/advanced/pagination.md new file mode 100644 index 0000000000..672d0147a2 --- /dev/null +++ b/i18n/ar/pages/advanced/pagination.md @@ -0,0 +1,89 @@ +--- +translation: + sections: [a9aba7a026c7bd85, 83e2a08b9d46a398, 9fd8a0aa384b3257, 22a0129ee78b3c63, d875373c06d8d2f9] + tool: 1 +--- +# تقسيم النتائج إلى صفحات {#pagination} + +لا تحتاج معظم الخوادم إلى هذه الميزة مطلقًا. + +يجيب `MCPServer` عن كل طلب `list_*` بكل ما لديه في صفحة واحدة، مع `next_cursor=None`. وهذا مناسب لبضع عشرات من الأدوات أو الموارد أو قوالب التوجيه، ولا يحتاج إلى أي إعداد. + +يُستخدم تقسيم النتائج إلى صفحات عندما تكون قائمة موارد الخادم قاعدة بيانات فعلية: آلاف الصفوف التي لا يريد تسلسلها في رد واحد. حل البروتوكول هو **مؤشر**: يعيد الخادم صفحة مع رمز معتم، ويرسل العميل ذلك الرمز مجددًا للحصول على الصفحة التالية. + +لا تتضمن `@mcp.resource()` دالة تتيح ذلك. لتقسيم النتائج، اكتب دالة معالجة القائمة بنفسك على **[الخادم منخفض المستوى](low-level-server.md)**. + +## خادم يقسّم النتائج {#a-server-that-pages} + +```python title="server.py" hl_lines="12 15-16" +--8<-- "docs_src/pagination/tutorial001.py" +``` + +* في `Server` منخفض المستوى، تكون دوال المعالجة وسيطات للمُنشئ وليست مزخرفات. تجيب `on_list_resources` عن كل طلب `resources/list`؛ وهذا كل ما يلزم لربطها. +* نوع مَعلمة كل دالة معالجة مقسّمة إلى صفحات هو `params: PaginatedRequestParams | None`، ويقبل المثال الحالتين. لكن عبر اتصال، لا تمرّر SDK إليك `None` مطلقًا (يصل الطلب الذي لا يتضمن عضو `params` كنموذج بقيمه الافتراضية)، ولذلك فالإشارة المهمة هي `params.cursor is None`: **ابدأ من البداية**. +* أنت تحدد *ما هو* المؤشر. هنا هو إزاحة ممثلة كنص. قد يكون طابعًا زمنيًا أو مفتاحًا أساسيًا أو كتلة base64: أي شيء تستطيع إصداره عند الإرسال والتعرف عليه عند عودته. +* تعني `next_cursor=None` «كانت تلك الصفحة الأخيرة». لا عدد ولا مجموع ولا `has_more`. تمثل `None` الإشارة كاملةً. + +!!! tip + تجعل قيمة `PAGE_SIZE` البالغة 10 المثال سهل القراءة. اختر حجمك حسب نقطة النهاية: قد تسع + صفحة الموارد المؤلفة من سطر واحد 500 مورد، بينما لا يناسب ذلك قوالب التوجيه الكبيرة. + ليس للعميل رأي في ذلك، وهذا مقصود. + +### جرّبه {#try-it} + +لا تقبل `mcp run` سوى `MCPServer`، ولذلك تشغّل هذا الخادم بنفسك. يبني السطر الأخير من `server.py` تطبيق ASGI عاديًا من `Server`، ويشغّله uvicorn: + +```console +uvicorn server:app --port 8000 +``` + +وجّه أي عميل (**[العميل](../client/index.md)** أو Inspector) إلى `http://localhost:8000/mcp`، واستدعِ `list_resources()` دون وسيطات. تحصل على عشرة موارد، من `book-1` إلى `book-10`، وتكون `next_cursor` النص `"10"`. + +أعِده باستخدام `list_resources(cursor="10")`، فيكون المورد الأول `book-11`، وتصبح `next_cursor` الجديدة `"20"`. + +تعود الصفحة العاشرة مع `next_cursor` تساوي `None`. اكتمل الجلب. + +## حلقة العميل {#the-client-loop} + +تقبل كل طريقة `list_*` في `Client` (`list_tools` و`list_resources` و`list_resource_templates` و`list_prompts`) وسيطة مسماة `cursor=`. لا يتطلب جلب القائمة المقسّمة كاملةً سوى حلقة `while True` واحدة: + +```python title="client.py" hl_lines="9-15" +--8<-- "docs_src/pagination/tutorial002.py" +``` + +* تبدأ `cursor` بقيمة `None`، ولذلك لا يحمل الطلب الأول مؤشرًا. +* أضف النتائج **قبل** فحص `next_cursor`: فالصفحة الأخيرة تحمل موارد أيضًا. +* تمثل `next_cursor is None` شرط الخروج. أما أي قيمة أخرى فتُعاد مباشرة إلى `cursor=` دون تغيير. + +بينما يواصل uvicorn تقديم `server.py`، شغّل `python client.py` في طرفية ثانية. يطبع `100 resources`: عشر صفحات من عشرة موارد، جمعتها حلقة لم تكن تعرف أن عدد الصفحات عشرة. + +هذه الحلقة نفسها التي تعرضها صفحة **[العميل](../client/index.md)** لكل طريقة `list_*`، ولا تكلّف شيئًا إضافيًا مع خادم لا يقسّم النتائج: تكون `next_cursor` مساوية لـ`None` في الرد الأول، وتُنفّذ الحلقة مرة واحدة. + +## القواعد الثلاث {#the-three-rules} + +**المؤشرات معتمة.** يجب ألا يحلل العميل مؤشرًا أو يبنيه أو يخمّنه مطلقًا. المصدر المقبول الوحيد للمؤشر هو `next_cursor` من الصفحة السابقة، كما هو حرفيًا. + +**يختار الخادم حجم الصفحة.** لا توجد `limit=` في البروتوكول. إذا احتجت حجم صفحة مختلفًا، فغيّر الخادم. + +**يظل العميل الذي يتجاهل التقسيم يعمل.** يستدعي `list_resources()` مرة واحدة، ويحصل على أول عشرة موارد، ولا يلاحظ `next_cursor` التي أهملها. لا يتعطل شيء؛ إنما يرى موارد أقل. + +!!! check + تعني معتمة أنها معتمة فعلًا. اخترع مؤشرًا (`list_resources(cursor="page-2")`)، ولن يستطيع + البروتوكول مساعدتك. يحاول هذا الخادم تنفيذ `int("page-2")`، فتثير دالة المعالجة استثناءً، + ويعود إلى العميل ما يلي: + + ```text + MCPError(-32603, 'Internal server error', None) + ``` + + المؤشر الذي لم تحصل عليه من الخادم خطأ برمجي، وليس طلب ميزة. + +## مراجعة {#recap} + +* يعيد `MCPServer` كل شيء في صفحة واحدة. تقسيم النتائج اختياري، وتفعّله على `Server` منخفض المستوى. +* تتلقى `on_list_resources` (وكذلك `on_list_tools` و`on_list_prompts` و`on_list_resource_templates`) قيمة `PaginatedRequestParams | None`؛ وتكون `params.cursor` مساوية لـ`None` للصفحة الأولى. +* تعيد صفحة مع `next_cursor`: أي نص تستطيع التعرف عليه لاحقًا، أو `None` عندما لا تبقى نتائج. +* حلقة العميل: مرّر `cursor=`، واجمع النتائج، وكرر حتى تصبح `next_cursor is None`. +* المؤشرات معتمة، والخادم يحدد حجم الصفحة، ويحصل العميل الذي لا يتابع الصفحات على الصفحة الأولى رغم ذلك. + +تشرح صفحة **[الخادم منخفض المستوى](low-level-server.md)** بقية واجهة `Server` المكتوبة يدويًا (`on_call_tool` وقواميس `input_schema` و`_meta`). diff --git a/i18n/ar/pages/client/caching.md b/i18n/ar/pages/client/caching.md new file mode 100644 index 0000000000..a5ba52777b --- /dev/null +++ b/i18n/ar/pages/client/caching.md @@ -0,0 +1,136 @@ +--- +translation: + sections: [9e7b9a1710e5aeba, 66a2e9acc9101d54, d3d25aa802f5145a, 04db67a886b7271c, 857690fb8f876800] + tool: 1 +--- +# تلميحات التخزين المؤقت {#caching-hints} + +تحمل كل نتيجة يعيدها الخادم للطرائق `tools/list` و`prompts/list` و`resources/list` و`resources/templates/list` و`resources/read` و`server/discover` حقلين في بروتوكول 2026-07-28: `ttlMs`، وهو عدد الملّي ثواني التي يستطيع العميل اعتبار النتيجة حديثة خلالها، و`cacheScope`، الذي يحدد ما إذا كان يمكن مشاركة النتيجة المخزّنة مؤقتًا بين المستخدمين (`"public"`) أو كانت تخص سياق تفويض واحدًا (`"private"`). + +لا يخزّن الخادم أي شيء مؤقتًا. الحقلان *إعلان*: «قائمة الأدوات هذه متطابقة للجميع ولن تتغير لمدة دقيقة». يمكن للعميل (أو لبوابة أمامك) حينها تجنّب جولة طلب ورد. يختار العميل احترام التلميحات؛ أما إصدارها فمهمة الخادم، وتنفّذها SDK نيابة عنك. + +تقول كل نتيجة افتراضيًا `ttlMs: 0, cacheScope: "private"`: منتهية الحداثة فورًا، ولا تُشارك مطلقًا. وهذا آمن ومتوافق دائمًا. إذا كانت قوائمك مستقرة فعلًا ومتطابقة لكل المستدعين، فأعلن ذلك عند الإنشاء: + +```python title="server.py" hl_lines="5-8" +--8<-- "docs_src/caching/tutorial001.py" +``` + +* تعتمد الخريطة على **اسم الطريقة** كمفتاح، ولا تقبل سوى أسماء الطرائق الست القابلة للتخزين المؤقت. نوع المَعلمة هو `Mapping[CacheableMethod, CacheHint]`، ولذلك يكمل المحرر المفاتيح تلقائيًا وينبّه إلى الأخطاء قبل التشغيل؛ وأي خطأ يفلت من فاحص الأنواع يثير استثناءً عند الإنشاء. +* تحتفظ الطريقة التي لم تذكرها بالقيم الافتراضية. الخريطة مجموعة تجاوزات، وليست قائمة حصرية بالطرائق. +* تركت `CacheHint(ttl_ms=5_000)` قيمة `scope` دون تعيين، فتبقى `"private"`: خمس ثوانٍ من الحداثة لكل مستدعٍ. تحديد النطاق ومدة الصلاحية قراران مستقلان. +* تمثل `"server/discover"` أيضًا مفتاحًا مقبولًا، لأن نتيجة الاكتشاف قابلة للتخزين المؤقت مثل أي قائمة. + +!!! warning + تعني `cacheScope: "public"` أن *أي شخص* قد يتلقى ردك المخزّن مؤقتًا. قد تقدّم + بوابة مشتركة نتيجة مستخدم إلى مستخدم آخر، حتى عندما يكون الطلب + مصادقًا عليه. علّم النتيجة بأنها `"public"` فقط إذا كانت متطابقة لكل مستدعٍ، + ولا تستخدم `cacheScope` للتحكم في الوصول: فهي وسم وليست قفلًا. + +## التجاوز لكل دالة معالجة {#per-handler-override} + +في `Server` منخفض المستوى، تبني دوال المعالجة نتائجها يدويًا، وتكون `ttl_ms` / `cache_scope` مجرد حقول في نماذج النتائج. يتقدم تعيين دالة المعالجة الصريح لها دائمًا على خريطة المُنشئ، لكل حقل على حدة: + +```python title="server.py" hl_lines="11 17" +--8<-- "docs_src/caching/tutorial002.py" +``` + +عيّنت دالة المعالجة `ttl_ms=1_000` ولم تحدد النطاق. فتُرسل `ttlMs: 1000` (قيمة الدالة، وليس `60_000` في الخريطة) و`cacheScope: "public"` (قيمة الخريطة، لأن الدالة لم تعيّنها). يتقدم التعيين الصريح على الإعداد، والإعداد على الافتراضي. تنطبق هذه القاعدة لكل حقل، فيمكن للدالة تثبيت أحدهما وترك الآخر لسياسة الخادم العامة. + +هذا أيضًا منفذ للتحكم المباشر في السلوك الديناميكي الذي لا يستطيع المُنشئ معرفته: يمكن لدالة ترشّح `resources/read` حسب المستخدم إعادة `cache_scope="private"` لعنوان URI واحد في خادم تكون بقية نتائجه عامة. + +تنبيه بخصوص القوائم المقسّمة إلى صفحات: يشترط البروتوكول **قيمة `cacheScope` نفسها في كل صفحة** من القائمة الواحدة. تحقق خريطة المُنشئ ذلك تلقائيًا، لأنها تعتمد على الطريقة لا الصفحة. أما الدالة التي تتجاوز النطاق بنفسها فمسؤولة عن هذا الاتساق: تجاوزه في *كل* صفحة، وليس فقط عند وجود مؤشر، وإلا اختلفت الصفحة الأولى عن الثانية. + +## ما يراه العميل {#what-the-client-sees} + +في جلسة 2026-07-28، يحترم `Client` التلميحات نيابة عنك: لديه ذاكرة تخزين مؤقت مدمجة للردود، مفعّلة افتراضيًا. تُخزّن النتيجة التي تحمل `ttlMs`، ويُجاب عن الاستدعاء المطابق خلال تلك المدة من الذاكرة دون جولة طلب ورد. أما النتيجة التي *لا* تحمل تلميحًا فلا تُخزّن: تحصل النتائج بلا تلميحات على `CacheConfig.default_ttl_ms`، وافتراضيها `0` (منتهية الحداثة فورًا)، ولذلك يتلقى الخادم الذي لا يعلن شيئًا العدد نفسه من الاستدعاءات كما في السابق. + +لمشاهدة ذلك، شغّل `server.py` من القسم السابق باستخدام uvicorn (يبني سطره الأخير تطبيق ASGI). تطبع دالة المعالجة سطرًا كل مرة تُشغّل فيها فعليًا: + +```console +uvicorn server:app --port 8000 +``` + +```python title="client.py" hl_lines="20 23 28" +--8<-- "docs_src/caching/tutorial003.py" +``` + +شغّل `python client.py` من طرفية ثانية. يطبع التلميحات التي حملتها النتيجة الأولى، وقيمة `ttlMs` الخاصة بالدالة إلى جانب `cacheScope` الخاصة بالخريطة: + +```text +1000 public +``` + +توضح طرفية الخادم بقية ما يحدث: بين سجلات طلبات uvicorn، تظهر `tools/list served` ثلاث مرات. + +أربعة استدعاءات وثلاث عمليات جلب. وجد الاستدعاء الثاني إدخالًا حديثًا فلم يصل إلى الخادم؛ وأدى تقديم الساعة (المحقونة) إلى ما بعد مدة الصلاحية إلى جلب ثالث جديد؛ وحدد الرابع `cache_mode="refresh"`. تتوفر هذه الوسيطة المسماة في طرائق التخزين المؤقت الخمس (`list_tools` و`list_prompts` و`list_resources` و`list_resource_templates` و`read_resource`): + +* تخدم `"use"` (الافتراضية) إدخالًا حديثًا إن وُجد، وإلا تجلب النتيجة وتخزّنها. +* لا تخدم `"refresh"` إدخالًا مخزّنًا: تجلب النتيجة وتخزّنها، مستبدلة ما كان مخزّنًا. +* تنفّذ `"bypass"` جولة الطلب والرد دون لمس الذاكرة: لا قراءة ولا كتابة. + +تتقدم قاعدة واحدة على `"use"`: **الاستدعاءات التي تحمل `meta` تصل دائمًا إلى الخادم.** يتوقع الطلب الذي يعيّن `meta` (رمز تقدم أو حقول تتبّع) إرسالًا فعليًا، ولذلك يُعامل تحت `cache_mode="use"` مثل `"refresh"`: تُتخطى القراءة من الذاكرة، وتظل النتيجة المجلوبة تحل محل الإدخال المخزّن. وتتصرف `"bypass"` و`"refresh"` الصريحة كالمعتاد. + +لتعطيل التخزين المؤقت بالكامل، مرّر `cache=None` عند إنشاء `Client`: يعود كل استدعاء إلى تنفيذ جولة طلب ورد، ولا يعود لـ`cache_mode` أي أثر رغم استمرار قبولها. + +يُحترم النطاق تلقائيًا أيضًا: ترتبط إدخالات `"private"` بـ*قسم* الذاكرة (أدناه)، بينما يمكن لإدخالات `"public"` الاشتراك في مشاركة أوسع. كذلك **تتقدم الإشعارات على مدة الصلاحية** للإدخالات المحددة التي تسمّيها: يزيل إشعار `list_changed` القائمة المخزّنة المقابلة، ويزيل `resources/updated` قراءة المورد المخزّنة تحت عنوان URI المطابق تمامًا، مهما كانت حديثة. في اتصال 2026-07-28، تصل هذه الإشعارات عبر تدفّق `subscriptions/listen` تفتحه باستخدام `client.listen(...)`، وتكتمل الإزالة قبل أن يرى المراقب الحدث؛ تشرح ذلك صفحة **[الاشتراكات](subscriptions.md)**. + +تنبيه بخصوص `resources/updated`: لا تشمل الإزالة إلا عنوان URI المطابق تمامًا. لا يتضمن عقد المخزن عملية تعداد أو مسح (كما في تنفيذ TypeScript المرجعي)، ولذلك لا يزيل إشعار يحمل URI لمورد *فرعي* القراءة المخزّنة لمورده الأب. إذا أرسل خادمك إشعارات الموارد الفرعية بهذه الطريقة، فأعد جلب الأب باستخدام `cache_mode="refresh"`. + +### الإعداد: `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`: موضع حفظ الإدخالات. الافتراضي مخزن جديد داخل الذاكرة لكل عميل؛ مرّر تنفيذك الخاص لـ`ResponseCacheStore` (يعتمد على Redis مثلًا) لمشاركة الذاكرة بين العملاء أو العمليات. يمكن استيراد أنواع العقد (`ResponseCacheStore` و`CacheKey` و`CacheEntry` و`InMemoryResponseCacheStore` الافتراضي) من `mcp.client`. قد تنفّذ عملية بحث استدعاءي `get` متتاليين للمخزن (الفرع الخاص ثم العام)، فضع ذلك في حساب زمن استجابة المخزن البعيد. المخزن المخصص **يتطلب** تعيين `partition` صراحةً. +* `partition`: وسم سياق التفويض الذي يمنع تقديم إدخالات `"private"` الخاصة بهوية إلى هوية أخرى داخل مخزن مشترك. +* `target_id`: هوية الخادم الصريحة لوسائل النقل المخصصة والخوادم داخل العملية (أدناه). +* `default_ttl_ms`: مدة الصلاحية المطبقة على النتائج التي لا تحمل تلميح `ttlMs`. تترك القيمة الافتراضية `0` النتائج بلا تلميحات دون تخزين مؤقت. +* `share_public`: تقديم الإدخالات التي يصنّفها الخادم بأنها `"public"` عبر الأقسام (أدناه). معطّلة افتراضيًا. +* `clock`: مصدر الوقت الفعلي، بالثواني منذ بداية الحقبة. احقن مصدرًا كما في المثال أعلاه، ولن تحتاج اختبارات انتهاء الصلاحية إلى الانتظار. + +!!! warning "القسم = هوية متحقق منها" + اشتق `partition` من **بيانات اعتماد متحقق منها**، مثل هوية صاحب رمز جرى التحقق منه. لا تشتقها مطلقًا من بيانات يقدّمها الطلب، ولا من عنوان URL للخادم (فهوية الخادم بُعد مستقل في المفتاح). SDK مكتبة لا تنفّذ مصادقة خاصة بها: مرتكز الثقة هو من ينشئ `CacheConfig`، أي بيئة النشر لا المستأجر. تنشئ البوابة متعددة المستأجرين `CacheConfig` لكل هوية مصادق عليها. + + يظل القسم ثابتًا أيضًا طوال عمر `Client`. إذا تغير سياق تفويض الاتصال أثناء الجلسة (بإعادة المصادقة بهوية أخرى مثلًا)، فلن تتبعه الذاكرة؛ أنشئ `Client` جديدًا للهوية الجديدة. + +تحمل مفاتيح الذاكرة أيضًا **هوية الخادم**: نص عنوان URL الذي اتصلت به، مع حذف معلومات المستخدم `user:pass@` إن وجدت، وبقاء بقية النص مطابقًا بالبايت. دون توحيد حالة الأحرف أو إعادة ترتيب الاستعلام أو حذف الشرطة المائلة الأخيرة. نقص التطبيع يحد المشاركة فقط، بينما قد يدمج الإفراط فيه مستأجرين (`?tenant=a` مقابل `?tenant=b`)، ولذلك لا تشارك عناوين URL المختلفة ظاهريًا الإدخالات. عند غياب URL (خادم داخل العملية أو نسخة `Transport`)، يحصل العميل بدلًا منه على هوية عشوائية خاصة بالنسخة؛ عيّن `CacheConfig.target_id` لتسمية الخادم (هذا مطلوب مع مخزن مخصص، ويخبرك المُنشئ بذلك). تُجزّأ الهوية باستخدام sha256 قبل تضمينها في المفتاح، حتى لا يظهر URL يحمل أسرارًا في استعلامه في مفاتيح المخزن. ولا تسجّل أنت أيضًا القيمة السابقة للتجزئة. + +!!! warning "يثق `share_public` بالخادم نيابة عن جميع العملاء" + تبقى حتى إدخالات `"public"` داخل قسمها افتراضيًا. تقدّم `share_public=True` الإدخالات التي علّمها الخادم بـ`cacheScope: "public"` إلى **كل** قسم يستخدم المخزن، مع الثقة بتصنيف الخادم نيابة عن الجميع. إذا وضع خادم وسم `"public"` على بيانات تخص مستأجرًا (بسبب خطأ أو بقصد خبيث)، فسيسرب رد ذلك المستأجر إلى الآخرين. يُتاح الخيار عند الإنشاء فقط عن قصد: تستطيع `cache_mode` لكل استدعاء تضييق التخزين المؤقت، لكن لا يستطيع أي خيار لكل استدعاء توسيع المشاركة. + +### ما لا تنفّذه الذاكرة مطلقًا {#what-the-cache-never-does} + +* **تتجاوزها استدعاءات مستوى الجلسة.** تنفّذ `client.session.list_tools()` والطرائق المشابهة دائمًا جولة الطلب والرد؛ فالذاكرة موجودة في طرائق `Client`. +* **تبقى `server/discover` خارجها.** تصل نتيجة الاكتشاف مرة واحدة عند الاتصال، ولا تدخل ذاكرة الردود مطلقًا، حتى إذا حملت `ttlMs`. إذا حفظت نتيجة بنفسك لتجنب فحص إعادة الاتصال ([`prior_discover`](../protocol-versions.md#reconnecting-with-prior_discover))، فأنت مسؤول عن حداثتها: تحمل `DiscoverResult` حقلي `ttl_ms` و`cache_scope` محلّلين بالفعل لهذا الغرض تحديدًا. +* **لا تُخزّن صفحات المتابعة مطلقًا.** تشارك فقط الاستدعاءات بلا مؤشر. تؤدي صفحة متابعة مرفوضة لانتهاء صلاحية المؤشر إلى *إزالة* القائمة المخزّنة، لأن القائمة تغيرت أثناء استخدامها. +* **لا تُخزّن القراءات متعددة جولات الطلب والرد مطلقًا.** لا تدخل الذاكرة قراءة `read_resource` التي تبدأ بـ`input_responses`/`request_state` أو التي تُحسم عبر جولات إدخال (متطلب إلزامي في المواصفة). +* **تحتاج الإزالة بالإشعارات إلى وصول الإشعارات.** تتوقف موثوقيتها على تسليم وسيلة النقل، والمسار الحديث داخل العملية (`Client(server)` مع `mode="auto"` الافتراضي) لا يسلّم إشعارات مستقلة حاليًا. +* **الإزالة لاحقة وليست لحظية.** تُوزّع إشعارات مسار النقل من مهام منشأة، ولذلك قد يتلقى استدعاء يتزامن مع وصول إشعار الإدخال السابق للإزالة مرة أخرى؛ تحدد مهلة التوزيع هذه النافذة، وتحدث الإزالة رغم ذلك. +* **لا تقديم لنتيجة قديمة عند الخطأ.** لا يُقدّم إدخال منتهي الصلاحية لأن إعادة الجلب فشلت؛ بل يُمرّر الخطأ. +* **لا إعادة جلب مبكرة.** يُقدّم الإدخال المخزّن حتى تنتهي مدته، ثم يتحمل الاستدعاء التالي جولة الطلب والرد؛ لا تحديث في الخلفية. +* **لا دمج للاستدعاءات.** يعني استدعاءان متزامنان متطابقان عمليتي جلب. +* **لا مدة صلاحية تتجاوز 24 ساعة.** تُخفض قيمة `ttlMs` الأكبر، سواء أرسلها الخادم أو عُيّنت في الإعداد، عند التخزين (`mcp.client.caching.MAX_TTL_MS`)، مما يحد مدة تقديم أي إدخال مهما كان تلميحه سخيًا. +* في **مخزن مشترك**، تتسابق العملاء. يسقط كل عميل كتابته إذا سبقت إزالةٌ عملية الجلب الجارية، لكن عميلًا *لمستأجر آخر* قد يعيد كتابة إدخال أزالته عملية لم يرها؛ وتتبع هذا السباق محدود أيضًا: بعد 4096 مفتاحًا متتبّعًا، تُسقط حماية أقدم مفتاح أولًا. تُقبل النافذتان، ويحدهما سقف مدة الصلاحية أعلاه. +* **لا تقديم عبر أجيال البروتوكول.** ترتبط الإدخالات بإصدار البروتوكول المتفاوض عليه: في مخزن مشترك دائم، لا تقدّم جلسة إدخالًا كُتب تحت إصدار آخر (فالقائمة نفسها تختلف فعلًا حسب الجيل، لأن SDK تحذف حقول 2026 للجلسات الأقدم). تلمس الإزالة أيضًا إدخالات الجيل الحالي فقط؛ وتنتهي إدخالات الأجيال الأخرى بمرور مدة صلاحيتها. + +### قراءة التلميحات بنفسك {#reading-the-hints-yourself} + +التلميحات أيضًا حقول عادية في كل نتيجة قابلة للتخزين (`result.ttl_ms` و`result.cache_scope`، محلّلان بالفعل)، إذا أردت إضافة تتبّع خاص بك فوق الذاكرة المدمجة أو بدلًا منها. + +مع **خادم أقدم** (بروتوكول يسبق 2026)، تغيب الحقول عن البيانات المرسلة، وتعرض النماذج قيمها المتحفظة الافتراضية: `ttl_ms == 0` و`cache_scope == "private"`، أي قديمة وغير مشتركة، وهو الافتراض المناسب لخادم لم يعلن شيئًا. تعامل الذاكرة الجلسة القديمة بالطريقة نفسها: لا تُستشار التلميحات فيها مطلقًا (مهما ظهرت مفاتيح على الشبكة)، وتُطبق `default_ttl_ms` فقط، وافتراضيها `0` لا يخزّن شيئًا، فيتصرف اتصال ما قبل 2026 تمامًا كما كان قبل وجود الذاكرة. للتمييز بين «قال الخادم 0» و«لم يقل الخادم شيئًا»، افحص `"ttl_ms" in result.model_fields_set`: لا يُعيّن إلا عندما يصل الحقل فعلًا. + +## العملاء الأقدم {#older-clients} + +لا ترى العملاء على إصدارات البروتوكول السابقة لعام 2026 أيًا من الحقلين؛ تحذفهما SDK عند تسلسل البيانات لهذه الاتصالات. اضبط التلميحات مرة واحدة؛ لا حاجة لشيفرة خاصة بالإصدار. + +## مراجعة {#recap} + +* تحمل ست طرائق `ttlMs`/`cacheScope`؛ وتضع SDK افتراضيًا `0`/`"private"`، أي قديمة وغير مشتركة وآمنة دائمًا. +* تضبط `cache_hints={method: CacheHint(...)}` عند الإنشاء (في كل من `MCPServer` و`Server`) قيمًا عامة للخادم لكل طريقة. +* تتجاوز دالة المعالجة التي تعيّن الحقول في نتيجتها الخريطة، لكل حقل على حدة. +* تمثل `"public"` وعدًا بأن النتيجة متطابقة لكل مستدعٍ. وهي ليست تحكمًا في الوصول. +* يحترم `Client` التلميحات تلقائيًا: ذاكرة ردوده مفعّلة افتراضيًا، وتقدّم الإدخالات الحديثة بدلًا من إعادة جلبها، ولا تخزّن شيئًا للخوادم أو الجلسات التي لا تقدّم تلميحات. +* لكل استدعاء، تعيد `cache_mode="refresh"` الجلب، وتتجاوز `"bypass"` الذاكرة؛ وتُعطّلها `cache=None` عند الإنشاء بالكامل. diff --git a/i18n/ar/pages/client/callbacks.md b/i18n/ar/pages/client/callbacks.md new file mode 100644 index 0000000000..8b982a272e --- /dev/null +++ b/i18n/ar/pages/client/callbacks.md @@ -0,0 +1,154 @@ +--- +translation: + sections: [adf3c545b5be46b6, 916cd3ab1c03f461, 32ef568335dd95a7, 565890a636288ecf, 6af7e49db9129ec3, 06b0238c174186af, 0abc5ea5cb7ff6b3] + tool: 1 +--- +# دوال رد النداء لدى العميل {#client-callbacks} + +تسير تقريبًا جميع طلبات MCP في اتجاه واحد: من العميل إلى الخادم. + +يستطيع الخادم أيضًا طلب أشياء من **العميل**: طرح سؤال على المستخدم، أو طلب توليد من نموذجه، أو عرض مجلدات مساحة عمله. تجيب عن هذه الطلبات بتمرير **دوال رد نداء** (callbacks) إلى `Client(...)`. + +## خادم يسأل {#a-server-that-asks} + +إليك خادمًا لا تستطيع أداته إنهاء عملها بمفردها: + +```python title="server.py" hl_lines="16" +--8<-- "docs_src/client_callbacks/tutorial001.py" +``` + +* ترسل `ctx.elicit(...)` طلب `elicitation/create` **إلى العميل** وتنتظر. +* لا تعود الأداة حتى يقدّم أحد (شخص في نموذج أو شيفرتك) قيمة `name`. + +هذا جانب الخادم، وتشرحه صفحة **[استقاء المعلومات](../handlers/elicitation.md)** (elicitation). هذه الصفحة للطرف الآخر. + +## دالة رد النداء لاستقاء المعلومات {#the-elicitation-callback} + +```python title="client.py" hl_lines="6-10 16-17" +--8<-- "docs_src/client_callbacks/tutorial002.py" +``` + +* دالة رد النداء لاستقاء المعلومات هي `async (context, params) -> ElicitResult`. +* `params.message` هو السؤال. و`params.requested_schema` هو JSON Schema للإجابة المطلوبة. يعرض العميل الفعلي نموذجًا منه؛ ويملؤه هذا المثال تلقائيًا. +* تعيد `ElicitResult(action="accept", content={...})` أو `action="decline"` أو `action="cancel"`. الخيار الآخر الوحيد `ErrorData(...)`، الذي يرفض الطلب ويُفشل الاستدعاء كله. +* `context` هو `ClientRequestContext`: الجلسة الحالية `session`، ومعرّف طلب الخادم `request_id`، وأي `meta` أرفقها. + +!!! tip + `params` اتحاد نمطَي استقاء المعلومات. هنا `params.mode` هي `"form"`؛ ويحمل طلب `"url"` + الحقل `params.url` بدلًا من مخطط. تعالج دالة واحدة النمطين؛ تفرّع حسب `params.mode`. + تعرض **[استقاء المعلومات](../handlers/elicitation.md)** النمط كاملًا. + +### جرّبها {#try-it} + +استدعِ `issue_card` وراقب الطرفين. + +تتلقى دالتك سؤال الخادم بعد تحليله بالفعل: + +```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'} +``` + +تجيب، فتستأنف `ctx.elicit(...)` داخل الأداة، وتكمل الأداة عملها: + +```python +result.content # [TextContent(type='text', text='Card issued to Ada Lovelace.')] +``` + +طلب `tools/call` واحد منك، وطلب `elicitation/create` عكسي واحد من الخادم تجيب عنه دالتك، وكل ذلك داخل استدعاء أداة واحد. + +!!! info + يؤدي `mode="legacy"` في استدعاء `Client(...)` عملًا فعليًا. تتفاوض `Client(...)` افتراضيًا على مسار + البروتوكول الحديث، ولا يملك ذلك المسار قناة عكسية لطلبات الخادم إلى العميل: تفشل `ctx.elicit` + قبل تشغيل دالتك أصلًا. لا تحدد وسيلة النقل ذلك؛ بل يحدده + البروتوكول المتفاوض عليه. ثبّت `mode="legacy"` كلما اضطر عميلك + إلى الإجابة عن طلب كهذا؛ وهذا ما يفعله كل اختبار لهذه الصفحة. تتضمن **[إصدارات البروتوكول](../protocol-versions.md)** التفاصيل كاملة. + + على جلسة 2026-07-28، لا تصبح الدالة معطّلة بل تُغذّى بصورة مختلفة: عندما تعيد الأداة + `InputRequiredResult` تحمل `ElicitRequest`، توجّه `Client` ذلك الإدخال إلى + `elicitation_callback` نفسها وتعيد الاستدعاء نيابة عنك. هذا تدفق **[الطلبات متعددة جولات التبادل](../handlers/multi-round-trip.md)**. + +## دالة رد النداء إعلان قدرة {#a-callback-is-a-capability} + +لم تخبر الخادم بأن عميلك يستطيع الإجابة عن استقاء المعلومات. فعلت SDK ذلك. + +عند اتصال العميل، يعلن `capabilities` الخاصة به، وهي المقابل لقدرات الخادم. لا تكتب ذلك الكائن. **تسجيل دالة رد نداء هو الإعلان.** + +| ما تمرّره | ما يعلنه العميل | +| --- | --- | +| `elicitation_callback=` | `"elicitation": {"form": {}, "url": {}}` | +| `sampling_callback=` | `"sampling": {}` | +| `list_roots_callback=` | `"roots": {"listChanged": true}` | +| لا شيء منها | `{}` | + +القدرات الفرعية لأخذ العينات (sampling) هي الاستثناء التفصيلي: مرّر `sampling_capabilities=SamplingCapability(tools=SamplingToolsCapability())` بجانب `sampling_callback` عندما تدعم دالتك مَعلمات `tools` / `tool_choice`. يجب أن يرى الخادم إعلان `sampling.tools` قبل إرسالها. + +لا توجد `logging_callback` و`message_handler` في الجدول. تتعاملان مع إشعارات، ولا تحتاج الإشعارات إلى قدرة. + +يقرأ الخادم الإعلان باستخدام `ctx.session.check_client_capability(...)`. أضف أداة تفعل ذلك: + +```python title="server.py" hl_lines="23-31" +--8<-- "docs_src/client_callbacks/tutorial003.py" +``` + +اتصل باستخدام `elicitation_callback` فقط واستدعِها: + +```python +result.structured_content # {'result': ['elicitation']} +``` + +مرّر الدوال الثلاث فتحصل على `['elicitation', 'sampling', 'roots']`. لا تمرّر أيًّا منها فتحصل على `[]`. + +!!! check + جرّب الآن الاختيار الخاطئ: اتصل **دون** `elicitation_callback` واستدعِ `issue_card` رغم ذلك. + + ما زال طلب `elicitation/create` من الخادم يصل إلى عميلك، وتجيب عنه SDK نيابة + عنك بخطأ لأنك لم تعلن قدرتك على معالجته. يُفشل ذلك الخطأ الاستدعاء كله. + لا تعيد `call_tool` نتيجة `is_error`، بل تثير استثناءً: + + ```text + MCPError: Elicitation not supported + ``` + + هذا خطأ بروتوكول (`-32600`، *طلب غير صالح*)، لا خطأ أداة: لا شيء + يقرؤه النموذج ليعيد المحاولة. لذلك يفيد وجود `client_features`: يفحص الخادم الملتزم + قبل السؤال. + +## الزوج المهجور {#the-deprecated-pair} + +تجيب `sampling_callback` عن `sampling/createMessage`: طلب الخادم من نموذجك *أنت* توليد استكمال. وتجيب `list_roots_callback` عن `roots/list`: سؤال الخادم عن المجلدات التي يجوز له العمل فيها. + +كلاهما يعمل ويتبع القاعدة أعلاه. وكلاهما يخدم RPC **تزيلها مواصفة 2026-07-28**: لا يستدعي الخادم الحديث عميلك عكسيًا أثناء الطلب، بل يعيد الطلب كجزء من نتيجة الأداة (**[الطلبات متعددة جولات التبادل](../handlers/multi-round-trip.md)**). لا تُلغى الدوال نفسها. عندما تحمل `InputRequiredResult` طلب `CreateMessageRequest` أو `ListRootsRequest`، توجّهه الحلقة التلقائية في `Client` إلى `sampling_callback` أو `list_roots_callback` نفسها المسجلة هنا. القائمة الكاملة في **[الميزات المهجورة](../deprecated.md)**. + +ما زلت تحتاج إلى الدوال للتواصل مع خوادم لم تنتقل بعد. التوقيعات: + +```python title="client.py" +--8<-- "docs_src/client_callbacks/tutorial004.py" +``` + +* تتلقى دالة أخذ العينات `CreateMessageRequestParams` كاملة (`messages` و`model_preferences` و`max_tokens`) وتعيد `CreateMessageResult`. *أنت* تشغّل النموذج كما تشاء؛ ولا تحمل SDK إلا الطلب. +* لا تأخذ دالة المجلدات الجذرية (roots) أي مَعلمات، وتعيد `ListRootsResult`. +* تستطيع أيٌّ منهما إعادة `ErrorData(...)` بدلًا من ذلك للرفض. + +مرّرهما إلى `Client(...)` تمامًا مثل `elicitation_callback`. + +## دوال رد النداء للإشعارات {#the-notification-callbacks} + +دالتان إضافيتان. لا تعلن أيٌّ منهما قدرة. + +تتلقى `logging_callback` إشعار `notifications/message` الذي يرسله الخادم كـ`LoggingMessageNotificationParams` (`level` و`logger` و`data`). تسجيل البروتوكول نفسه مهجور بمواصفة 2026-07-28 (توضح **[التسجيل](../handlers/logging.md)** البديل)، لذلك توجد الدالة للخوادم التي ما زالت ترسله. على اتصال جيل 2026، لا تمنحك الدالة وحدها شيئًا، لأن خوادم 2026 لا ترسل رسائل سجل إلا للطلبات التي تختار ذلك: مرّر `log_level="info"` (أو مستوى آخر) إلى `Client(...)` لإضافة الاختيار لكل طلب وتلقي ذلك المستوى وما فوقه. تتجاهله الخوادم الأقدم من 2026 وتحتفظ بسلوك `logging/setLevel`. + +`message_handler` هي الدالة العامة: يصلها كل إشعار خادم تتيحه الجلسة (إضافة إلى دالته المخصصة)، وكل `Exception` على مستوى النقل إذا كانت وسيلة النقل قائمة على تدفّق. لا يصل نوعان أبدًا: تطبّق SDK إشعار `notifications/cancelled` بدلًا من إتاحته، ويستهلك تدفّق `listen()` النشط إقرار اشتراكه. استخدم تعليق النوع `IncomingMessage` للمَعلمة (`ServerNotification | Exception`، وتصدّره `mcp.client`). النمط المهم `if isinstance(message, Exception): raise message`، كي يفشل الاتصال المعطل بوضوح بدلًا من اختفائه. + +## مراجعة {#recap} + +* يستطيع الخادم إرسال طلبات للعميل. تجيب عنها بدوال رد نداء تُمرَّر إلى `Client(...)`. +* دالة استقاء المعلومات هي الحالية: `async (context, params) -> ElicitResult`، دالة واحدة لنمطَي النموذج وURL. +* **تسجيل دالة رد نداء إعلان للقدرة.** وبدونها ترفض SDK طلب الخادم نيابة عنك ويفشل الاستدعاء كله بـ`MCPError`. +* يتحقق الخادم قبل السؤال باستخدام `ctx.session.check_client_capability(...)`. +* تعمل `sampling_callback` و`list_roots_callback` بالطريقة نفسها لكن لميزات مهجورة؛ وتستخدم الخوادم الحديثة طلبات متعددة الجولات بدلًا منها. +* تتلقى `logging_callback` و`message_handler` الإشعارات. ولا تعلنان شيئًا. + +تختار الوسيطة الأولى لـ`Client(...)` وسيلة النقل. تغطي **[وسائل نقل العميل](transports.md)** كل نوع. diff --git a/i18n/ar/pages/client/identity-assertion.md b/i18n/ar/pages/client/identity-assertion.md new file mode 100644 index 0000000000..369a7d92b0 --- /dev/null +++ b/i18n/ar/pages/client/identity-assertion.md @@ -0,0 +1,152 @@ +--- +translation: + sections: [a91322c46111d16d, 8e6fd6d6f59bb568, 7cf38181f6c99fd5, 37804d4fb36d6302, 1034c653c0bcf1b0] + tool: 1 +--- +# إفادة الهوية {#identity-assertion} + +يبدأ موفّر OAuth المعتاد (**[عملاء OAuth](oauth-clients.md)**) بسؤال خادم MCP: *أي خادم تفويض تثق به؟* ثم يتبع الإجابة أينما أشارت، ويسجّل شخص دخوله أو يحل سر مشترك مسبقًا محل ذلك. + +لا تريد المؤسسة أن يُتخذ أي من القرارين لكل خادم على حدة. فلديها موفّر هوية بالفعل (Okta أو Microsoft Entra ID أو موفّر خاص بها)، وقد سجّل المستخدم دخوله إليه هذا الصباح، ويريد فريق الأمن أن يكون هذا الموفّر المكان الوحيد لتحديد من يستطيع الوصول إلى ماذا. ينقل امتداد **التفويض الذي تديره المؤسسة**، [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990)، القرار إليه. يوقّع موفّر الهوية JWT قصير العمر، هو **منحة تفويض JWT لإفادة الهوية**، أو **ID-JAG**: إفادة بأن *هذا المستخدم* يستطيع، من خلال *هذا العميل*، الوصول إلى *خادم MCP هذا*. يستبدل العميل بها رمز وصول عاديًا، دون متصفح أو شاشة موافقة أو تسجيل ديناميكي. + +تشرح هذه الصفحة طرفي عملية الاستبدال. أما خادم MCP نفسه فلا يتغير: يظل خادم الموارد الموضّح في **[التفويض](../run/authorization.md)**، ويتحقق من الرمز الذي يصله. + +## طلبان للرموز {#two-token-requests} + +توجد جهتان مختلفتان، والتمييز بينهما أساس فهم هذه الصفحة. **موفّر هوية المؤسسة** هو موفّر الهوية في مؤسستك: يعرف الموظف، ويطبّق السياسة، ويصدر ID-JAG. لا تتواصل معه SDK مطلقًا. أما **خادم تفويض MCP** فهو الجهة نفسها التي عرضتها صفحة **[التفويض](../run/authorization.md)**: المُصدِر المذكور في البيانات الوصفية لخادم MCP، والذي يصدر الرموز التي يقبلها ذلك الخادم. في تدفّق OAuth المعتاد، غالبًا ما تؤدي جهة واحدة الدورين. هنا توجد جهتان، وقوام المنحة بأكملها أن توافق الثانية على الثقة بالأولى. + +يرسل العميل طلبًا للحصول على رمز إلى كل جهة. + +1. **إلى موفّر هوية المؤسسة.** يستبدل العميل بإثبات تسجيل دخول المستخدم (رمز هويته في OpenID Connect) إفادة ID-JAG. هذه عملية تبادل رموز وفق [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693)، وتخضع بالكامل لواجهة API الخاصة بموفّر الهوية، و**لا تنفّذها SDK**. تنفّذها أنت داخل دالة رد نداء غير متزامنة واحدة. وهنا أيضًا يُتخذ قرار السياسة: إذا رفض موفّر الهوية، فلن يصدر ID-JAG، ولن توجد إفادة يمكن تقديمها. +2. **إلى خادم تفويض MCP.** يقدّم العميل ID-JAG عبر منحة `jwt-bearer` وفق [RFC 7523](https://datatracker.ietf.org/doc/html/rfc7523) (مع `grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer` وID-JAG في `assertion`) ويحصل على رمز الوصول. **هذا هو الطلب الذي تنفّذه SDK**، وقبوله هو الإضافة الوحيدة التي تقدّمها هذه الصفحة لخادم التفويض. + +كل ما يلي يتعلق بالطلب الثاني: العميل الذي يرسله وخادم التفويض الذي يجيب عنه. + +## العميل {#the-client} + +توجد **`IdentityAssertionOAuthProvider`** في `mcp.client.auth.extensions.identity_assertion`. وهي، مثل كل موفّر في **[عملاء OAuth](oauth-clients.md)**، من نوع `httpx2.Auth`: أنشئ نسخة منها، وضعها في `auth=`، ومرّر `httpx2.AsyncClient` إلى وسيلة النقل. + +```python title="client.py" hl_lines="49-50 53-61" +--8<-- "docs_src/identity_assertion/tutorial001.py" +``` + +اقرأ المثال بدءًا من نهايته. + +* `main()` هي دالة `main()` المعتادة لعميل OAuth (**[عملاء OAuth](oauth-clients.md)**)، دون تغيير أي سطر. وهذا هو المقصود: بعد إنشاء الموفّر، لا يحتاج أي جزء لاحق إلى معرفة المنحة التي أنتجت الرمز. +* يأخذ الموفّر ما لا تستطيع الموفّرات الأخرى اكتشافه: `client_id` و`client_secret` **سجّلهما شخص مسبقًا** لدى خادم التفويض، و`issuer` الخاص بذلك الخادم، و`assertion_provider`، وهي دالة رد نداء غير متزامنة تعيد ID-JAG جديدًا عند الطلب. +* يستخدم `storage` بروتوكول `TokenStorage` نفسه. لا تُستدعى سوى دالتي الرموز؛ فلا يوجد تسجيل ديناميكي هنا، وبالتالي لا توجد `client_info` لحفظها. + +### موفّر الإفادة {#the-assertion-provider} + +الدالة `fetch_id_jag(audience, resource)` هي الشيفرة الوحيدة التي تكتبها. يجري انتظارها مرة لكل تبادل رموز، وليس عند الإنشاء، وفقط *بعد* جلب البيانات الوصفية لخادم التفويض والتحقق منها، حتى لا يؤدي إعداد المُصدِر بصورة خاطئة إلى تسريب إفادة. وسيطتاها تمثلان ادعاءين يجب تضمينهما عند إصدار ID-JAG: `audience` هو مُصدِر خادم التفويض (`aud` في ID-JAG)، و`resource` هو المعرّف المعتمد لخادم MCP (`resource` في ID-JAG). أما الادعاء الثالث فتعرفه بالفعل: يجب أن يسمّي ادعاء `client_id` في ID-JAG قيمة `client_id` التي أعطيتها للموفّر، وإلا رفض خادم التفويض التبادل. + +الدالة `idp_issue_id_jag` التي تسبقها **ليست شيفرتك**. إنها تحاكي موفّر الهوية، وتوقّع الإفادة داخل العملية حتى يكون الملف مكتملًا وتستطيع قراءة كل ادعاءات ID-JAG. أما `fetch_id_jag` الفعلية فتنفّذ الطلب الأول في القسم السابق: تبادل رموز وفق [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693) مع موفّر الهوية، كما تحدده مسودة منحة تفويض JWT لإفادة الهوية التي يضع [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) ملف تعريف لها. يُمرّر رمز هوية المستخدم الذي سجّل دخوله في `subject_token`، وتكون قيمة `requested_token_type` هي URN الخاص بـID-JAG (`urn:ietf:params:oauth:token-type:id-jag`)، وتُمرّر `audience` و`resource` مباشرة، ويحمل الرد ID-JAG. ابحث في توثيق موفّر الهوية عن هذا التبادل بهذه الأسماء. + +!!! tip + يُطلب ID-JAG جديد لكل تبادل، وهذا مقصود: فهي منحة تُستخدم مرة واحدة، + ولا تعيش إلا دقائق، ويرفض خادم التفويض في هذه الصفحة قبول الإفادة نفسها + مرتين. لا تخزّنها مؤقتًا. رمز الوصول الذي تحصل عليه مقابلها هو ما يُعاد استخدامه. + +### المُصدِر جزء من الإعداد {#the-issuer-is-configuration} + +هنا تنعكس الآلية. تسأل `OAuthClientProvider` خادم الموارد عن خادم التفويض الذي ينبغي استخدامه، وتتبع الإجابة أينما أشارت. يرفض هذا الموفّر ذلك: فـ`issuer` مطلوب، وتُجلب البيانات الوصفية وفق [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414) من المسار المعروف للمُصدِر نفسه، ويجب أن تكون نقطة نهاية الرموز على أصل المُصدِر نفسه، ولا يُسأل خادم الموارد عن أي شيء. + +لا يفرض الامتداد ذلك؛ إنه اختيار أشد صرامة عن قصد. يحمل هذا العميل عنصرين ثمينين للمهاجم: سرًا مسجّلًا مسبقًا وإفادة مرتبطة بجمهور محدد. وإذا سمح العميل لخادم MCP مخترق بتوجيهه إلى خادم تفويض للمهاجم، فسيرسل إليه العنصرين. يزيل تثبيت المُصدِر عند الإنشاء هذا الاحتمال. + +!!! warning + تُقارَن قيمة `issuer` في الإعداد بحقل `issuer` في وثيقة البيانات الوصفية وفق RFC 8414 §3.3 + بمقارنة نصية بسيطة: حرفًا بحرف، بما في ذلك الشرطة المائلة الأخيرة، ودون تطبيع. + لا تخمّن القيمة. اجلب `/.well-known/oauth-authorization-server` من خادم التفويض + وانسخ قيمة `issuer` التي يعيدها. بالنسبة إلى خادم التفويض في هذه الصفحة، فهي + `https://auth.example.com/`، مع الشرطة المائلة، لأن مُصدِره بُني من كائن URL في pydantic. + يوقف عدم التطابق التدفّق بالخطأ `OAuthFlowError: Authorization server metadata issuer + mismatch` قبل إرسال أي بيانات اعتماد أو إفادة. + +### عميل سري {#a-confidential-client} + +تُطلب `client_secret`؛ ويرفع المُنشئ `ValueError` عند غيابها. يوصي ملف تعريف IETF الذي يستند إليه [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) بهذه المنحة للعملاء السريين فقط، ويترك [RFC 7521](https://datatracker.ietf.org/doc/html/rfc7521) هذه السياسة لخادم التفويض. تتبع SDK التفسير المتحفظ في الجانبين: يرفض خادم التفويض المدمج العميل الذي لا يملك سرًا مشتركًا، ويشترط هذا الموفّر وجوده. تحدد `token_endpoint_auth_method` موضع إرساله: `client_secret_post` (الافتراضي، في جسم النموذج) أو `client_secret_basic` (في ترويسة HTTP Basic). يسمح ملف التعريف أيضًا بـ`private_key_jwt`، لكن هذا الموفّر لا يدعمه. + +!!! tip + اقرأ `client_secret` من البيئة أو من مدير للأسرار، ولا تحفظه في نظام التحكم في الإصدارات. + +### ما ينفّذه الموفّر نيابة عنك {#what-the-provider-does-for-you} + +يُرسل الطلب الأول دون مصادقة، ويبدأ رد الخادم `401` التدفّق. + +1. **الاكتشاف.** يجلب البيانات الوصفية لخادم التفويض من المسار المعروف وفق [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414) للمُصدِر المُعدّ، ويتحقق من تطابق `issuer` في الوثيقة ومن وجود نقطة نهاية الرموز على أصل المُصدِر نفسه. +2. **الإفادة.** ينتظر `assertion_provider` التي وفّرتها. +3. **التبادل.** يرسل منحة `jwt-bearer` بطلب POST إلى نقطة نهاية الرموز، ويخزّن `OAuthToken`، ويعيد طلبك الأصلي مع `Authorization: Bearer ...`. + +يعيد رد `403` الذي تذكر فيه `WWW-Authenticate` قيمة `insufficient_scope` الخطوتين 2 و3، باستخدام اتحاد `scope` لديك والنطاق المطلوب في الرد. (تظل `scope` مجرد طلب؛ ولا يمنح خادم التفويض في هذه الصفحة سوى ما تنص عليه ID-JAG.) لا يوجد رمز تحديث في أي جزء من هذه العملية: عندما تنتهي صلاحية رمز الوصول، يؤدي رد `401` التالي إلى إصدار ID-JAG جديد وتبادل جديد، و*هذه* هي وسيلة التحكم التي يملكها موفّر الهوية. تستخدم الإخفاقات الاستثناءين نفسيهما كما في بقية **[عملاء OAuth](oauth-clients.md)**: `OAuthFlowError` للاكتشاف والتحقق، وصنفه الفرعي `OAuthTokenError` عندما ترفض نقطة نهاية الرموز. + +## خادم التفويض {#the-authorization-server} + +في معظم الحالات، يمكنك التوقف هنا. يكون خادم تفويض MCP منتجًا لجهة أخرى، وتفعّل قبول ID-JAG في إعداداته، ويقتصر جانب SDK من [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) على العميل أعلاه. + +تستطيع SDK أيضًا أن *تكون* خادم التفويض: تعيد `create_auth_routes` مسارات خادم التفويض في قائمة تستطيع أي تطبيقات Starlette تركيبها، وهذه هي طريقة تشغيل `examples/servers/simple-auth/` في المستودع. يضيف SEP-990 خيارًا واحدًا ودالة واحدة إلى هذه الواجهة: + +```python title="auth_server.py" hl_lines="48-50 105-107" +--8<-- "docs_src/identity_assertion/tutorial002.py" +``` + +* تتحكم `identity_assertion_enabled=True` في تفعيل كل ذلك. عندما تكون معطّلة، وهو الافتراضي، تجيب `/token` عن هذه المنحة بـ`unsupported_grant_type` حتى لو نفّذت الدالة، ولا تذكرها البيانات الوصفية. عند تفعيلها، تضيف البيانات الوصفية نوع المنحة `jwt-bearer` وتدرج `urn:ietf:params:oauth:grant-profile:id-jag` في `authorization_grant_profiles_supported`، وهو الحقل الذي يستخدمه الامتداد للإعلان عن الدعم. (لا يقرأه عميل SDK هذا مطلقًا: فهو مُعدّ لمُصدِر واحد ويرسل الطلب مباشرة.) +* تمثل **`exchange_identity_assertion`** الدالة المطلوبة. قبل تشغيلها، تكون SDK قد صادقت العميل ورفضت العملاء العموميين والعملاء الذين لا يُدرج تسجيلهم هذه المنحة. تتلقى `IdentityAssertionParams` (بما فيها `assertion` الخام و`scopes` المطلوبة و`resource`) وتعيد `OAuthToken` عاديًا. +* رفض العملاء العموميين سياسة SDK، وليس متطلبًا في المواصفة. يصادق الخادم المدمج العملاء بسر مشترك فقط: فلا يدعم `private_key_jwt`، ولا يحل وثائق البيانات الوصفية لمعرّف العميل بعد ([#1801](https://github.com/modelcontextprotocol/python-sdk/issues/1801))، ولذلك لا يستطيع عميل مُعرّف بهذه الطريقة استخدام المنحة هنا. تستطيع بيئة نشر تحتاج إلى سياسة أخرى استبدال مسار `/token` الذي تعيده `create_auth_routes` بمسار خاص بها. +* يرفض التسجيل الديناميكي للعملاء هذه المنحة دون استثناء، ولذلك تخدم `get_client` هنا عميلًا جرى إعداده يدويًا. لا يستطيع عميل ID-JAG تسجيل نفسه ليصبح موجودًا. +* نصف الصنف عبارة عن حالات رفض. تمثل `OAuthAuthorizationServerProvider` خادم التفويض *بأكمله*، ولذلك تطلب أيضًا تنفيذ تدفّق رمز التفويض؛ والخادم الذي يتيح تسجيل دخول المستخدمين ينفّذ تلك الدوال فعليًا، أما هذا الخادم فله مدخل واحد فقط. + +!!! warning + لا تفك SDK ترميز الإفادة مطلقًا: وحدها بيئة نشرك تعرف موفّر الهوية الذي تثق به + والمفاتيح التي ينشرها، ولذلك تعتمد الحماية على كل ما تنفّذه داخل `exchange_identity_assertion`. + تحقّق من التوقيع باستخدام مفاتيح موفّر الهوية المنشورة (JWKS الخاص به؛ أما السر المشترك هنا + فهو خاص بالمثال)، ومن `iss` و`exp` وفق [RFC 7523](https://datatracker.ietf.org/doc/html/rfc7523) §3. اشترط أن تكون `typ` في ترويسة JWT + مساوية لـ`oauth-id-jag+jwt`، وهي حماية ملف التعريف من إعادة استخدام JWT آخر كمنحة. + اشترط أن تساوي `aud` مُصدِرك. واشترط أن يساوي ادعاء `client_id` في ID-JAG العميل + الذي صادقت عليه دالة المعالجة، وأن يسمّي ادعاء `resource` موردًا تخدمه فعليًا. + تتبّع `jti` حتى انتهاء `exp` للإفادة حتى تُقبل مرة واحدة. وخذ النطاقات الممنوحة، + وخصوصًا `resource` في الرمز المُصدَر، من ID-JAG المتحقق منها، وليس من + الطلب: فقيمة `params.resource` هي ما أدخله العميل. توجد قواعد المعالجة الكاملة في + [مواصفة التفويض الذي تديره المؤسسة](https://modelcontextprotocol.io/extensions/auth/enterprise-managed-authorization). + +ارفض الإفادة غير الصالحة باستخدام `TokenError("invalid_grant", ...)`. رمز الخطأ الآخر في هذا التدفّق هو `invalid_target`: تُرفض به ID-JAG التي تسمّي موردًا لا تخدمه، وهو ما يمنع هذا الخادم من إصدار رموز لموارد خادم آخر. وتأتي النطاقات الممنوحة من ادعاء `scope` في ID-JAG (وتُرفض أيضًا الإفادة التي لا تتضمنه)؛ وقد يربط تنفيذك مجموعات المستخدم بالنطاقات بدلًا من ذلك. + +لاحظ أيضًا ما لا يحمله `OAuthToken` المُعاد: رمز تحديث. يقرر موفّر الهوية مدة استمرار وصول المستخدم من خلال قرار إصدار ID-JAG التالية. إصدار رمز تحديث هنا يعيد ذلك القرار إلى خادم التفويض دون أن يكون الأمر واضحًا. + +!!! info + يصل الخادم الذي ما زال يضم خادم التفويض داخله باستخدام `auth_server_provider=` إلى الشيفرة + نفسها عبر `AuthSettings(identity_assertion_enabled=True)`. تشرح صفحة **[التفويض](../run/authorization.md)** سبب عدم بدء + الخوادم الجديدة بهذه الطريقة. + +!!! check + اربط الملفين في هذه الصفحة، وستكون المنحة بأكملها طلب `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`، ولا جلب للبيانات الوصفية للموارد المحمية. الطلبات الوحيدة + المرسلة هي الطلب الذي تلقى `401`، وجلب المسار المعروف، وهذا التبادل، ثم حركة + MCP العادية مع رمز حامل مرفق. وتطابق قيمة `sub` التي قرأها متحقّقك من ID-JAG + تمامًا ما تعيده `get_access_token().subject` داخل الأداة. + +### جرّبه {#try-it} + +يمثل `examples/stories/identity_assertion/` في مستودع SDK هذه الصفحة في برنامج فعلي: متحقّق `exchange_identity_assertion` نفسه، وخادم MCP يشترط رموزه، وموفّر هوية بديل للتجربة، والعميل، في برنامج واحد يتحقق من نفسه. يشغّل `uv run python -m stories.identity_assertion.client --http` التبادل كاملًا ويتحقق من أن المستخدم الذي سمّاه موفّر الهوية هو المستخدم الذي تراه الأداة. + +## مراجعة {#recap} + +* يتيح [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) لموفّر هوية المؤسسة، بدلًا من المستخدم النهائي، تحديد خوادم MCP التي يستطيع العميل الوصول إليها. يوقّع موفّر الهوية هذا القرار في **ID-JAG**. +* الحصول على ID-JAG هو تبادل رموز وفق [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693) مع *موفّر هويتك*، ولا تنفّذه SDK. أما تقديمها إلى خادم تفويض MCP فهو منحة `jwt-bearer` وفق [RFC 7523](https://datatracker.ietf.org/doc/html/rfc7523)، وتنفّذ SDK كلا جانبيها. +* `IdentityAssertionOAuthProvider` تنفيذ آخر لـ`httpx2.Auth`: عميل سري مسجّل مسبقًا، و`issuer` مثبت، ودالة رد نداء واحدة هي `assertion_provider(audience, resource)`. دون متصفح أو تسجيل أو رمز تحديث. +* لا يُكتشف خادم التفويض من خادم الموارد مطلقًا. اضبط `issuer` على النص نفسه تمامًا الذي تعرضه وثيقة البيانات الوصفية؛ فالمقارنة حرف بحرف. +* على جانب الخادم، استخدم `identity_assertion_enabled=True` مع `exchange_identity_assertion`. تصادق SDK العميل وتتحكم في قبول المنحة؛ أما التحقق من ID-JAG فمسؤوليتك بالكامل، ويرتبط الرمز المُصدَر بـ`resource` في ID-JAG، وليس في الطلب. + +الجهة الوحيدة التي لم تغيّرها هذه الصفحة هي خادم MCP. فما يفعله بالرمز الذي أصدرته الآن هو ما كان يفعله بالفعل في **[التفويض](../run/authorization.md)**. diff --git a/i18n/ar/pages/client/index.md b/i18n/ar/pages/client/index.md new file mode 100644 index 0000000000..9149998028 --- /dev/null +++ b/i18n/ar/pages/client/index.md @@ -0,0 +1,231 @@ +--- +translation: + sections: [ebef1e7a0df854f4, 7e16449f66e7dfd6, eeb0682f7d2a1079, 5713f0196a34e6e7, 0e844597859e4248, 3a97d9195ddcc92e, 1da08c483e59c141, 84702cc6e0a1fd42, 8dee7a31c86ffc5c, 83a5bce168ef23d7] + tool: 1 +--- +# العميل {#the-client} + +**`Client`** وسيلة برنامج Python للتواصل مع خادم MCP. + +كائن واحد بدورة حياة واحدة: أنشئه، وادخل `async with`، واستدعِ الطرق. كل عملية في البروتوكول (عرض الأدوات أو استدعاء واحدة أو قراءة مورد أو توليد رسائل قالب توجيه) طريقة `async` عليه تعيد نتيجة محددة النوع. + +## عميلك الأول {#your-first-client} + +يحتاج العميل إلى خادم يتواصل معه. خادم Bookshop هذا هو ما تتصل به كل أمثلة الصفحة. احفظه باسم `server.py` واتركه يعمل عبر HTTP: + +```python title="server.py" +--8<-- "docs_src/client/tutorial001.py" +``` + +```console +uv run mcp run server.py --transport streamable-http +``` + +يُتاح على `http://localhost:8000/mcp`. العميل برنامج مستقل. احفظه باسم `client.py` وشغّل `python client.py` في نافذة طرفية ثانية: + +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/client/tutorial001_client.py" +``` + +* تتلقى `Client("http://localhost:8000/mcp")` **URL**، فتتصل عبر Streamable HTTP بالخادم الذي بدأته للتو. +* `async with` هي **دورة الحياة**. الدخول يتصل ويتفاوض؛ والخروج يقطع الاتصال. لا زوج `connect()` / `close()`، ولا يمكن إعادة استخدام `Client` بعد انتهاء الكتلة. +* داخل الكتلة، تكون معلومات الاتصال موجودة بالفعل كخصائص عادية. + +### ما تستطيع تمريره إلى `Client` {#what-you-can-pass-to-client} + +تأخذ `Client` وسيطة موضعية واحدة وتحدد وسيلة النقل من نوعها: + +* سلسلة URL (`Client("http://localhost:8000/mcp")`): Streamable HTTP، وسيلة النقل التي تنشر عبرها. +* `StdioServerParameters`: الأمر الذي يُشغَّل كـ**عملية فرعية** محلية، ويجري التواصل معه عبر stdin وstdout. +* **وسيلة نقل**: أي شيء تستطيع استخدامه بصيغة `async with ... as (read, write)`، مثل `streamable_http_client(url, http_client=...)` حول عميل HTTP الخاص بك. +* نسخة `MCPServer` (أو `Server` منخفض المستوى): اتصال **داخل العملية**، دون عملية فرعية أو منفذ. هذا للاختبارات، وتبني عليه **[الاختبار](../get-started/testing.md)**. + +بقية هذه الصفحة متطابقة في الأشكال الأربعة. للترويسات والعمليات الفرعية والمهل وبروتوكول `Transport` صفحة خاصة: **[وسائل نقل العميل](transports.md)**. + +### ما يوجد على عميل متصل {#whats-on-a-connected-client} + +أربع خصائص للقراءة فقط، تُملأ بمجرد دخول الكتلة: + +* `client.server_info`: هوية الخادم، أو `None` لخادم جيل 2026 لا يقدّمها (تقدّمها خوادم python-sdk افتراضيًا). هنا `server_info.name` هي `"Bookshop"`، و`server_info.version` ما يعلنه الخادم. +* `client.server_capabilities`: ما يستطيع الخادم فعله (`tools` و`resources` و`prompts` و`completions`، ...). القدرة التي لا يملكها الخادم هي `None`. +* `client.protocol_version`: إصدار البروتوكول الذي اتفق عليه الطرفان. وهو هنا `"2026-07-28"`. +* `client.instructions`: سلسلة `instructions=` للخادم، أو `None` إذا لم يعيّنها. + +لم تختر إصدار بروتوكول. تفحص `Client` الخادم افتراضيًا وتعود إلى المصافحة التقليدية مع الخوادم الأقدم، فيعمل عميل واحد مع أي جيل خادم. إذا احتجت إلى التحكم بذلك، فتتضمن **[إصدارات البروتوكول](../protocol-versions.md)** التفاصيل كاملة. + +!!! tip + `client.session` هي `ClientSession` الأساسية، منفذ التحكم المباشر منخفض المستوى. + لن تحتاج إليها لأي شيء في هذه الصفحة. + +## عرض الأدوات {#listing-tools} + +```python title="client.py" hl_lines="8-13" +--8<-- "docs_src/client/tutorial002.py" +``` + +تعيد `list_tools()` كائن `ListToolsResult`؛ وتوجد الأدوات في `.tools`. كل منها تعريف كامل يقدّمه المضيف للنموذج. إليك الأولى: + +```python +tool.name # 'search_books' +tool.title # 'Search the catalog' +tool.description # 'Search the catalog by title or author.' +``` + +و`tool.input_schema` هو JSON Schema الذي اشتقه الخادم من تلميحات أنواع الدالة: + +```json +{ + "type": "object", + "properties": { + "query": {"title": "Query", "type": "string"}, + "limit": {"default": 10, "title": "Limit", "type": "integer"} + }, + "required": ["query"], + "title": "search_booksArguments" +} +``` + +ذلك المخطط كل ما تحتاج إليه الواجهة لعرض نموذج وسائط، وكل ما يحتاج إليه النموذج لإنتاج وسائط صالحة. + +سُجّلت الأداة الثانية `lookup_book` دون `title=`، لذا تكون `tool.title` هي `None`. + +!!! tip + `title` اختياري، لذا يجب على واجهة تعرض الأدوات لشخص الاختيار: `title` إن وُجد، + وإلا `name`. تفعل `from mcp.shared.metadata_utils import get_display_name` ذلك بالضبط + للأدوات والموارد وقوالب الموارد وقوالب التوجيه. + +## استدعاء أداة {#calling-a-tool} + +تشغّل `call_tool(name, arguments)` الأداة وتعيد `CallToolResult`. + +```python title="client.py" hl_lines="9-16" +--8<-- "docs_src/client/tutorial003.py" +``` + +تعيد `lookup_book` على الخادم نموذج Pydantic باسم `Book`. إليك ما يراه العميل: + +```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 +``` + +قيمة إرجاع واحدة وثلاثة أشياء للقراءة. لكل منها مستهلك مختلف. + +### `content`: ما يقرؤه النموذج {#content-what-the-model-reads} + +`content` هي `list` من **كتل المحتوى**، وكتلة المحتوى اتحاد أنواع: `TextContent` أو `ImageContent` أو `AudioContent` أو `ResourceLink` أو `EmbeddedResource`. تستطيع الأداة إعادة عدة كتل من أنواع مختلفة. + +لذلك تضيّق `main` النوع باستخدام `isinstance(block, TextContent)` قبل الوصول إلى `block.text`. لاحظ غياب `.text` خارج `isinstance`: يمنعه فاحص الأنواع لأن `ImageContent` تملك `.data` لا `.text`. يعكس اتحاد الأنواع بدقة ما يجوز للأداة إرساله؛ وينبغي لشيفرتك مراعاة ذلك أيضًا. + +### `structured_content`: ما يقرؤه تطبيقك {#structured_content-what-your-application-reads} + +`structured_content` هي قيمة إرجاع الأداة بصيغة JSON، مطابقة لـ`output_schema` المعلنة للأداة. لا تحليل نصوص ولا تخمين. + +عند وجودهما معًا، يعبّران عن الشيء نفسه مرتين عمدًا: `content` للنموذج و`structured_content` للشيفرة. مصدر الجزء المنظّم وكيفية التحكم به في صفحة **[المخرجات المنظّمة](../servers/structured-output.md)**. + +### `is_error`: هل فشلت الأداة؟ {#is_error-whether-the-tool-failed} + +الأداة التي تثير استثناءً **لا** تثيره في عميلك. تعود كنتيجة عادية مع `is_error=True`. + +!!! check + اطلب من `lookup_book` العنوان `"Solaris"` (غير الموجود في الفهرس)، فتثير الدالة + `ToolError`. ما زال الاستدعاء يعود بصورة عادية: + + ```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 + ``` + + تظهر رسالة `ToolError` في `content` حيث يستطيع **النموذج** قراءتها والمحاولة مجددًا. هذا + مقصود: خطأ الأداة جزء من المحادثة، لا انهيار. (لو انهارت الأداة + باستثناء آخر، لما قالت `content` إلا `Error executing tool lookup_book`.) افحص دائمًا + `is_error` قبل الوثوق بـ`structured_content`. + +!!! warning + يشمل `is_error=True` أكثر من `raise` التي تكتبها. اطلب أداة لا يملكها الخادم أصلًا + (`call_tool("does_not_exist", {})`)، ولا يُثار استثناء. تحصل على الشكل نفسه: + `is_error=True` مع `Unknown tool: does_not_exist` في `content`. لا تثير طريقة `Client` + الاستثناء `MCPError` إلا عندما يجيب الخادم بـ**خطأ** JSON-RPC بدلًا من نتيجة، + وتغطي **[معالجة الأخطاء](../servers/handling-errors.md)** متى ينتج الخادم كل نوع. + +## الموارد {#resources} + +عمليات الموارد مترابطة: طريقتان لعرض القوائم وطريقة للقراءة. + +```python title="client.py" hl_lines="9-18" +--8<-- "docs_src/client/tutorial004.py" +``` + +* تعيد `list_resources()` الموارد **المحددة**، ذات URI ثابت. هنا: `['catalog://genres']`. +* تعيد `list_resource_templates()` الموارد **ذات المَعلمات**. هنا: `['catalog://genres/{genre}']`. هما قائمتان مختلفتان لأن القالب لا يُقرأ حتى تملأه. +* تأخذ `read_resource(uri)` عنوان URI كـ`str` عادية وتعمل مع النوعين: مرّر `"catalog://genres/poetry"` فيطابقه الخادم بالقالب. + +تعيد `read_resource` الحقل `contents`، وهو قائمة `TextResourceContents` أو `BlobResourceContents`. الفكرة نفسها لمحتوى الأداة: ضيّق باستخدام `isinstance`، ثم اقرأ `.text` (أو `.blob`). + +يمكن أيضًا إبلاغ العميل عندما يتغير مورد. على اتصالات جيل 2025، يُستخدم `subscribe_resource(uri)` / `unsubscribe_resource(uri)`، وهو زوج طرق لا تنفّذه `MCPServer`، لذلك يجيب الطلب على بروتوكول 2026-07-28 (حيث لم تعد العمليتان موجودتين) بـ`-32601`، *الطريقة غير موجودة*. بديل 2026 تدفّق `subscriptions/listen` الذي *تخدمه* `MCPServer`؛ وتكون `server_capabilities.resources.subscribe` هي `True` فيه. تشرح صفحة **[الاشتراكات](subscriptions.md)** استهلاكه باستخدام `client.listen(...)`. + +## قوالب التوجيه {#prompts} + +```python title="client.py" hl_lines="8-13" +--8<-- "docs_src/client/tutorial005.py" +``` + +تخبرك `list_prompts()` بما يتيحه الخادم وما يحتاج إليه كل قالب توجيه (prompt): + +```python +prompt.name # 'recommend' +prompt.title # 'Recommend a book' +prompt.arguments # [PromptArgument(name='genre', required=True)] +``` + +تولّد `get_prompt(name, arguments)` رسائله. قاموس الوسائط هو `str -> str`: وسائط قوالب التوجيه نصوص دائمًا. النتيجة `messages`، قائمة `PromptMessage` لكل منها `role` وكتلة `content`: + +```python +message.role # 'user' +message.content # TextContent(type='text', text='Recommend one poetry book from the catalog and say why.') +``` + +يسلّم المضيف تلك الرسائل مباشرة إلى النموذج. هذه الميزة كاملة. + +## الإكمالات {#completions} + +يستطيع خادم يملك دالة معالجة للإكمال اقتراح قيم وسائط قوالب التوجيه والموارد تلقائيًا أثناء كتابة المستخدم. + +```python title="client.py" hl_lines="9-13" +--8<-- "docs_src/client/tutorial006.py" +``` + +* يحدد `ref` *أي* قالب توجيه أو مورد تملؤه: `PromptReference` أو `ResourceTemplateReference`. +* `argument` هي `{"name": ..., "value": ...}`: الوسيطة وما كتبه المستخدم حتى الآن. + +الإجابة في `result.completion.values`. اكتب `"p"` فيعيد الخادم `['poetry']`. جانب الخادم وكيف تستخدم دالة المعالجة الوسائط *الأخرى* المملوءة مسبقًا لتضييق الاقتراحات في صفحة **[الإكمالات](../servers/completions.md)**. + +## تقسيم النتائج إلى صفحات {#pagination} + +تأخذ كل طريقة `list_*` الخيار `cursor=`، وتحمل كل نتيجة `next_cursor`. عندما تكون `next_cursor` هي `None`، تكون لديك جميع النتائج. + +```python title="client.py" hl_lines="7-15" +--8<-- "docs_src/client/tutorial007.py" +``` + +تعمل `list_all_tools` بصورة صحيحة مع كل خادم. تعيد `MCPServer` كل شيء في صفحة واحدة، لذا تكون `next_cursor` هي `None` وتعمل الحلقة مرة واحدة، ولذلك لا تكتبها معظم الشيفرات. الخوادم التي تقسّم النتائج فعلًا وقواعد المؤشرات في **[تقسيم النتائج إلى صفحات](../advanced/pagination.md)**. + +## في الاختبارات {#in-tests} + +تصل كل `client.py` في هذه الصفحة إلى `server.py` عبر HTTP. في اختبار، تتجاوز الشبكة وتمرّر إلى `Client` كائن الخادم نفسه: `from server import mcp` ثم `Client(mcp)`. لا عملية ولا منفذ، وتعمل كل الطرق أعلاه بالطريقة نفسها. + +يوجد خيار مُنشئ لهذا الغرض: `Client(mcp, raise_exceptions=True)`. لا يؤثر إلا في الاتصالات داخل العملية، وتشرحه **[الاختبار](../get-started/testing.md)** وتبني النمط كاملًا حوله. + +## مراجعة {#recap} + +* تتصل `Client(x)` عبر Streamable HTTP بسلسلة URL، وتشغّل عملية فرعية لـ`StdioServerParameters`، وتدخل وسيلة نقل مباشرة، وتأخذ كائن الخادم نفسه في الاختبارات. +* `async with` هي دورة الحياة كاملة. داخلها تكون `server_capabilities` و`protocol_version` مملوءتين بالفعل؛ وكذلك `server_info` و`instructions` عندما يقدّمهما الخادم. +* تمنحك `list_tools()` حقول `name` و`title` و`description` و`input_schema` لكل أداة. +* تعيد `call_tool()` كلًّا من `content` للنموذج و`structured_content` لشيفرتك و`is_error`. الأداة التي تثير استثناءً تعيد نتيجة، لا استثناءً في العميل. +* `content` اتحاد أنواع كتل؛ ضيّق باستخدام `isinstance` قبل القراءة. +* تكمل `list_resources` / `list_resource_templates` / `read_resource` و`list_prompts` / `get_prompt` و`complete` العمليات. +* تأخذ كل `list_*` الخيار `cursor=`؛ كرر حتى تصبح `next_cursor` هي `None`. + +ما يستطيع الخادم طلبه من *العميل*، وكيف تجيب، في **[دوال رد النداء لدى العميل](callbacks.md)**. diff --git a/i18n/ar/pages/client/oauth-clients.md b/i18n/ar/pages/client/oauth-clients.md new file mode 100644 index 0000000000..09fe857657 --- /dev/null +++ b/i18n/ar/pages/client/oauth-clients.md @@ -0,0 +1,155 @@ +--- +translation: + sections: [c6899d3892bd9fa0, 79372cff3cc48a88, c2dae1ebe2ebd543, 13175843d3588af4, df06056fb16b3846, 758f06399b513c1f, a05d7278487d610b] + tool: 1 +--- +# عملاء OAuth {#oauth-clients} + +بعض خوادم MCP محمية. أرسل طلبًا دون رمز فتجيب بـ`401 Unauthorized`. + +**`OAuthClientProvider`** وسيلتك للحصول على الرمز. ليست كائن MCP أصلًا، بل `httpx2.Auth`، خطاف httpx2 القياسي لتطبيق إجراء على كل طلب. تربطها بـ`httpx2.AsyncClient`، وتمرّر ذلك العميل إلى وسيلة نقل Streamable HTTP، ولا تحتاج إلى التفكير فيها بعد ذلك. + +هذه الصفحة لجانب العميل. جعل خادمك يطلب رمزًا في **[التفويض](../run/authorization.md)**. + +## المزوّد {#the-provider} + +```python title="client.py" hl_lines="44-54" +--8<-- "docs_src/oauth_clients/tutorial001.py" +``` + +تقدّم له أربعة أشياء: + +* `server_url`: نقطة نهاية MCP التي تتصل بها. يكتشف المزوّد كل ما عداها منها. +* `client_metadata`: ما تكتبه في نموذج "تسجيل تطبيق" لدى خادم تفويض. +* `storage`: موضع حفظ الرموز بين مرات التشغيل. +* `redirect_handler` و`callback_handler`: اللحظتان اللتان يشارك فيهما شخص. + +لا يذكر باقي الملف OAuth. لا ترى `main()` رمزًا أبدًا. + +### البيانات الوصفية للعميل {#client-metadata} + +`OAuthClientMetadata` هو مستند التسجيل الفعلي وفق [RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591)، كنموذج Pydantic. + +تعيّن ثلاثة حقول. تملأ القيم الافتراضية الباقي: `grant_types` هي `["authorization_code", "refresh_token"]` بالفعل، و`response_types` هي `["code"]`، وهو التدفق الذي ينفّذه هذا المزوّد بالضبط. + +!!! check + لأنه نموذج Pydantic، يتحقق من البيانات **قبل إرسال بايت واحد عبر الشبكة**. + احذف `redirect_uris` فيفشل الإنشاء فورًا بـ`ValidationError` يذكر + الحقل: + + ```text + redirect_uris + Field required [type=missing, input_value={'client_name': 'Bookshop Agent'}, input_type=dict] + ``` + + لا يُفتح متصفح ولا يبقى تسجيل غير مكتمل على خادم التفويض. + +### تخزين الرموز {#token-storage} + +**`TokenStorage`** هو `Protocol` بأربع طرق غير متزامنة. لا ترث من شيء؛ اكتب الطرق فتصبح أي فئة مخزن رموز: + +* تحتفظ `get_tokens` / `set_tokens` بكائن `OAuthToken`: رمز الوصول ورمز التحديث ووقت انتهاء الصلاحية والنطاق. +* تحتفظ `get_client_info` / `set_client_info` بكائن `OAuthClientInformationFull` الذي أصدره خادم التفويض عند تسجيل المزوّد لك، بما فيه `client_id`. + +تعمل النسخة داخل الذاكرة أعلاه. لكنها تنسى كل شيء عند خروج العملية، فيعيد التشغيل التالي التدفق كله. احفظه في ملف أو مخزن مفاتيح منصتك، فيجري التشغيل التالي دون مطالبة. + +!!! tip + خزّن `client_info`، لا الرموز فقط. يسجّل المزوّد ديناميكيًا أول مرة + لا يجد فيها `client_info` محفوظة. إذا أهملتها، تنشئ تسجيلًا جديدًا في كل تشغيل. + +### دالتا المعالجة {#the-two-handlers} + +يحتاج تدفق رمز التفويض إلى شخص مرة واحدة: يجب أن يسجّل أحد الدخول وينقر "السماح". + +* تُنتظَر **`redirect_handler`** مع URL التفويض المبني بالكامل. يتضمن بالفعل `client_id` و`redirect_uri` و`state` وتحدي PKCE. مهمتك الوحيدة فتحه في متصفح. يستدعي تطبيق مكتبي `webbrowser.open`؛ ويطبعه هذا الملف. +* تُنتظَر **`callback_handler`** بعد ذلك. تنتظر عودة المستخدم إلى `redirect_uri` وتعيد مَعلمات استعلام التحويل كـ`AuthorizationCodeResult`. + +يشغّل العميل الفعلي خادم HTTP محليًا صغيرًا على URI التحويل بدلًا من استدعاء `input()`. الشكل نفسه: استقبل التحويل وأعِد `code` و`state` و`iss`. + +!!! warning + مرّر `state` و`iss` كما وصلتا بالضبط. يقارن المزوّد `state` بما + ولّده و`iss` بالمُصدِر الذي اكتشفه، ويرفض عدم التطابق. هما دفاعا CSRF + والخلط بين خوادم التفويض. + +### الربط بـ`Client` {#into-the-client} + +انظر إلى `main()`. يوضع المزوّد على **عميل httpx2**، ويُمرَّر عميل httpx2 إلى `streamable_http_client(url, http_client=...)`، وتُمرَّر وسيلة النقل إلى `Client`. + +لا تملك `streamable_http_client` خيار `auth=`. كل ما على مستوى HTTP (المصادقة والترويسات والمهل والوكلاء) يخص `httpx2.AsyncClient` التي توفرها. تشرح **[وسائل نقل العميل](transports.md)** هذه الطبقات. + +## ما يفعله المزوّد نيابة عنك {#what-the-provider-does-for-you} + +عندما ترسل `Client` أول طلب، يجيب الخادم بـ`401`. يتولى المزوّد العمل: + +1. **الاكتشاف.** يقرأ ترويسة `WWW-Authenticate`، ويجلب بيانات المورد المحمي من `/.well-known/oauth-protected-resource`، ويعرف خادم التفويض الذي يحمي المورد، ثم يجلب بيانات *ذلك* الخادم. (إذا لم ينشر خادم أقدم بيانات المورد، يطلب بيانات خادم التفويض من أصله نفسه بدلًا من ذلك.) في الحالتين، يجب أن تسمّي البيانات في `issuer` الخادم الذي جُلبت لأجله؛ ويُرفض غير ذلك. +2. **التسجيل.** لا شيء في التخزين؟ يسجّلك ديناميكيًا باستخدام `OAuthClientMetadata` ويحفظ النتيجة. +3. **التفويض.** يولّد زوج PKCE و`state`، ويبني URL التفويض، وينتظر `redirect_handler` ثم `callback_handler` للحصول على رمز التفويض. +4. **المبادلة.** يبادل رمز التفويض بـ`OAuthToken`، ويخزّنه، ويعيد طلبك الأصلي مع `Authorization: Bearer ...`. + +بعد ذلك يعمل بصمت. تأتي الرموز من التخزين، ويُجدَّد رمز وصول منتهي باستخدام رمز التحديث، ولا يُعاد التدفق إلا عندما لا ينجح شيء من ذلك. + +تنطبق قاعدة نقل واحدة على كل هذه الطلبات: مثل طلب MCP الذي تعمل داخله، لا تتبع تحويلًا إلا إذا بقي على الأصل نفسه واحتفظ بالطريقة (مثل 307/308 لإضافة شرطة نهائية)، وتتعامل مع أي تحويل آخر كعدم إجابة ذلك URL. + +لم تكتب شيئًا من ذلك. تبقى وسيطتان مسمّاتان (`client_metadata_url` و`validate_resource_url`)، ولا يحتاج هذا الملف إلى أيٍّ منهما. تستحق `client_metadata_url` المعرفة؛ ولها قسم أدناه. + +### جرّبه {#try-it} + +لا يفيد `Client(server)` داخل الذاكرة في اختباراتك هنا: أساس التدفق HTTP `401`، ولا يوجد HTTP بين عميل داخل الذاكرة وخادمه. + +يضم المستودع النسخة العملية. يشغّل `examples/servers/simple-auth/` خادم تفويض مستقلًا وخادم MCP محميًا؛ و`examples/clients/simple-auth-client/` عميل هذه الصفحة مطوّرًا إلى CLI صغيرة. يتضمن README الأمرين: شغّل الخوادم ثم العميل عليها، وسترى الخطوات الأربع. + +## مستندات البيانات الوصفية لمعرّف العميل {#client-id-metadata-documents} + +تهجر مراجعة المواصفة 2026-07-28 تسجيل العملاء الديناميكي لصالح **مستندات البيانات الوصفية لمعرّف العميل** (CIMD). بدلًا من إرسال POST لتسجيل جديد إلى كل خادم تفويض يقابله، ينشر عميلك مستند JSON واحدًا عنه على URL ثابت عبر HTTPS، ويكون ذلك URL *هو* `client_id`. يجلب خادم التفويض المستند؛ ولا يتعامل معه المزوّد. + +تدعم SDK ذلك بالفعل: مرّر URL كـ`client_metadata_url=` عند إنشاء المزوّد. عندما تعلن بيانات خادم التفويض `client_id_metadata_document_supported: true`، يتجاوز المزوّد طلب `/register` بالكامل: يدخل URL التدفق كـ`client_id`، ولا يوجد `client_secret`. عندما لا يعلن الخادم الدعم (ومعظمها لا يعلنه بعد)، أو لا تمرّر URL، يعود المزوّد إلى التسجيل الديناميكي **بصمت**، ويعمل كل ما سبق كما وُصف. تبقى `client_info` المخزّنة ذات الأولوية على الاثنين. + +يجب أن يستخدم URL بروتوكول HTTPS ومسارًا غير الجذر؛ يثير غير ذلك `ValueError` عند الإنشاء قبل أي اتصال شبكي. يأخذه المثال `examples/clients/simple-auth-client/` كمتغير البيئة `MCP_CLIENT_METADATA_URL`. + +## من آلة إلى آلة {#machine-to-machine} + +مهمة ليلية أو خطوة CI أو خدمة أخرى. لا متصفح ولا شخص ينقر "السماح". هذه منحة **بيانات اعتماد العميل**: لديك بالفعل `client_id` و`client_secret`، ونقطة نهاية الرموز هي التدفق كله. + +`ClientCredentialsOAuthProvider` هي `httpx2.Auth` نفسها دون الشخص: + +```python title="client.py" hl_lines="4 27-34" +--8<-- "docs_src/oauth_clients/tutorial002.py" +``` + +ما تغيّر: + +* لا `OAuthClientMetadata` ولا دوال معالجة. تمرّر `client_id` و`client_secret`؛ ويبني المزوّد تسجيل `client_credentials` محدودًا حولهما ويتجاوز التسجيل الديناميكي بالكامل. +* يحدد `issuer` خادم التفويض الذي أصدر بيانات الاعتماد؛ استخدم قيمة `issuer` التي يعيدها مستند `/.well-known/oauth-authorization-server`. ما زال الاكتشاف يعمل كما سبق، لكن لا تُبنى طلبات الرموز إلا من بيانات *ذلك* المُصدِر؛ إذا أشار خادم MCP إلى غيره، يتوقف التدفق بـ`OAuthFlowError`. إغفاله مهجور ويصبح مطلوبًا في 3.0 (راجع **[الميزات المهجورة](../deprecated.md#deprecated-sdk-helpers)**)؛ وحتى ذلك الحين، يحذّر المزوّد ويستخدم خادم التفويض الذي يكتشفه. +* `scope` سلسلة تفصلها مسافات، وهي صيغة OAuth أثناء النقل. +* كل ما بعد ذلك متطابق: `TokenStorage` نفسها و`httpx2.AsyncClient(auth=...)` نفسها و`streamable_http_client` نفسها. + +ينتقل السر افتراضيًا كمصادقة HTTP Basic على طلب الرمز (`client_secret_basic`). مرّر `token_endpoint_auth_method="client_secret_post"` لوضعه في جسم النموذج بدلًا من ذلك. لا تقبل بعض خوادم التفويض إلا إحدى الطريقتين. + +!!! tip + اقرأ `client_secret` من البيئة أو مدير أسرار، لا من نظام التحكم بالإصدارات أبدًا. + +!!! info + يوجد مزوّد آخر في `mcp.client.auth.extensions.client_credentials`: + **`PrivateKeyJWTOAuthProvider`**، للعملاء الذين يصادقون باستخدام JWT بدلًا من + سر مشترك (`private_key_jwt`، نمط زوج المفاتيح وهوية حمل العمل). يتبع + النمط نفسه: أنشئه (يأخذ `issuer` الاختياري نفسه) وضعه على `auth=`. توفر الوحدة نفسها + `SignedJWTParameters` و`static_assertion_provider`، وهما مساعدان لبناء إفادته. + +هناك حالة أخرى دون شخص: عميل تابع لمؤسسة يقرر مزوّد هويتها، لا المستخدم، خوادم MCP التي يجوز له الوصول إليها. هذه منحة مختلفة بنموذج ثقة وصفحة خاصين، **[إفادة الهوية](identity-assertion.md)**. + +## عندما يفشل {#when-it-fails} + +عندما يختل تدفق OAuth، يثير المزوّد `OAuthFlowError` من `mcp.client.auth`. له فئتان فرعيتان. تعني `OAuthRegistrationError` أن التسجيل لم ينتج عميلًا صالحًا للاستخدام: رفض خادم التفويض تسجيلك، أو سجّلك ببيانات اعتماد لا يستطيع التدفق استخدامها (مثل طريقة مصادقة لا ينفذها). وتعني `OAuthTokenError` تعذّر الحصول على رمز: رفضت نقطة نهاية الرموز، أو حمل سجل عميل محفوظ طريقة مصادقة لا يستطيع هذا العميل تطبيقها، ويُبلَّغ عنها أثناء بناء طلب الرمز بدلًا من إرساله. تغطي `except OAuthFlowError:` واحدة الاكتشاف والتسجيل والتفويض والمبادلة. + +ليس كل شيء خطأ تدفق. قد تفشل الشبكة أيضًا؛ وهذه استثناءات `httpx2` عادية تمر دون تغيير. + +## مراجعة {#recap} + +* `OAuthClientProvider` هي `httpx2.Auth`. ضعها على `httpx2.AsyncClient` ومرّرها إلى `streamable_http_client(url, http_client=...)`؛ ولا تعرف `Client` أن OAuth حدث. +* توفر أربعة أشياء: URL الخادم و`OAuthClientMetadata` و`TokenStorage` وزوج دالتي التحويل ورد النداء. +* `TokenStorage` هو `Protocol`: أربع طرق غير متزامنة دون فئة أساسية. احفظ `client_info` إلى جانب الرموز. +* الاكتشاف والتسجيل (ديناميكيًا أو عبر **مستند بيانات وصفية لمعرّف العميل**) وPKCE وفحوص `state` و`iss` وتجديد الرموز مسؤولية المزوّد، لا مسؤوليتك. +* `ClientCredentialsOAuthProvider` النسخة دون شخص: `client_id` + `client_secret`، دون دوال معالجة أو متصفح. +* كل إخفاق OAuth هو `OAuthFlowError`؛ و`OAuthRegistrationError` و`OAuthTokenError` فئتاه الفرعيتان. + +النصف الآخر من هذه المصافحة، أي جعل *خادمك* يطلب الرمز، في **[التفويض](../run/authorization.md)**. diff --git a/i18n/ar/pages/client/session-groups.md b/i18n/ar/pages/client/session-groups.md new file mode 100644 index 0000000000..4598355151 --- /dev/null +++ b/i18n/ar/pages/client/session-groups.md @@ -0,0 +1,87 @@ +--- +translation: + sections: [09c857a25a9dc37a, 43bc6a76a243a50e, 0a716022a88768df, 4b7f78042bfcfff7, c112662e61b03315, 58974ba1f489a8b4, ed4d17e894864056] + tool: 1 +--- +# مجموعات الجلسات {#session-groups} + +تتصل `Client` بخادم واحد. تحتاج التطبيقات الفعلية غالبًا إلى عدة خوادم (بحث وقاعدة بيانات وAPI داخلية)، فتدير اتصالًا وقائمة أدوات لكل منها. + +**`ClientSessionGroup`** كائن واحد يحتفظ باتصالات عديدة ويجمع كل ما تتيحه في عرض واحد. + +## خادمان {#two-servers} + +ابدأ بخادمين عاديين. لا علاقة بينهما، لذا سمّى كل منهما أداته طبيعيًا `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" +``` + +## مجموعة واحدة {#one-group} + +أنشئ `ClientSessionGroup` واستدعِ **`connect_to_server`** مرة لكل خادم: + +```python title="client.py" hl_lines="10-12" +--8<-- "docs_src/session_groups/tutorial003.py" +``` + +* تأخذ `connect_to_server` مَعلمات نقل، لا كائن خادم: `StdioServerParameters` (من `mcp`) لتشغيل عملية فرعية، أو `StreamableHttpParameters` / `SseServerParameters` (من `mcp.client.session_group`) لخادم يستمع بالفعل على URL. +* `group.tools` هي `dict[str, Tool]` لأدوات كل خادم متصل. ولـ`group.resources` و`group.prompts` الشكل نفسه. +* تبحث `group.call_tool(name, arguments)` عن الاسم وتجد الجلسة التي تملكه وتمرّر الاستدعاء. لا تحدد الخادم بنفسك. + +!!! check + ضع `client.py` بجانب الخادمين وشغّله. يرفض `connect_to_server` الثاني: + + ```text + mcp.shared.exceptions.MCPError: {'search'} already exist in group tools. + ``` + + هذا `MCPError` يُثار قبل تسجيل أي شيء من الخادم الثاني. يجب أن يكون الاسم + فريدًا في المجموعة **كلها**، وسيتصادم خادمان لا تتحكم فيهما في النهاية. + +## `component_name_hook` {#component_name_hook} + +تصلح ذلك على مستوى المجموعة، لا الخوادم. مرّر دالة تأخذ `(name, server_info)` فتشغّلها المجموعة على كل اسم تسجّله: + +```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'] +``` + +* **المفتاح** من اختيارك. بنته `by_server` من `server_info.name`، وهو الاسم الذي أُنشئت به كل `MCPServer(...)`. +* يبقى كائن `Tool` الداخلي دون تغيير: ما زالت `group.tools["Web.search"].name` هي `"search"`، وهذا الاسم الذي تضعه `call_tool` في النقل. لا تغادر البادئة عمليتك. +* لا يقتصر ذلك على الأدوات. يُسجَّل مورد `hours` للمكتبة كـ`Library.hours`. + +!!! tip + يعمل الخطاف على **كل** اسم من **كل** خادم، لا على التعارضات فقط: لا يوجد + وضع لإضافة بادئة عند التصادم وحده. اختر نظام تسمية واحدًا وطبّقه في كل مكان. + +## إضافة الخوادم وإزالتها {#adding-and-removing-servers} + +تعيد `connect_to_server` الجلسة `ClientSession` التي فتحتها. احتفظ بها إذا أردت إزالة الخادم لاحقًا: تزيل `await group.disconnect_from_server(session)` أدواته وموارده وقوالب توجيهه من المجموعة. + +إذا كانت لديك `ClientSession` متصلة بالفعل (`Client.session` إحداها)، فمرّرها إلى `await group.connect_with_session(server_info, session)` بدلًا من فتح وسيلة نقل جديدة. يجري التجميع بالطريقة نفسها. لا تغلق المجموعة جلسة لم تفتحها. تسمّي `server_info` الخادم لبادئات المكونات؛ وقد تكون `client.server_info` هي `None` على اتصال جيل 2026 (الهوية اختيارية)، فمرّر `Implementation(name=..., version=...)` الخاصة بك حينها. + +## المصافحة التقليدية {#the-classic-handshake} + +تُبنى `ClientSessionGroup` على `ClientSession`، لا `Client`. ينفّذ كل `connect_to_server` مصافحة `initialize` التقليدية. لا يرسل فحص `server/discover` الموضح في **[إصدارات البروتوكول](../protocol-versions.md)**. يفهم كل خادم MCP المصافحة، فلا تفقد توافقًا؛ بل تسلك المجموعة الطريق الأقدم والأبطأ إلى خادم يستطيع أفضل من ذلك. + +## مراجعة {#recap} + +* تحتفظ `ClientSessionGroup` باتصالات عدة خوادم وتجمع أدواتها ومواردها وقوالب توجيهها في `dict` واحدة لكل نوع. +* استخدم `connect_to_server(params)` لكل خادم. تأخذ مَعلمات نقل، لا URL أو `Transport` التي تأخذها `Client` أبدًا. +* توجّه `group.call_tool(name, arguments)` الاستدعاء إلى الخادم المالك نيابة عنك. +* يجب أن تكون الأسماء فريدة في المجموعة كلها؛ لا يتعايش خادمان بأداة `search` دون معالجة ذلك. +* تعيد `component_name_hook=` كتابة كل اسم مسجّل. يتغير مفتاح القاموس، لا الاسم المنقول. +* تضيف `connect_with_session` جلسة لديك بالفعل؛ وتزيل `disconnect_from_server` واحدة. + +المصافحة التي تستخدمها المجموعة (والأسرع التي تفضّلها `Client`) موضوع **[إصدارات البروتوكول](../protocol-versions.md)**. diff --git a/i18n/ar/pages/client/subscriptions.md b/i18n/ar/pages/client/subscriptions.md new file mode 100644 index 0000000000..2868058f32 --- /dev/null +++ b/i18n/ar/pages/client/subscriptions.md @@ -0,0 +1,91 @@ +--- +translation: + sections: [8f9558e57f29eee1, a88c587739e0465c, 46ebfd5b325ed041, 4d10b00b57ce4bd9, 2cdb0edd1f59b3e2] + tool: 1 +--- +# الاشتراكات {#subscriptions} + +فهرس الخادم ليس ثابتًا. تظهر أدوات أثناء التشغيل ويتغير المحتوى وراء URI المورد. يعرف العميل بذلك عبر `client.listen(...)`: طلب `subscriptions/listen` واحد تكون استجابته *هي* التدفّق. يبقى مفتوحًا ويحمل إشعارات التغيير التي طلبها العميل. + +هذه الصفحة لطرف العميل: فتح التدفّق ومراقبته بجانب عملك الرئيسي والتعامل مع نهايته. نشر التغييرات وترشيحها وخدمة الطريقة جانب الخادم، وتشرحه **[الاشتراكات](../handlers/subscriptions.md)** ضمن *داخل دالة المعالجة*. تتواصل الأمثلة هنا مع خادم لوحة دورة العمل المبني هناك. + +## مراقبة التدفّق {#watching-the-stream} + +الاشتراك مدير سياق واحد. يرسل دخوله الطلب، مستخدمًا وسائطك المسمّاة كمرشّح للاشتراك، وينتظر إقرار الخادم، فيكون التدفّق نشطًا عند بدء الكتلة. + +```python title="client.py" hl_lines="15 18 28" +--8<-- "docs_src/subscriptions/tutorial003.py" +``` + +ينتج التكرار أربعة أحداث محددة النوع: `ToolsListChanged` و`PromptsListChanged` و`ResourcesListChanged` و`ResourceUpdated(uri=...)`. + +يقول الحدث *ما* تغيّر، لا *كيف*. لذلك تستدعي `follow_board` كلًّا من `read_resource` و`list_tools`: الحدث إشارة لإعادة الجلب. اقرأ `event.uri` بدلًا من افتراض المورد الذي تغيّر: قد يسمّي المرشّح عدة عناوين URI، وقد يبلّغ الخادم عن تغيّر مورد فرعي لأحدها. + +تندمج الأحداث المكررة التي تنتظر الاستهلاك في حدث واحد، وما زالت إعادة الجلب تعطيك الحالة الحالية. لا تندمج إلا الأحداث المتطابقة: حدثا `ResourceUpdated` لعناوين URI مختلفة يبقيان حدثين. + +خاصيتان أخريان لكائن الاشتراك: + +* `sub.honored` هو المرشّح الذي أقرّه الخادم: `SubscriptionFilter` بالحقول التي مرّرتها، تُقرأ كخصائص (`sub.honored.prompts_list_changed`). تقبل `MCPServer` كل نوع تطلبه، فتعيد طلبك كما هو. يقر خادم يدعم أنواعًا أقل مجموعة أقل، وقد لا يصدر نوع مقبول أحداثًا رغم ذلك. وقد يرفض الخادم الطلب كله بدلًا من إقراره (راجع [تحديد من يجوز له المشاهدة](../handlers/subscriptions.md#deciding-who-may-watch) في صفحة الخادم)، ويظهر ذلك كخطأ الطلب. +* `sub.subscription_id` هو معرّف طلب الاستماع، الذي يُضاف إلى كل إطار لهذا التدفّق. يمكن فتح عدة اشتراكات معًا، ويُوجَّه كل منها بحسب معرّفه الخاص. + +## المراقبة دون حجب التنفيذ {#watching-without-blocking} + +تعمل `follow_board` حتى يغلق الخادم التدفّق، وقد لا يحدث ذلك، لذا تستحوذ وحدها على برنامجك. تريد العملاء الفعلية مراقبًا *بجانب* التدفق الرئيسي: يستدعي وكيل الأدوات بينما يحدّث المراقب ذاكرة التخزين المؤقت أو الواجهة. + +افتح الاشتراك أولًا، ثم ابدأ المراقب وتابع عملك. + +=== "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` من المثال الأول، الذي يحفظه هذا المستودع باسم + `tutorial003.py`. إذا حفظت الملفات المعروضة بجانب بعضها باسم `client.py` و`app.py`، + فاكتب `from client import BOARD, read_board` بدلًا من ذلك. ويستورد مثال `watch.py` أدناه + الدالة `read_board` بالطريقة نفسها. + +الترتيب هو الغاية. لا تُعاد الأحداث، لذا يفوتك حدث نُشر قبل وجود تدفّقك. ينتظر الدخول إلى `client.listen(...)` الإقرار، فيصل كل تغيير من تلك اللحظة إلى المراقب، ولا يمكن أن تفوت اللقطة التي تأخذها داخل الكتلة تغييرًا. + +تعمل الطلبات بحرية بجانب تدفّق مفتوح، من مهمة المراقب أو غيرها، على العميل نفسه. ولأن الأحداث غير المستهلكة *المكررة* تندمج، قد ينتج عمل رئيسي كثيف إعادة جلب واحدة بدلًا من ثلاث. لا تندمج الأحداث المختلفة: يصطف حدث معلّق لكل URI في مرشّح يسمّي عدة عناوين. + +لإيقاف المراقبة، اخرج من الكتلة: لا استدعاء `unsubscribe`. يفعله إلغاء المهمة المالكة للكتلة نيابة عنك، وتلغي SDK طلب الاستماع كما تتوقع وسيلة النقل: عبر Streamable HTTP بإغلاق تدفّق ذلك الطلب. لا يعود مراقب يعمل طوال حياة تطبيقك بمفرده، فألغِه أو ألغِ نطاق مجموعة مهامه عند الإغلاق. + +## تنتهي التدفّقات {#streams-end} + +ينتهي التدفّق بإحدى طريقتين، وكلاهما تدفق تحكم عادي. ينهي إغلاق الخادم السلس `async for`؛ ويثير الانقطاع المفاجئ `SubscriptionLost`. + +الفرق تشخيصي، لا في الخطوة التالية: انتهى التدفّق ولم تُعَد أحداث، ويعيد المراقب الذي ما زال يحتاجها الاستماع والجلب. + +```python title="watch.py" hl_lines="16 20" +--8<-- "docs_src/subscriptions/tutorial005.py" +``` + +تغلق الخوادم التدفّقات بسلاسة لأسبابها، ومنها إسقاط مشترك تراكمت أحداثه كثيرًا، لذا ليست النهاية السليمة إشارة للتوقف عن المراقبة. انتظر بتأخير تدريجي قبل إعادة الاستماع. + +لـ`SubscriptionLost` سبب محلي أيضًا. يحتفظ العميل بـ1024 حدثًا غير مستهلك كحد أقصى، ويفقد مستهلك يتأخر إلى ذلك الحد الاشتراك بدلًا من نمو الذاكرة دون حد. أبقِ جسم `async for` قصيرًا ونفّذ العمل البطيء في مكان آخر. + +تلتقط `keep_following` الاستثناء `SubscriptionLost` فقط. قد يثير الدخول إلى `listen()` أيضًا `MCPError` (فشل الاتصال أو عدم خدمة الخادم للطريقة)، أو `TimeoutError` (لم يصل الإقرار)، أو `ListenNotSupportedError` (اتصال أقدم من 2026). قرر أيها ينبغي للمراقب إعادة محاولته: الأخير لا يُصلَح بإعادة المحاولة. + +## مراجعة {#recap} + +* ادخل `async with client.listen(...)`؛ ينتظر الدخول الإقرار، فلا يفوت شيء يُنشر بعده. +* كرر باستخدام `async for event in sub`. الأحداث إشارات لإعادة الجلب، لا حمولات محتوى. +* افتح الاشتراك، ثم شغّل المراقب كمهمة، وتستمر استدعاءات الأدوات بجانبه. +* تنهي النهاية السليمة الحلقة؛ ويثير الانقطاع `SubscriptionLost`. في الحالتين: أعد الاستماع والجلب، مع تأخير تدريجي أولًا. +* الخروج من الكتلة هو إلغاء الاشتراك. + +نشر هذه الأحداث وتضييق المرشّح والتوسع إلى أكثر من عملية جانب الخادم: **[الاشتراكات](../handlers/subscriptions.md)**. تحافظ الأحداث نفسها أيضًا على صحة ذاكرة تخزين مؤقت لدى العميل، وتأتي **[التخزين المؤقت](caching.md)** بعدها. diff --git a/i18n/ar/pages/client/transports.md b/i18n/ar/pages/client/transports.md new file mode 100644 index 0000000000..73a60a317d --- /dev/null +++ b/i18n/ar/pages/client/transports.md @@ -0,0 +1,166 @@ +--- +translation: + sections: [9cac816674181eb0, 5619e950d206e6c8, 40b4916d82eaf1d4, 10d151f2cc75317f, 3d0832f39b0d7059, 92742ba36533633d, 991c10e47fda2636] + tool: 1 +--- +# وسائل نقل العميل {#client-transports} + +تتواصل كل `Client` مع خادمها عبر **وسيلة نقل**: ما يحمل الرسائل فعليًا. + +لا تضبط واحدة منفصلة. تأخذ `Client` وسيطة موضعية واحدة وتحدد وسيلة النقل من نوعها. + +جانب *الخادم* لكل وسيلة (ما تفعله `mcp.run()` وما تنشره) في **[تشغيل خادمك](../run/index.md)**. + +## Streamable HTTP {#streamable-http} + +مرّر سلسلة URL فتحصل على **Streamable HTTP**، وسيلة نقل النشر والخيار الأول: + +```python title="client.py" hl_lines="5" +--8<-- "docs_src/client_transports/tutorial002.py" +``` + +هذا عميل الإنتاج كاملًا. تغلّف `Client` عنوان URL في `streamable_http_client(...)` نيابة عنك، فوق `httpx2.AsyncClient` معدّة كما يحتاج MCP: مهلة 30 ثانية للاتصال والكتابة وانتظار اتصال متاح، و300 ثانية للقراءة لأن الخادم قد يُبقي تدفّق استجابة مفتوحًا. + +!!! check + `Client` التي أنشأتها **ليست** متصلة. الإنشاء يختار وسيلة النقل فقط؛ + وتفتحها `async with`. إذا استخدمت الاتصال قبل الدخول، تخبرك SDK بذلك: + + ```text + RuntimeError: Client must be used within an async context manager + ``` + + لم يُحَل عنوان أو يُجلَب شيء أو تُنشأ عملية عندما كتبت `Client("http://...")`. هذا السطر بلا تكلفة اتصال. + +### استخدم `httpx2.AsyncClient` الخاصة بك {#bring-your-own-httpx2asyncclient} + +عندما تحتاج إلى ترويسة `Authorization` أو ملف تعريف ارتباط أو وكيل أو mTLS أو مهلة مختلفة، ابنِ `httpx2.AsyncClient` بنفسك ومرّرها إلى `streamable_http_client`: + +```python title="client.py" hl_lines="8-13" +--8<-- "docs_src/client_transports/tutorial003.py" +``` + +لاحظ أمرين: + +* أنت تملك `httpx2.AsyncClient`، لذلك **أنت** من يدخل سياقها ويخرج منه. لا تغلق SDK عميلًا لم تنشئه. +* تعيد `streamable_http_client(url, http_client=...)` وسيلة نقل، وتقبلها `Client(transport)` مثل أي وسيلة أخرى. + +احتفظ بـ`timeout=`. إنها التي يستخدمها عميل SDK نفسه (30 ثانية و300 للقراءة)؛ وتحصل `httpx2.AsyncClient` دونها على مهلة `httpx2` الافتراضية، 5 ثوانٍ، فيفشل استدعاء أداة يتجاوزها بانتهاء مهلة القراءة. + +ملاحظة TLS: تتحقق `httpx2` من الشهادات مقابل مخزن الثقة لنظام التشغيل (عبر +[`truststore`](https://pypi.org/project/truststore/))، لا قائمة CA مضمّنة. في بيئة +دون مخزن CA نظام صالح (بعض الحاويات المصغّرة)، عيّن متغيرَي البيئة القياسيين `SSL_CERT_FILE`/`SSL_CERT_DIR` +أو مرّر `verify=ssl_context` صريحًا إلى `httpx2.AsyncClient` +(الخلفية في +[استبدال `httpx` و`httpx-sse` بـ`httpx2`](../migration.md#httpx-and-httpx-sse-replaced-by-httpx2)). + +### أحداث SSE الأكبر {#larger-sse-events} + +مرّر `max_sse_event_size` عندما يرسل الخادم نتيجة أداة أو إشعارًا كبيرًا في حدث SSE واحد: + +```python title="client.py" hl_lines="6-9" +--8<-- "docs_src/client_transports/tutorial005.py" +``` + +الافتراضي 1 MiB لكل حدث، ويُقاس بالبايتات قبل تحليل الحدث. ينطبق الحد على +استجابات POST وتدفّق GET والتدفّقات المستأنفة. يُفشل الحدث الأكبر من المسموح في استجابة POST أو تدفّق +مستأنف ذلك الطلب بخطأ SSE. على تدفّق GET الخلفي، يسجّل العميل +الخطأ ويعيد محاولة التدفّق. عيّن `max_sse_event_size=None` لتعطيل الحد عندما تثق +بالخادم وتحتاج إلى أحداث أكبر. لا تتأثر استجابات JSON. إذا استخدمت `ClientSessionGroup`، فعيّن +الخيار نفسه على `StreamableHttpParameters`. + +!!! warning + كانت `streamable_http_client` تقبل `headers=` و`timeout=` مباشرة. لم تعد تفعل: + مَعلماتها `url` و`http_client` و`terminate_on_close` و`max_sse_event_size`. إذا استخدمت `headers=` + بحكم العادة، فستحصل على: + + ```text + TypeError: streamable_http_client() got an unexpected keyword argument 'headers' + ``` + + توجد الترويسات والمصادقة والوكلاء والمهل على `httpx2.AsyncClient` التي تمرّرها. + أما `max_sse_event_size` فينطبق على قارئات SSE في وسيلة نقل MCP. + +!!! info + تحتفظ `httpx2` بواجهة `httpx` المألوفة، لذا إذا عرفت `httpx`، فأنت تعرف إعداد المصادقة + والوكلاء وخطافات الأحداث وإعادة المحاولة وحدود الاتصالات هنا. لا تضيف SDK شيئًا فوق ذلك ولا + تزيل شيئًا، عدا [معالجة التحويلات](#redirects). وهذا موضع دمج OAuth أيضًا: + `httpx2.AsyncClient(auth=OAuthClientProvider(...))`. التدفق كاملًا في **[عملاء OAuth](oauth-clients.md)**. + +### التحويلات {#redirects} + +تتصل وسيلة النقل بعنوان URL الذي أعطيتها وبذلك الأصل فقط. + +* يُتّبع تحويل `307`/`308` الذي يبقى على المخطط والمضيف والمنفذ نفسها، وكذلك `http://` → `https://` على المضيف نفسه. يشمل ذلك تحويل الشرطة النهائية المعتاد `/mcp` → `/mcp/`. +* **لا** يُتّبع تحويل إلى أي وجهة أخرى. يفشل الاستدعاء بالرسالة: + + ```text + MCPError: Redirect to https://other.example.com/mcp not followed; use that URL as the endpoint if it is the intended server + ``` + + إذا كان ذلك URL هو الخادم المقصود، فضعه في إعداداتك. وإلا فإعداد الخادم أو وكيل أمامه غير صحيح. + +ينطبق هذا على أي `httpx2.AsyncClient` تمرّرها: لا يُستشار إعداد `follow_redirects` لطلبات MCP في أي اتجاه. يطبّق مزوّدو OAuth في SDK القاعدة نفسها على طلباتهم. + +!!! tip + تعني `Redirect to http://… not followed: it would downgrade this HTTPS endpoint to plain HTTP` أن + الخادم وراء وكيل ينهي TLS لا يعرف عنه، ويصدر تحويلات `http://`. + يُصلَح ذلك على الخادم (**[النشر والتوسع](../run/deploy.md#behind-a-tls-terminating-proxy)**)، + أو باستخدام URL الدقيق `https://…/` الذي تقترحه الرسالة. + +## stdio {#stdio} + +خادم **stdio** عملية فرعية. يشغّله العميل ويكتب JSON-RPC إلى stdin ويقرأ JSON-RPC من stdout. هكذا يشغّل مضيف مكتبي خادمًا على جهازك: المضيف *هو* هذه الشيفرة مع واجهة، وتعرض **[الاتصال بتطبيق مضيف فعلي](../get-started/real-host.md)** العلاقة نفسها من جانب المضيف كملف إعدادات. + +صِف العملية باستخدام `StdioServerParameters` ومرّرها إلى `Client`: + +```python title="client.py" hl_lines="3-7 11" +--8<-- "docs_src/client_transports/tutorial004.py" +``` + +ينشئ دخول الكتلة العملية. ويغلق الخروج منها العملية الفرعية: يغلق stdin وينتظر ويقتلها إن استمرت. لا تنظّفها بنفسك. + +تذهب stderr للعملية الابنة إلى stderr لديك. لإرسالها إلى مكان آخر، ابنِ وسيلة النقل بنفسك باستخدام `stdio_client` (من `mcp`) ومرّرها بدلًا من ذلك: `Client(stdio_client(server, errlog=log_file))`. + +!!! warning + **لا** ترث العملية الابنة بيئتك. تحصل على قائمة سماح محدودة (`HOME` و`LOGNAME` و + `PATH` و`SHELL` و`TERM` و`USER` على POSIX)، كي لا تتسرب بيانات حساسة إلى عملية قد + لا تكون كتبتها. + + لن يجد خادم يحتاج إلى مفتاح API ذلك المفتاح فيها. مرّره صراحة باستخدام `env=`؛ تُدمَج هذه + المتغيرات فوق قائمة السماح. هذا ما يفعله `BOOKSHOP_API_KEY` أعلاه. + +## داخل الذاكرة {#in-memory} + +في الاختبار، لا شيء لتنشره أو تشغّله. مرّر كائن الخادم نفسه: + +```python hl_lines="14" +--8<-- "docs_src/client_transports/tutorial001.py" +``` + +لا عملية فرعية ولا منفذ ولا بايتات عبر الشبكة. العميل والخادم كائنان في العملية نفسها، وما زال الاستدعاء يمر بطبقة البروتوكول الفعلية: تُعرض `search_books` ويُتحقَّق منها وتُستدعى كما يحدث عبر HTTP تمامًا. تبني **[الاختبار](../get-started/testing.md)** النمط كاملًا حوله. + +يعمل الشكل نفسه أيضًا كواجهة تضمين: يستطيع تطبيق ينشئ الخادم بنفسه استدعاء أدواته دون انتقال عبر الشبكة. + +## SSE {#sse} + +`sse_client(url)` من `mcp.client.sse` هي وسيلة نقل HTTP التي حلّت محلها Streamable HTTP. غلّفها بالطريقة نفسها، `Client(sse_client("http://localhost:8000/sse"))`، للتواصل مع خادم ما زال يستخدمها، ولا تبنِ شيئًا جديدًا عليها. + +## بروتوكول `Transport` {#the-transport-protocol} + +كل ما سبق شيء واحد بالنسبة إلى `Client`. + +**وسيلة النقل** أي مدير سياق غير متزامن ينتج زوج تدفّقات رسائل `(read, write)`؛ رسميًا، بروتوكول `Transport` في `mcp.client`. تحدد `Client` وسيطتها بحسب النوع: تصبح `str` هي `streamable_http_client(url)`، وتصبح `StdioServerParameters` هي `stdio_client(params)`، ويتصل كائن الخادم داخل العملية، ويُدخَل سياق أي شيء آخر كوسيلة نقل مباشرة. هذه القاعدة الأخيرة سبب ملاءمة `stdio_client(...)` و`streamable_http_client(...)` و`sse_client(...)` كلها للموضع نفسه، وسبب قدرتك على كتابة وسيلتك الخاصة. + +## مراجعة {#recap} + +* تتصل `Client("http://.../mcp")` (عنوان URL) عبر Streamable HTTP، وسيلة نقل الإنتاج. +* توجد الترويسات والمصادقة والوكلاء والمهل على `httpx2.AsyncClient` تمرّرها إلى `streamable_http_client(url, http_client=...)`. لا خيار `headers=`. +* استخدم `streamable_http_client(url, max_sse_event_size=...)` لتغيير حد البايتات لكل حدث SSE. +* لا تُتّبع التحويلات إلا داخل أصل URL نفسه (`307`/`308` لإضافة الشرطة النهائية)، إضافة إلى `http`→`https` على المضيف نفسه. يفشل غير ذلك بـ`Redirect to … not followed`؛ اضبط URL النهائي. +* stdio هي `Client(StdioServerParameters(...))`. غلّفها في `stdio_client(...)` بنفسك فقط لإعادة توجيه stderr للعملية الابنة. +* تتلقى العملية الفرعية بيئة بقائمة سماح، لا بيئتك؛ وتضيف `env=` إليها. +* تتصل `Client(mcp)` (كائن الخادم) داخل الذاكرة. استخدمها في الاختبارات أو لتضمين خادم في التطبيق الذي بناه. +* وسيلة النقل أي شيء تستخدمه بصيغة `async with x as (read, write)`. تسلّم `Client` كل ما ليس كائن خادم أو URL أو `StdioServerParameters` إلى ذلك البروتوكول مباشرة. +* إنشاء `Client` يختار وسيلة النقل. وتفتحها `async with`. + +بعد فتح وسيلة النقل، يجب أن يتفق الطرفان على إصدار البروتوكول. غالبًا لا تفكر في ذلك؛ وعندما تحتاج إليه، راجع **[إصدارات البروتوكول](../protocol-versions.md)**. diff --git a/i18n/ar/pages/deprecated.md b/i18n/ar/pages/deprecated.md new file mode 100644 index 0000000000..49ace689f3 --- /dev/null +++ b/i18n/ar/pages/deprecated.md @@ -0,0 +1,157 @@ +--- +translation: + sections: [4c1dea378b2b1bf7, 01262a123ad9501d, 429db5b574a2ac08, e2d0d273fbd2d74b, 64ab0331e868f3d4, 6c8878ce2d1f6d56, c3d1099701156881, e185a1b8e53669f6] + tool: 1 +--- +# الميزات المهملة {#deprecated-features} + +تستغني مواصفة 2026-07-28 عن خمس ميزات. ما زالت SDK تنفّذها كلها، وأصبحت كل واحدة تحمل **تحذير إهمال**. وهناك أيضًا حالات إهمال خاصة بـSDK، مُدرجة [في النهاية](#deprecated-sdk-helpers). + +يسمّي الجدول أدناه كل ميزة مهملة، وسبب الاستغناء عنها، والبديل الذي تبني عليه. + +## ما المهمل {#what-is-deprecated} + +| الميزة المهملة | السبب | ما تفعله بدلًا منها | +|---|---|---| +| **المجلدات الجذرية**: `ctx.session.list_roots()` و`client.send_roots_list_changed()` و`list_roots_callback=` التي تمرّرها إلى `Client(...)` | يستغني [SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577) عن هذه القدرة. | خذ المسارات كوسيطات أدوات عادية أو عناوين URI للموارد، أو ضمّن `ListRootsRequest` في `InputRequiredResult` (راجع **[الطلبات متعددة جولات الطلب والرد](handlers/multi-round-trip.md)**). | +| **أخذ العينات الذي يبدأه الخادم**: `ctx.session.create_message()` و`sampling_callback=` التي تمرّرها إلى `Client(...)` | يستغني SEP-2577 عن هذه القدرة. | أعِد `InputRequiredResult` ودع العميل يعيد الاستدعاء (راجع **[الطلبات متعددة جولات الطلب والرد](handlers/multi-round-trip.md)**). | +| **التسجيل عبر البروتوكول**: `ctx.log()` و`ctx.debug()` و`ctx.info()` و`ctx.warning()` و`ctx.error()` و`ctx.session.send_log_message()` و`client.set_logging_level()` | يستغني SEP-2577 عن هذه القدرة. لا يوجد بديل لها داخل البروتوكول. | استخدم `import logging` العادي إلى stderr (راجع **[التسجيل](handlers/logging.md)**). | +| **`ping`**: `client.send_ping()` | **أُزيلت** من البروتوكول، ولم تُهمل فقط. لا توجد طريقة `ping` في 2026-07-28. | لا شيء. تعمل فقط في اتصال `mode="legacy"`. | +| **التقدم من العميل إلى الخادم**: `client.send_progress_notification()` | يجعل 2026-07-28 التقدم من الخادم إلى العميل فقط. | لا شيء لإرساله. يبلغ *خادمك* عن التقدم باستخدام `ctx.report_progress()` (راجع **[التقدم](handlers/progress.md)**). | + +تنتج ثلاث نقاط من هذا الجدول: + +* المجلدات الجذرية وأخذ العينات والتسجيل مترابطة. يهمل اقتراح واحد، **SEP-2577**، القدرات الثلاث معًا. +* يشترك أخذ العينات والمجلدات الجذرية في مشكلة أعمق: كلاهما موضع يرسل فيه **الخادم** **طلبًا** إلى **العميل**. يستبدل 2026-07-28 هذا الاتجاه كله بـ**[الطلبات متعددة جولات الطلب والرد](handlers/multi-round-trip.md)**. ما اختفى هو طرائق RPC المستقلة (`sampling/createMessage` و`roots/list` و`elicitation/create` بالدفع)؛ أما أنواع الحمولة `CreateMessageRequest` / `ListRootsRequest` / `ElicitRequest` فتبقى، مضمنة في `InputRequiredResult.input_requests`، وتصل إلى دوال رد النداء نفسها على جانب العميل. +* تختلف `ping` عن البقية. لا تهملها المواصفة، بل تزيلها. ما زالت طريقة SDK تحذّر (تقول رسالتها *أُزيلت* لا *أُهملت*)، ويرد استدعاؤها في اتصال حديث بـ*«Method not found»*. + +## الإهمال إرشادي {#deprecated-is-advisory} + +لا يتعطل شيء حاليًا. + +تظل كل طريقة أعلاه تعمل في أي جلسة تفاوضت على **2025-11-25 أو أقدم**. ثبّت `mode="legacy"` في العميل لتحصل على سلوك ما قبل 2026 نفسه تمامًا. لا تغييرات في البيانات المنقولة، ولا في التفاوض على القدرات. + +ما يتغير هو ظهور تحذير واضح أول مرة تعمل فيها كل طريقة: + +```text +MCPDeprecationWarning: The logging capability is deprecated as of 2026-07-28 (SEP-2577). +``` + +يشتق `MCPDeprecationWarning` من `UserWarning`، **وليس** `DeprecationWarning`. هذا مقصود: لا يعرض مرشح Python الافتراضي `DeprecationWarning` إلا في الشيفرة المشغّلة مباشرة باسم `__main__`، ولذلك قد تهمل المكتبات شيئًا دون أن يلاحظ أحد لمدة سنتين. يظهر هذا التحذير في كل مكان، دون خيار `-W`. + +!!! warning + يتوقف معنى «إرشادي» عند النقل. أخذ العينات والمجلدات الجذرية *طلبات* من الخادم إلى العميل، ولا توجد + في جلسة 2026-07-28 قناة لحملها. استدعِ `ctx.session.create_message()` + داخل أداة في اتصال حديث، وسيظهر التحذير، ثم يفشل + الإرسال بخطأ: + + ```text + Cannot send 'sampling/createMessage': this transport context has no back-channel + for server-initiated requests. + ``` + + إشارتان بهذا الترتيب. يظهر `MCPDeprecationWarning` بمجرد استدعاء + الطريقة، في أي اتصال. ثم يعود الخطأ عندما تحاول SDK + الإرسال. لا تعمل الميزتان من طرف إلى طرف إلا في اتصال `mode="legacy"` سجّل عميله + دالة رد النداء المناسبة. + +## `ping` في جلسة قديمة {#ping-on-a-legacy-session} + +**ping** طلب فارغ يستطيع أي طرف إرساله للتحقق من أن الآخر ما زال يجيب. تزيله مواصفة 2026-07-28 ([SEP-2575](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2575)): يثبت كل طلب يرسله عميل حديث بالفعل أن الخادم موجود، ولا يملك الخادم الحديث قناة لإرسال طلب كهذا. تظل طريقتا SDK تعملان في جلسة من جيل المصافحة. من العميل: + +```python +async def main() -> None: + async with Client("http://localhost:8000/mcp", mode="legacy") as client: + await client.send_ping() # warns; returns an EmptyResult +``` + +ومن الخادم، داخل أي دالة معالجة: + +```python +@mcp.tool() +async def check_client(ctx: Context) -> str: + """A tool that still pings the client mid-call.""" + await ctx.session.send_ping() # no warning; an EmptyResult while the client is connected + return "client answered" +``` + +* تحذّر `client.send_ping()` باستخدام `MCPDeprecationWarning` في كل استدعاء. وفي اتصال افتراضي (`2026-07-28`)، يجيب الخادم بـ`MCPError: Method not found` بدلًا من ذلك. +* لا تحمل `ctx.session.send_ping()` تحذيرًا. في اتصال حديث، ترفع خطأ غياب القناة العكسية نفسه مثل أي طلب آخر يبدأه الخادم. +* لا يسجّل أي طرف شيئًا للإجابة عن ping. + +## إشعارات تغير المجلدات الجذرية {#roots-change-notifications} + +يستطيع عميل من جيل 2025 أعلن قدرة المجلدات الجذرية إخبار الخادم بتغير مجلدات مساحة عمله عبر إرسال `notifications/roots/list_changed`؛ فيطلب الخادم `roots/list` مجددًا. تزيل مواصفة 2026-07-28 الإشعار مع بقية تدفّق المجلدات الجذرية بالدفع. في العميل، تعلن `list_roots_callback=` (**[دوال رد نداء العميل](client/callbacks.md)**) القيمة `"roots": {"listChanged": true}`، ويفي استدعاء واحد بهذا الوعد: + +```python +async def open_folder(client: Client, uri: str, name: str) -> None: + """The user opened another folder: expose it through the roots callback, then tell the server.""" + workspace.append(Root(uri=FileUrl(uri), name=name)) + await client.send_roots_list_changed() +``` + +في الخادم، يأخذ `Server` منخفض المستوى دالة معالجة الاستقبال: + +```python +async def roots_changed(ctx: ServerRequestContext, params: NotificationParams | None) -> None: + """The client's roots changed: ask for the new list.""" + roots = (await ctx.session.list_roots()).roots + + +server = Server("Bookshop", on_roots_list_changed=roots_changed) +``` + +* `workspace` هي القائمة التي تعيدها `list_roots_callback`. تحذّر `client.send_roots_list_changed()`، وتحتاج إلى عميل `mode="legacy"`: في اتصال حديث، يُسقط الإشعار بصمت. أبقِ الجلسة مفتوحة بعد ذلك، لأن طلب الخادم اللاحق `roots/list` يصل عبرها. +* لا تتضمن `MCPServer` دالة لمعالجة الإشعار. في `Server` منخفض المستوى، تسجّل `on_roots_list_changed=` الدالة (وهي مهملة أيضًا وتحذّر عند الإنشاء). لا يحمل الإشعار حمولة، ولذلك تستدعي الدالة `ctx.session.list_roots()` للحصول على القائمة الجديدة. + +## إسكات التحذير {#silencing-the-warning} + +لا تفعل ذلك في الشيفرة الجديدة. + +لكن الخادم الذي تصونه ويخدم فعلًا عملاء ما قبل 2026 يحق له سجل هادئ. رشّح الفئة قبل أول استدعاء مهمل: + +```python +import warnings + +from mcp import MCPDeprecationWarning + +warnings.filterwarnings("ignore", category=MCPDeprecationWarning) +``` + +هذه واجهة API كاملةً. لا يوجد خيار لكل طريقة، ولا تحتاج إليه: فالغرض من الفئة الواحدة أن يسكتها سطر واحد ويعيدها سطر واحد. + +!!! check + استخدم المرشح في الاتجاه الآخر لتحصل على اختبار انحدار مجاني. أضف + `"error::mcp.MCPDeprecationWarning"` إلى إعداد `filterwarnings` في + إعدادات pytest، وسيؤدي الاستدعاء المهمل إلى **رفع استثناء** بدلًا من التحذير. تتوقف أداة اسمها + `old_log` ما زالت تستدعي `ctx.info()` عن النجاح: يعود الاستدعاء بـ`is_error=True` مع + `Error executing tool old_log`، ويسمّي سجل الخادم الملتقط السبب: + + ```text + mcp.shared.exceptions.MCPDeprecationWarning: The logging capability is deprecated as of 2026-07-28 (SEP-2577). + ``` + + سطر واحد من إعداد pytest، ولن يستطيع استدعاء مهمل العودة إلى + شيفرتك دون إخفاق اختبار. + +## الدوال المساعدة المهملة في SDK {#deprecated-sdk-helpers} + +ليست هذه تغييرات في المواصفة، وإنما استخدامات SDK لها بدائل أفضل. تحذّر باستخدام `MCPDeprecationWarning` نفسه، ويزيل 3.0 الشكل القديم. + +| الاستخدام المهمل | ما تفعله بدلًا منه | +|---|---| +| `FuncMetadata.call_fn_with_arg_validation()` | استخدم `FuncMetadata.validate_arguments()` ثم `FuncMetadata.call_fn()`. لم يستدعها إلا من يدير `FuncMetadata` مباشرة (مثل صنف `Tool` مخصص). | +| `AuthSettings(resource_server_url=...)` دون `validate_token_resource=` | عيّنها: تجعل `True` الخادم يرفض رموز الحامل التي لا يؤكد متحققك إصدارها لـ`resource_server_url`، وتعني `False` أن متحققك يفحص جمهور الرمز بنفسه (راجع **[التفويض](run/authorization.md#a-token-verifier)**). تتصرف القيمة غير المعيّنة مثل `False`؛ ويجعل 3.0 قيمة `True` افتراضية عندما تكون `resource_server_url` معيّنة. | +| `ClientCredentialsOAuthProvider(...)` أو `PrivateKeyJWTOAuthProvider(...)` دون `issuer=` | مرّر `issuer=` باسم خادم التفويض الذي أصدر بيانات الاعتماد (راجع **[كتابة عملاء OAuth](client/oauth-clients.md#machine-to-machine)**). دونها، يقرر خادم MCP خادم التفويض الذي يتلقاها؛ ويجعل 3.0 الوسيطة المسماة مطلوبة. | + +## مراجعة {#recap} + +* تهمل مواصفة 2026-07-28 **المجلدات الجذرية** و**أخذ العينات** الذي يبدأه الخادم و**التسجيل** عبر البروتوكول (كلها في [SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577))، وتقصر **التقدم** على الخادم إلى العميل، وتزيل **`ping`**. +* يرشدك عمود البدائل إلى **[الطلبات متعددة جولات الطلب والرد](handlers/multi-round-trip.md)** لأخذ العينات والمجلدات الجذرية، و**[التسجيل](handlers/logging.md)** للتسجيل، و**[التقدم](handlers/progress.md)** للتقدم. لا تحتاج `ping` إلى أي بديل. +* الإهمال إرشادي: لا تغييرات في البيانات المنقولة، وتظل الميزات تعمل في جلسات ما قبل 2026، ويظهر `MCPDeprecationWarning` واضح (من نوع `UserWarning`، ولذلك يُعرض افتراضيًا). +* يحتاج أخذ العينات والمجلدات الجذرية أيضًا إلى قناة عكسية لا تملكها جلسة 2026-07-28. في اتصال حديث، يحذّران ثم يثيران استثناءً. +* تسكت `warnings.filterwarnings("ignore", category=MCPDeprecationWarning)` الفئة كاملةً؛ وتحولها `"error::mcp.MCPDeprecationWarning"` في pytest إلى إخفاق اختبار. +* تتبع [حالات الإهمال في SDK](#deprecated-sdk-helpers) القاعدة نفسها: تحذّر الآن، ويحذف 3.0 الشكل القديم. +* ينبغي ألا تُبنى الشيفرة الجديدة على أي من هذه الميزات. + +تعلّمك كل صفحة أخرى في هذا التوثيق واجهة API الحالية. diff --git a/i18n/ar/pages/get-started/first-steps.md b/i18n/ar/pages/get-started/first-steps.md new file mode 100644 index 0000000000..84a2e1e67a --- /dev/null +++ b/i18n/ar/pages/get-started/first-steps.md @@ -0,0 +1,143 @@ +--- +translation: + sections: [0d6c05bcbf836bf3, 9a78b5f6b44b18ab, 7114d8d6daba203f, e8bbb56a98ba7bc9, bfd2fd1153e71dac, 1615a994ef071fdd, 65c599fae991f245] + tool: 1 +--- +# الخطوات الأولى {#first-steps} + +تنتقل **[الصفحة الرئيسية](../index.md)** بسرعة: اكتب خادمًا، وشغّله، واستدعِ أداة. + +تشرح هذه الصفحة الأمور بتأنٍّ، مع الأنواع الثلاثة التي يمكن أن يتيحها الخادم، وتسمية كل مفهوم أثناء الشرح. + +## التطبيق المضيف والعميل والخادم {#host-client-and-server} + +سترى هذه الكلمات الثلاث في كل صفحة من الآن فصاعدًا: + +* **التطبيق المضيف** هو تطبيق LLM: مثل Claude أو IDE أو بيئة تشغيل وكيل. وهو ما يتفاعل معه المستخدم. +* **العميل** موجود داخل التطبيق المضيف ويتواصل ببروتوكول MCP. يشغّل المضيف عميلًا لكل خادم يتصل به. +* **الخادم** هو ما تبنيه باستخدام هذه SDK. يتيح إمكانات للعملاء، ولا يتواصل مع النموذج مباشرة أبدًا. + +أنت تكتب الخادم. أما التطبيقات المضيفة فهي منتجات الآخرين. وتوفر لك SDK أيضًا `Client`، وهي الفئة نفسها التي يستخدمها المضيف للوصول إلى خادم عبر URL أو تشغيله كعملية فرعية. ستراها لاحقًا في هذه الصفحة، وهي أيضًا وسيلتك لاختبار خوادمك. + +## العناصر الأساسية الثلاثة {#the-three-primitives} + +يتيح الخادم ثلاثة أنواع بالضبط. وما يميزها هو **من يقرر استخدامها**: + +| العنصر الأساسي | جهة التحكم | ماهيته | مثال | +|---------------|-----------------|-----------------------------------------------------|------------------------------------| +| **الأدوات** | النموذج | دالة يستدعيها النموذج لتنفيذ إجراء | استدعاء API، كتابة في قاعدة بيانات | +| **الموارد** | التطبيق | بيانات يحمّلها المضيف في سياق النموذج | محتوى ملف، استجابة API | +| **قوالب التوجيه** | المستخدم | قالب رسائل قابل لإعادة الاستخدام يستدعيه المستخدم بالاسم | أمر يبدأ بشرطة مائلة، عنصر قائمة | + +جهة التحكم هي أساس هذا التقسيم. تعمل الأداة لأن **النموذج** قرر استدعاءها. ويُرفق المورد لأن **التطبيق** قرر أن النموذج يحتاج إليه. ويعمل قالب التوجيه لأن **المستخدم** اختاره. + +!!! info + إذا سبق أن بنيت API للويب، فأنت تعرف معظم الفكرة: **المورد** مثل `GET` + (يحمّل بيانات دون تغيير شيء)، و**الأداة** مثل `POST` (تنفّذ عملًا وقد تكون لها + آثار جانبية). أما **قالب التوجيه** (prompt) فلا نظير له في HTTP؛ وهو أقرب إلى استعلام محفوظ يشغّله المستخدم + بالاسم. + +## خادم واحد يتيح العناصر الثلاثة {#one-server-all-three} + +```python title="server.py" hl_lines="6 12 18" +--8<-- "docs_src/first_steps/tutorial001.py" +``` + +ثلاث دوال عادية وثلاثة مزخرفات. كل مزخرف يتولى التسجيل بالكامل: + +* يجعل `@mcp.tool()` الدالة `add` **أداة**. +* يجعل `@mcp.resource("greeting://{name}")` الدالة `greeting` **قالب مورد**: فـ`{name}` في URI هي مَعلمة الدالة. +* يجعل `@mcp.prompt()` الدالة `summarize` **قالب توجيه**. وتصبح السلسلة النصية التي تعيدها رسالة مستخدم. + +تقرأ SDK كل ما عدا ذلك (الاسم والوصف ومخطط الوسائط) من الدالة نفسها: اسمها وسلسلة توثيقها وتلميحات أنواعها. لم تعلن أيًّا منها بصورة منفصلة. + +!!! tip + لنصفي SDK مساران للاستيراد: `from mcp import Client` و + `from mcp.server import MCPServer`. لا يوجد `from mcp import MCPServer`. + +### جرّبه {#try-it} + +شغّله باستخدام MCP Inspector: + +```console +uv run mcp dev server.py +``` + +افتح عنوان URL الذي يطبعه. يخصص Inspector تبويبًا لكل عنصر أساسي؛ استكشفها بالترتيب. + +**Tools.** عنصر واحد: `add`، ووصفه *جمع عددين.* يتضمن النموذج حقل عدد صحيح مطلوبًا لـ`a` وآخر لـ`b`. املأهما واستدعِ الأداة، وستكون النتيجة `3`. أنشأ Inspector النموذج من `a: int, b: int`. وكذلك يفعل كل عميل آخر. + +**Resources.** قائمة *Resources* فارغة. توجد `greeting` ضمن **Resource Templates** لأن `greeting://{name}` يحتوي على مَعلمة: لا يوجد مورد محدد لعرضه حتى يقدّم أحدهم `name`. مرّر `World` واقرأه: + +```text +Hello, World! +``` + +**Prompts.** عنصر واحد: `summarize`، وله وسيطة مطلوبة واحدة هي `text`. اطلبه مع نص، وستتلقى رسالة واحدة فيها `role: user` وتكون السلسلة النصية الناتجة محتواها. هذا كل ما يفعله قالب التوجيه: دالة تبني رسائل. + +شغّل Inspector خادمك عبر **stdio**، إحدى وسائل النقل التي يدعمها خادم MCP. لا تختار وسيلة بعد؛ تشرح ذلك صفحة **[تشغيل خادمك](../run/index.md)**. + +## القدرات {#capabilities} + +رأيت ثلاثة تبويبات في Inspector. كيف عرف أن هناك ثلاثة؟ + +عندما يتصل عميل، يعلن الخادم **قدراته**: مجموعات الطلبات التي سيجيب عنها. يستخدم العميل هذا الإعلان ليقرر ما يمكنه طلبه أصلًا. لم تكتبه بنفسك؛ تعلنه `MCPServer` نيابة عنك. + +تحقّق بنفسك. اترك `server.py` يعمل عبر HTTP في نافذة طرفية: + +```console +uv run mcp run server.py --transport streamable-http +``` + +ووجّه إليه عميلًا من نافذة أخرى: + +```python title="client.py" hl_lines="7-8" +--8<-- "docs_src/first_steps/tutorial001_client.py" +``` + +```console +python client.py +``` + +```text +{'prompts': {'list_changed': True}, 'resources': {'subscribe': True, 'list_changed': True}, 'tools': {'list_changed': True}} +``` + +ذلك القاموس هو **القدرات** التي أعلن عنها خادمك. وهو أول ما يعرفه كل عميل يتصل به: + +| القدرة | ما يستطيع العميل استدعاءه الآن | +|-------------|------------------------------------------------------------| +| `tools` | `tools/list`، `tools/call` | +| `resources` | `resources/list`، `resources/templates/list`، `resources/read` | +| `prompts` | `prompts/list`، `prompts/get` | + +تتيح `MCPServer` العناصر الأساسية الثلاثة، لذا تُعلَن الثلاثة دائمًا. + +لاحظ ما لا يظهر هنا. تحتاج `completions` (الإكمال التلقائي لوسائط قوالب الموارد والتوجيه) إلى دالة معالجة تكتبها. ولا يملك هذا الخادم واحدة، لذلك تغيب القدرة ولن يطلبها عميل ملتزم بالبروتوكول. وهذه قاعدة كل ما هو اختياري: سجّل الإمكانية فتظهر القدرة؛ وتوضح **[الإكمالات](../servers/completions.md)** ذلك. + +!!! info + ملف `client.py` هذا عميل MCP كامل، وصفحته هي **[العميل](../client/index.md)**. + في الاختبار، تتجاوز الطرفية والمنفذ وتمرّر إلى `Client` كائن الخادم نفسه، + أي `Client(mcp)`. ولهذا أيضًا صفحة كاملة: **[الاختبار](testing.md)**. + +## ما لم تكتبه {#what-you-did-not-write} + +راجع هذه الصفحة. كتبت ثلاث دوال Python صغيرة. **لم** تكتب: + +* JSON Schema. فتلميح النوع `a: int, b: int` *هو* مخطط `add`. +* دالة معالجة طلبات. تُعالَج `tools/list` و`resources/read` و`prompts/get` نيابة عنك. +* إعلانًا للقدرات. أنشأته `MCPServer` نيابة عنك. +* سطرًا لمعالجة البروتوكول. تفاوض الإصدارات وتأطير JSON-RPC وتبادل القدرات: جرى كل ذلك داخل `mcp dev` و`client.py`، ولم تره أصلًا. + +هذه النسبة بين ما تكتبه وما يُنجَز هي الغاية من SDK. + +## مراجعة {#recap} + +* **التطبيق المضيف** هو تطبيق LLM، و**العميل** مكوّنه الذي يتواصل ببروتوكول MCP، و**الخادم** ما تبنيه أنت. +* يتحكم **النموذج** في الأدوات، و**التطبيق** في الموارد، و**المستخدم** في قوالب التوجيه. +* مزخرف واحد لكل عنصر أساسي: `@mcp.tool()` و`@mcp.resource(uri)` و`@mcp.prompt()`. ويأتي الاسم والوصف والمخطط من الدالة. +* وجود `{param}` في URI يجعل المورد **قالبًا**، يُعرض منفصلًا عن الموارد المحددة. +* تُعلَن **قدرات** الخادم نيابة عنك، ولا يطلب العميل إلا ما يعلن الخادم دعمه. +* يتواصل `Client("http://localhost:8000/mcp")` مع خادمك الجاري تشغيله. مرّر إليه كائن الخادم بدلًا من ذلك، أي `Client(mcp)`، ليصبح أداة اختبارك منذ اليوم الأول. + +التالي هو **[الاتصال بتطبيق مضيف فعلي](real-host.md)**: تشغيل هذا الخادم داخل Claude Desktop أو IDE فعلًا. ثم **[الاختبار](testing.md)**: صفحة واحدة وعميل واحد داخل الذاكرة، دون تخمين ما إذا كان يعمل. وبعد ذلك تحصل كل إمكانية أساسية على صفحتها، بدءًا بما يتحكم فيه النموذج: **[الأدوات](../servers/tools.md)**. diff --git a/i18n/ar/pages/get-started/index.md b/i18n/ar/pages/get-started/index.md new file mode 100644 index 0000000000..f9f7b3352b --- /dev/null +++ b/i18n/ar/pages/get-started/index.md @@ -0,0 +1,57 @@ +--- +translation: + sections: [ed4a756b4c53c585, 97e2fb315b7fe398, 4d04f1c6f4bf6c1d, 577d73078fc62baf] + tool: 1 +--- +# ابدأ هنا {#get-started} + +هل تبدأ مع MCP أو مع هذه SDK؟ ابدأ هنا. تنقلك هذه الصفحات من الصفر إلى +خادم يعمل وتُختبر صحته: [ثبّت SDK](installation.md)، وابنِ +[خادمك الأول](first-steps.md)، و[اربطه بتطبيق مضيف فعلي](real-host.md)، ثم +[اختبره](testing.md) باستخدام عميل داخل الذاكرة. + +## شغّل الشيفرة {#run-the-code} + +يمكنك نسخ جميع كتل الشيفرة واستخدامها مباشرة: فهي ملفات كاملة تعمل كما هي. + +للتطبيق مع الشرح، الصق كتلة في ملف `server.py` وافتحه في MCP Inspector: + +```console +uv run mcp dev server.py +``` + +**نوصي بشدة** بأن تكتب الشيفرة (أو تنسخها)، وتعدّلها، وتشغّلها محليًا. فاستخدامها في محررك هو ما يوضح الفكرة فعلًا: قلة الشيفرة التي تكتبها، والإكمال التلقائي، وفحوص الأنواع التي تكتشف الأخطاء قبل تشغيل أي شيء. + +## لن تضطر إلى التخمين {#you-will-not-be-guessing} + +كل مثال في هذا التوثيق ملف كامل ضمن [`docs_src/`](https://github.com/modelcontextprotocol/python-sdk/tree/main/docs_src) في مستودع SDK نفسه، وتختبر مجموعة اختبارات SDK كل مثال باستخدام **عميل داخل الذاكرة**: + +```python +import pytest +from mcp import Client + +from server import mcp + + +@pytest.mark.anyio +async def test_add() -> None: + async with Client(mcp) as client: + result = await client.call_tool("add", {"a": 1, "b": 2}) + assert result.structured_content == {"result": 3} +``` + +لا عملية فرعية، ولا منفذ، ولا وسيلة نقل. يتصل `Client(mcp)` بكائن الخادم مباشرة. + +إذا عطّل تغيير في SDK مثالًا في إحدى هذه الصفحات، تفشل فحوص CI قبل أن تتعطل الصفحة. الشيفرة التي تقرؤها هنا هي الشيفرة التي تعمل. + +ستستخدم ذلك بنفسك في [الاختبار](testing.md)؛ وهي أيضًا طريقة اختبار خوادمك. + +## إلى أين تتجه بعد ذلك؟ {#where-to-go-next} + +بعد تشغيل خادم، تصبح بقية هذه الصفحات مرجعًا، وليست دورة تعليمية. +كل صفحة مستقلة، لذا انتقل مباشرة إلى ما تحتاج إليه: + +* ما يتيحه الخادم (الأدوات والموارد وقوالب التوجيه) في **[الخوادم](../servers/index.md)**. +* ما يتاح داخل الدوال التي تسجّلها في **[داخل دالة المعالجة](../handlers/index.md)**. +* إتاحة الخادم للعملاء (stdio وHTTP وتطبيق FastAPI الموجود لديك) في **[تشغيل خادمك](../run/index.md)**. +* بناء الطرف الآخر، أي تطبيق *يستخدم* خوادم MCP، في **[العملاء](../client/index.md)**. diff --git a/i18n/ar/pages/get-started/installation.md b/i18n/ar/pages/get-started/installation.md new file mode 100644 index 0000000000..0a4e1ef4ab --- /dev/null +++ b/i18n/ar/pages/get-started/installation.md @@ -0,0 +1,47 @@ +--- +translation: + sections: [6e2f9bab94d5ed36, 8cf653388f69e28b, 6fd9ea2f65de0df6] + tool: 1 +--- +# التثبيت {#installation} + +تتوفر Python SDK على PyPI باسم [`mcp`](https://pypi.org/project/mcp/). وتتطلب **Python 3.10+**. + +يصف هذا التوثيق **v2**، سلسلة الإصدارات المستقرة الحالية: + +=== "uv" + + ```bash + uv add "mcp[cli]" + ``` + +=== "pip" + + ```bash + pip install "mcp[cli]" + ``` + +!!! note "هل تنتقل من v1؟" + v2 إصدار رئيسي يتضمن تغييرات غير متوافقة مع الإصدارات السابقة؛ ويغطي **[دليل الترحيل](../migration.md)** + كلًّا منها. إذا كانت *حزمتك* تعتمد على `mcp` ولم تكن جاهزة للترحيل، فاحتفظ + بالحد الأعلى `<2` (مثل `mcp>=1.28,<2`) كي يبقى حل الاعتماديات غير المثبّت على إصدار محدد ضمن سلسلة 1.x. + +## ما الذي يُثبَّت؟ {#what-gets-installed} + +لا تحتاج إلى معرفة هذه التفاصيل لاستخدام SDK، لكن إن كنت تتساءل عن دور كل اعتمادية: + +* `mcp-types`: جميع أنواع البروتوكول (الطلبات والنتائج وكتل المحتوى) في حزمة مستقلة تتزامن إصداراتها مع SDK. تستوردها الشيفرة التي تعتمد على `mcp` عبر الاسم البديل `mcp.types` (كل `from mcp.types import ...` في هذا التوثيق)؛ ولا تستورد `mcp_types` مباشرة إلا في مشروع يثبّت `mcp-types` دون SDK. +* [`anyio`](https://anyio.readthedocs.io/): بيئة التنفيذ غير المتزامن. كُتبت SDK بالكامل باستخدام anyio، لذا تعمل على `asyncio` أو `trio`. +* [`pydantic`](https://docs.pydantic.dev/): الأساس الذي تُبنى عليه جميع نماذج `mcp.types`، وكذلك توليد المخططات والتحقق منها. +* [`httpx2`](https://pypi.org/project/httpx2/): عميل HTTP الذي تعتمد عليه وسائل نقل *العميل* Streamable HTTP وSSE، مع دعم مدمج للأحداث المرسلة من الخادم. +* [`starlette`](https://www.starlette.io/) و[`uvicorn`](https://www.uvicorn.org/) و[`sse-starlette`](https://pypi.org/project/sse-starlette/) و[`python-multipart`](https://pypi.org/project/python-multipart/): وسائل نقل *الخادم* عبر HTTP. +* [`jsonschema`](https://pypi.org/project/jsonschema/): يتحقق من توافق مخرجات الأداة المنظّمة مع مخطط المخرجات الذي أعلنت عنه. +* [`pyjwt[crypto]`](https://pyjwt.readthedocs.io/): معالجة رموز OAuth للتفويض. +* [`opentelemetry-api`](https://opentelemetry-python.readthedocs.io/): API خفيفة فقط، لذا لا تفرض البرمجيات الوسيطة للتتبّع في SDK أي تكلفة ما لم تثبّت بنفسك OpenTelemetry SDK ومكوّن تصدير. +* [`typing-extensions`](https://typing-extensions.readthedocs.io/) و[`typing-inspection`](https://pypi.org/project/typing-inspection/): ميزات الأنواع الحديثة في Python 3.10. +* [`pywin32`](https://pypi.org/project/pywin32/): خاصة بـWindows، وتُستخدم لإدارة العمليات الفرعية في `stdio`. + +## الإضافات الاختيارية {#optional-extras} + +* تضيف `mcp[cli]` كلًّا من [`typer`](https://typer.tiangolo.com/) و[`python-dotenv`](https://pypi.org/project/python-dotenv/) لأداة سطر الأوامر `mcp` (`mcp dev` و`mcp run` و`mcp install`). ستحتاج إليها أثناء التطوير؛ وقد لا تحتاج إليها في خادم منشور. +* تضيف `mcp[rich]` مكتبة [`rich`](https://rich.readthedocs.io/) لتحسين عرض سجلات الخادم. diff --git a/i18n/ar/pages/get-started/real-host.md b/i18n/ar/pages/get-started/real-host.md new file mode 100644 index 0000000000..3315e1b181 --- /dev/null +++ b/i18n/ar/pages/get-started/real-host.md @@ -0,0 +1,182 @@ +--- +translation: + sections: [3c4f2f06b4e978b6, 51ea5fbcb0e93563, 32d8808606ffdae0, 2eb57992049671d9, 1ba83e9af37cc1b4, 4822586344b08d9e, 1c93afef72478992, b6b448f9eddd51dc, fe55370fd931815b] + tool: 1 +--- +# الاتصال بتطبيق مضيف فعلي {#connect-to-a-real-host} + +**التطبيق المضيف** هو التطبيق الذي سيعمل خادمك ضمنه: Claude Desktop أو Claude Code أو IDE. وهو ما يتفاعل معه المستخدم. داخله، يشغّل **عميل** MCP خادمك كعملية ابنة ويتواصل معه عبر stdin وstdout لتلك العملية. + +أي إن الاتصال بمضيف خطوة واحدة: تعطيه **الأمر الذي يشغّل خادمك**. كل ما في هذه الصفحة (أمران في CLI وثلاثة ملفات JSON) أماكن مختلفة لوضع الأمر نفسه. + +## خادم واحد لجميع التطبيقات المضيفة {#one-server-every-host} + +```python title="server.py" hl_lines="4 34-35" +--8<-- "docs_src/real_host/tutorial001.py" +``` + +أداتان ومورد في ملف واحد. تهم ثلاث خصائص لهذا الملف جميع المضيفين أدناه: + +* يبدأ `mcp.run()` دون وسائط خادم **stdio**: يحجب التنفيذ، ويقرأ رسائل البروتوكول من stdin، ويكتبها إلى stdout. هذه وسيلة النقل التي يدعمها كل مضيف في هذه الصفحة. يشغّل المضيف ملفك كعملية ابنة ويتحكم في هاتين القناتين، لذلك لا يتطلب الاتصال سوى تقديم الأمر. لا تختار منفذًا، ولا يستمع شيء على أي منفذ. +* يقع `run()` تحت `if __name__ == "__main__":`. كل ما يلي **يستورد** هذا الملف بدلًا من تنفيذه، لذلك يؤدي `run()` دون شرط حماية إلى تشغيل خادم بمجرد تحميل أي شيء للوحدة. +* كائن الخادم متغير عام على مستوى الوحدة اسمه `mcp`. هذا هو الاسم الذي يبحث عنه `mcp run` (ويعمل `server` و`app` أيضًا). إذا سمّيته شيئًا آخر، فحدده صراحة: `mcp run server.py:bookshop`. + +هذا آخر سطر Python في هذه الصفحة. من هنا فصاعدًا، كل شيء يتعلق بإعداد التطبيق المضيف. + +## أمر التشغيل {#the-launch-command} + +يتلقى كل مضيف أدناه الأمر نفسه: + +```bash +uv run --with "mcp[cli]" mcp run /absolute/path/to/server.py +``` + +أمر واحد للجميع لأن `uv run --with` يحل اعتماديات SDK في بيئة جديدة فورًا: يعمل من أي مجلد ولا يحتاج إلى مشروع أو بيئة افتراضية لتفعيلها. وتزداد أهمية ذلك هنا لأن المضيف يشغّل خادمك من مجلد العمل *الخاص به* وببيئة شبه فارغة، وليس من صدفتك. + +وهو أيضًا الأمر الذي يكتبه `mcp install` في إعدادات Claude Desktop نيابة عنك (أدناه)، لذلك يتطابق ما تكتبه يدويًا مع ما تولّده الأداة، باستثناء تثبيت الإصدار الدقيق الذي تضيفه. + +!!! tip "إذا لم يجد المضيف `uv`" + ينشئ المضيف خادمك باستخدام `PATH` محدود، وقد لا يكون `uv` فيه. استبدل الاسم المجرد + `uv` بالمسار المطلق الذي يعطيه `which uv` (macOS/Linux) أو `where uv` (Windows). وهذا + بالضبط ما يكتبه `mcp install`. + +!!! note "هذه الصفحة تتناول التشغيل المحلي" + كل ما هنا يشغّل خادمك على الجهاز نفسه الذي يعمل عليه المضيف: يشغّل المضيف + ملفك عبر stdio. وهذا مناسب تمامًا لأداة شخصية أو تعمل على جهاز واحد. لإتاحة + خادم لأشخاص *لا* يملكون ملفك، تعطيهم **عنوان URL** بدلًا من أمر: كائن + `mcp` نفسه مُتاحًا عبر Streamable HTTP. تعرض **[تشغيل خادمك](../run/index.md)** + هذا القرار في جدول واحد، وتشرح **[النشر والتوسع](../run/deploy.md)** الطريق من + هناك إلى اسم مضيف فعلي. + + والمضيف مجرد تطبيق بداخله عميل MCP، لذا يمكن لشيفرة + Python لديك أن تؤدي دوره: تشغّل **[وسائل نقل العميل](../client/transports.md)** + الملف نفسه كعملية فرعية باستخدام `Client(StdioServerParameters(...))`، بينما تتصل به **[الاختبار](testing.md)** + داخل الذاكرة دون أي عملية إضافية. + +## Claude Desktop {#claude-desktop} + +المضيف الوحيد الذي تستطيع SDK إعداده نيابة عنك: + +```bash +uv run mcp install server.py +``` + +هذا كل شيء. يستورد `mcp install` الملف لقراءة اسم الخادم، ويجد ملف إعدادات Claude Desktop، ويكتب فيه أمر التشغيل. وأثناء ذلك يحوّل مسارك إلى مسار مطلق، فلا تحتاج إلى فعل ذلك بنفسك. + +لا غموض في الأمر. هذا هو الإدخال الذي يكتبه: + +```json +{ + "mcpServers": { + "Bookshop": { + "command": "/absolute/path/to/uv", + "args": [ + "run", + "--frozen", + "--with", + "mcp[cli]==2.0.0", + "mcp", + "run", + "/absolute/path/to/server.py" + ] + } + } +} +``` + +إنه أمر التشغيل من القسم السابق مع ثلاث إضافات: المسار المطلق إلى `uv`، والخيار `--frozen` كي لا يعيد `uv` كتابة ملف قفل يقع قربه، وتثبيت إصدار `mcp` المثبّت لديك تحديدًا. يُكتب في `claude_desktop_config.json` الموجود في: + +* **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` +* **Windows**: `%APPDATA%\Claude\claude_desktop_config.json` + +يمكنك كتابة ذلك الملف يدويًا. وُجد `mcp install` كي لا تقع في الخطأ المعتاد (استخدام مسار نسبي) أثناء ذلك. + +أغلق Claude Desktop بالكامل (وليس نافذته فقط)، ثم افتحه مجددًا. + +!!! warning + يفشل `mcp install` بالرسالة `Claude app not found` إذا لم يكن *مجلد* إعدادات Claude Desktop + موجودًا بعد. ثبّت Claude Desktop وشغّله مرة واحدة: فهذا ما ينشئ المجلد. + +!!! tip + يبدأ Claude Desktop خادمك في عملية خاصة به، لذا لا تتوفر فيها متغيرات البيئة + الخاصة بصدفتك. يسجّل `uv run mcp install server.py -v API_KEY=abc123` (أو `-f .env`) هذه المتغيرات في + حقل `env` للإدخال. يستبدل `--name` اسم الإدخال؛ وافتراضيًا يُستخدم `name` للخادم. + +## Claude Code {#claude-code} + +لا يوجد ملف لتعديله. سجّل الخادم باستخدام CLI الخاصة بـ`claude`؛ كل ما يلي `--` هو أمر التشغيل. + +```bash +claude mcp add bookshop -- uv run --with "mcp[cli]" mcp run /absolute/path/to/server.py +``` + +شغّل `/mcp` داخل جلسة Claude Code للتحقق من اتصال `bookshop` وظهور أدواته. + +## Cursor {#cursor} + +أنشئ `.cursor/mcp.json` في جذر مشروعك. + +```json +{ + "mcpServers": { + "bookshop": { + "command": "uv", + "args": ["run", "--with", "mcp[cli]", "mcp", "run", "/absolute/path/to/server.py"] + } + } +} +``` + +الأمران `command` و`args` نفسيهما، تحت مفتاح `mcpServers` نفسه الذي يستخدمه Claude Desktop. يظهر الخادم في إعدادات MCP في Cursor مع الأداتين. + +## VS Code {#vs-code} + +أنشئ `.vscode/mcp.json` في جذر مشروعك. + +```json +{ + "servers": { + "bookshop": { + "type": "stdio", + "command": "uv", + "args": ["run", "--with", "mcp[cli]", "mcp", "run", "/absolute/path/to/server.py"] + } + } +} +``` + +يوجد اختلافان فقط عن ملف Cursor: المفتاح الخارجي هو `servers` بدلًا من `mcpServers`، ويعلن كل إدخال `type` الخاص به. وافق على مطالبة الثقة، ثم يعرض **MCP: List Servers** في لوحة الأوامر أن `bookshop` يعمل. + +!!! note + تحتاج إلى VS Code 1.99 أو أحدث مع تسجيل الدخول في إضافة **GitHub Copilot** (يكفي Copilot Free)، + ويجب أن يكون Copilot Chat في وضع **Agent** لأن الأوضاع الأخرى لا تستدعي الأدوات. + +## الخادم لا يظهر {#it-doesnt-show-up} + +قبل تعديل أي إعدادات للمضيف، شغّل أمر التشغيل بنفسك: + +```bash +uv run --with "mcp[cli]" mcp run /absolute/path/to/server.py +``` + +لا تُطبع أي مخرجات، ولا ينتهي الأمر. هذا الصمت صحيح: ينتظر خادم stdio أن يبدأ المضيف الحديث على stdin (استخدم `Ctrl-C` لإيقافه). أما تتبّع الاستثناء أو الخروج الفوري فهو العطل الحقيقي، ويمكنك الآن قراءته بدلًا من تخمينه عبر المضيف. + +بعد أن يستقر الأمر منتظرًا، تكون المشكلة المتبقية تقريبًا دائمًا واحدة من ثلاث: + +* **مسار نسبي.** يشغّل المضيف خادمك من مجلد العمل *الخاص به*، لا من المجلد الذي سجّلته منه. استخدام `server.py` حيث يلزم `/absolute/path/to/server.py` هو أكثر أسباب الإخفاق شيوعًا. إذا لم يجد المضيف `uv` أيضًا، فيجب أن يكون مساره مطلقًا كذلك. +* **ما زال المضيف يستخدم إعداداته القديمة.** تقرأ التطبيقات المضيفة إعداداتها عند التشغيل. يجب خصوصًا *إغلاق Claude Desktop بالكامل* (وليس نافذته فقط) ثم فتحه مجددًا كي يسري تعديل `claude_desktop_config.json`. +* **وصلت مخرجات إلى stdout خارج فترة إعادة التوجيه.** في stdio، stdout *هو* البروتوكول. تعيد SDK توجيه المخرجات العرضية المفرَّغة إلى stderr أثناء الخدمة، لكن المخرجات المفرَّغة إلى stdout قبل ذلك (سكربت تغليف يطبع نصًا، أو `print()` أثناء الاستيراد في عملية غير مخزّنة مؤقتًا)، أو `print()` مخزّنة مؤقتًا تُفرَّغ عند خروج المفسّر، تعطي المضيف رسالة تالفة فيقطع الاتصال. استخدم إعدادات `logging` الافتراضية، التي تفرّغ كل سجل عبر دالة معالجة stderr؛ ويجب أيضًا أن تتجنب دوال المعالجة المخصصة stdout. تتضمن **[التسجيل](../handlers/logging.md)** التفاصيل كاملة. + +يحتفظ Claude Desktop بسجل لكل خادم: يمثّل `mcp-server-.log` مخرجات stderr لخادمك، بجانب `mcp.log` للاتصالات، ضمن `~/Library/Logs/Claude` على macOS و`%APPDATA%\Claude\logs` على Windows. + +لأي مشكلة تتجاوز هذه الثلاث، راجع **[استكشاف الأخطاء وإصلاحها](../troubleshooting.md)**. + +## مراجعة {#recap} + +* يشغّل **التطبيق المضيف** (Claude Desktop أو IDE) عميل MCP يبدأ خادمك كعملية ابنة عبر stdio. الاتصال يعني إعطاءه أمر تشغيل واحدًا. +* ذلك الأمر هو `uv run --with "mcp[cli]" mcp run /absolute/path/to/server.py`: لا بيئة افتراضية لتفعيلها، ويعمل من أي مجلد. +* **Claude Desktop** هو المضيف الوحيد الذي يعدّه `mcp install` نيابة عنك. يكتب الأمر نفسه (مع المسار المطلق إلى `uv`، و`--frozen`، وتثبيت إصدارك المثبّت تحديدًا) في `claude_desktop_config.json`، فلا تحتاج إلى فعل ذلك. +* يستخدم **Claude Code** الأمر `claude mcp add bookshop -- `. ويستخدم **Cursor** ملف `.cursor/mcp.json` تحت `mcpServers`. ويستخدم **VS Code** ملف `.vscode/mcp.json` تحت `servers`، مع `type` لكل إدخال. +* استخدم مسارات مطلقة في كل مكان، وأعد تشغيل المضيف بعد تعديل إعداداته، ولا تسمح لشيء غير SDK بالكتابة إلى stdout. + +اتصل كل مضيف في هذه الصفحة بالملف نفسه وبالأمر نفسه. وتشرح بقية هذا التوثيق ما يمكن أن *يتيحه* ذلك الملف: **[الأدوات](../servers/tools.md)** و**[الموارد](../servers/resources.md)**، وكل وسائل النقل غير stdio في **[تشغيل خادمك](../run/index.md)**. diff --git a/i18n/ar/pages/get-started/testing.md b/i18n/ar/pages/get-started/testing.md new file mode 100644 index 0000000000..df30dd1e83 --- /dev/null +++ b/i18n/ar/pages/get-started/testing.md @@ -0,0 +1,114 @@ +--- +translation: + sections: [5d13c2f0ba42c0d2, c52a1de2b6b32f40, 8e792bf8c7489ec6, 38552ea228b0a04f] + tool: 1 +--- +# الاختبار {#testing} + +تستطيع فئة `Client` في SDK، وهي نفسها التي تتصل بعنوان URL أو تشغّل عملية فرعية، الاتصال **داخل الذاكرة** أيضًا: مرّر إليها كائن الخادم فتتواصل معه مباشرة. + +لا عملية فرعية. لا منفذ. لا شيء يمر عبر الشبكة. إنها الفكرة نفسها التي تعتمد عليها `TestClient` في FastAPI. + +## الاستخدام الأساسي {#basic-usage} + +لنفترض أن لديك خادمًا بسيطًا بأداة واحدة: + +```python title="server.py" +--8<-- "docs_src/testing/tutorial001.py" +``` + +لتشغيل الاختبار أدناه، ستحتاج إلى اعتماديتين إضافيتين للتطوير: + +=== "uv" + + ```bash + uv add --dev pytest inline-snapshot + ``` + +=== "pip" + + ```bash + pip install pytest inline-snapshot + ``` + +!!! info + يفترض هذا التوثيق أنك تعرف [`pytest`](https://docs.pytest.org/en/stable/) بالفعل. + + يستخدم الاختبار أدناه [`inline-snapshot`](https://15r10nk.github.io/inline-snapshot/latest/) + للتحقق من كائن النتيجة كاملًا في سطر واحد. وهي تسجّل مخرجات الاختبار بصيغة القيمة الحرفية + `snapshot(...)` التي تراها. إذا فضّلت عدم استخدامها، فاحذف الاستيراد وتحقق من + الحقول التي تهمك (`result.content[0].text == "3"`) كما في أي اختبار آخر. + +والآن الاختبار: + +```python title="test_server.py" +import pytest +from inline_snapshot import snapshot +from mcp import Client +from mcp.types import CallToolResult, TextContent + +from server import mcp + + +@pytest.fixture +def anyio_backend(): # (1)! + return "asyncio" + + +@pytest.fixture +async def client(): # (2)! + async with Client(mcp, raise_exceptions=True) as c: + yield c + + +@pytest.mark.anyio +async def test_call_add_tool(client: Client): + result = await client.call_tool("add", {"a": 1, "b": 2}) + # Drop the server identity stamp in `_meta`; it is not what this test is about. + result.meta = None + assert result == snapshot( + CallToolResult( + content=[TextContent(type="text", text="3")], + structured_content={"result": 3}, + ) + ) +``` + +1. إذا كنت تستخدم `trio`، فأعِد `"trio"` بدلًا من ذلك. راجع [توثيق anyio](https://anyio.readthedocs.io/en/stable/testing.html#specifying-the-backends-to-run-on) للتفاصيل. +2. يوفّر مُجهّز الاختبار عميلًا متصلًا. يحصل كل اختبار يأخذ `client` على اتصال جديد داخل الذاكرة بالخادم نفسه. + +هذا كل شيء! يمكنك الآن توسيع اختباراتك لتشمل سيناريوهات أكثر. + +## لماذا `raise_exceptions=True`؟ {#why-raise_exceptionstrue} + +قد يحدث نوعان مختلفان من الأخطاء، ولا يؤثر هذا الخيار إلا في أحدهما. + +الاستثناء داخل إحدى **أدواتك** ليس إخفاقًا في البروتوكول. بل يصبح نتيجة عادية فيها +`is_error=True` (وإذا كان `ToolError`، يقرأ النموذج رسالتك). لا يغيّر `raise_exceptions` +ذلك: سواء فعّلته أم لا، يعيد `call_tool` النتيجة نفسها مع `is_error=True`. توجد صفحة كاملة لذلك: +**[معالجة الأخطاء](../servers/handling-errors.md)**. + +أما الإخفاق **خارج** جسم الأداة فمختلف. في الاتصال الذي يوفّره `Client(mcp)`، يحوّل +الخادم الخطأ إلى رسالة عامة `"Internal server error"` قبل أن يراه العميل. ينبغي +ألّا تسرّب تفاصيل انهيار غير متوقع إلى مستدعٍ بعيد. وفي الاختبار، هذا تحديدًا ما +*لا* تريده، وهو ما يغيّره `raise_exceptions=True`: يرى اختبارك الرسالة الفعلية +بدلًا من الرسالة التي أُخفيت تفاصيلها. + +اتركه مفعّلًا في الاختبارات. لا معنى له في شيفرة الإنتاج. + +## مستقل عن جيل البروتوكول افتراضيًا {#era-neutral-by-default} + +!!! note + يتصل `Client(mcp)` داخل العملية ويكون **مستقلًا عن جيل البروتوكول** افتراضيًا: يفحص الخادم + ويختار مسار البروتوكول المناسب. ثبّت `mode="legacy"` إذا كان اختبارك يختبر سلوكًا خاصًا بالبروتوكول القديم + (دفع طلبات أخذ العينات أو استقاء المعلومات، أو `message_handler`)، واحذف `raise_exceptions=True` + حينها: فالاتصال القديم لا يُخفي التفاصيل أصلًا، ويعيد هذا الخيار إطلاق + الإخفاق داخل مهمة الخادم بدلًا من اختبارك. + +هذا السطر الواحد هو أيضًا ما يتيح لهذا التوثيق ضمان عمل أمثلته: تختبر +مجموعة اختبارات SDK كل ملف مثال، ومعظمها باستخدام هذا +العميل نفسه. أنت تستخدم الأداة نفسها التي تستخدمها SDK لاختبار ذاتها. + +لديك خادم يعمل وتُختبر صحته. توضح **[الاتصال بتطبيق مضيف فعلي](real-host.md)** وضعه داخل تطبيق حقيقي (Claude Desktop أو +IDE)؛ وتوضح كل الطرق الأخرى لإتاحته صفحة +**[تشغيل خادمك](../run/index.md)**. diff --git a/i18n/ar/pages/handlers/cancellation.md b/i18n/ar/pages/handlers/cancellation.md new file mode 100644 index 0000000000..2c2f3a335b --- /dev/null +++ b/i18n/ar/pages/handlers/cancellation.md @@ -0,0 +1,60 @@ +--- +translation: + sections: [07968345fdc0b84e, 4ea8416db9efa0dc, 336a7b4c5d0a4578, 18392e805dde6717, c30d50df43f9b55c] + tool: 1 +--- +# الإلغاء {#cancellation} + +يمكن للعميل التخلي عن استدعاء: ضغط المستخدم زر الإيقاف، أو انتهت المهلة. + +عندما يفعل، **تلغي SDK دالة المعالجة**. تثير عملية `await` التي تنتظرها استثناءً، وينتهي تنفيذ الدالة مع فك سياقها، ولا تُرسل أي قيمة تعيدها. لا تحتاج معظم دوال المعالجة إلى التعامل مع ذلك. + +لكن نوعين يحتاجان: دالة لديها موارد لتنظيفها، ودالة من نوع `def` عادية. + +## التنظيف في أداة `async def` {#clean-up-in-an-async-def-tool} + +ضع التنظيف في `finally`: + +```python title="server.py" hl_lines="23 26-28" +--8<-- "docs_src/cancellation/tutorial001.py" +``` + +* تعمل `finally` مهما كانت نهاية الأداة: إعادة نتيجة أو إثارة استثناء أو إلغاء. +* يحتاج التنظيف الذي يستخدم `await` إلى `shield=True`. في دالة معالجة مُلغاة، تثير كل `await` لاحقة استثناءً أيضًا، لذا ستتوقف `release_hold` عند أول سطر دون الحماية. +* لا يمكن لشيء إلغاء كتلة محمية، فامنحها حدًا زمنيًا. وهو هنا `5` ثوانٍ. + +!!! tip + استخدم `finally` بدلًا من `except`. يجب أن يستمر الإلغاء في الانتشار بعد + اكتمال التنظيف، وتتيح له `finally` ذلك. + +## التوقف مبكرًا في أداة `def` عادية {#stop-early-in-a-plain-def-tool} + +تعمل أداة `def` العادية في خيط تنفيذ، ولا يمكن مقاطعة خيط من الخارج. يجب أن تتحقق الأداة بنفسها: + +```python title="server.py" hl_lines="22 25-26" +--8<-- "docs_src/cancellation/tutorial002.py" +``` + +* لا تفعل `anyio.from_thread.check_cancelled()` شيئًا ما دام الاستدعاء نشطًا، وتثير استثناءً بعد إلغائه. استدعِها بين وحدات العمل. +* يوضع التنظيف في `finally` هنا أيضًا. لا توجد عمليات انتظار في الخيط، فلا يحتاج إلى حماية. +* تعمل أداة `def` التي لا تتحقق حتى النهاية، وتُهمَل نتيجتها. + +## أين ينطبق ذلك؟ {#where-it-applies} + +تُلغى دوال قوالب التوجيه والموارد مثل الأدوات تمامًا. + +يعمل بالطريقة نفسها عبر stdio وStreamable HTTP. مع `Client` في هذه SDK، يعني التخلي إلغاء المهمة التي تنتظر `call_tool`، أو انتهاء `read_timeout_seconds`. + +!!! warning + يمنع خياران في Streamable HTTP وصول الإلغاء إلى دالة المعالجة: `json_response=True` على + اتصال `2026-07-28`، و`stateless_http=True` على اتصال قديم. حينها تعمل الدالة حتى + النهاية مهما فعل العميل. + +## مراجعة {#recap} + +* عندما يتخلى العميل عن استدعاء، تلغي SDK دالة المعالجة: أداة أو قالب توجيه أو مورد. +* مع `async def`: نظّف في `finally`، وضع التنظيف الذي ينتظر داخل `anyio.move_on_after(seconds, shield=True)`. +* مع `def` العادية: استدعِ `anyio.from_thread.check_cancelled()` بين وحدات العمل، وإلا تعمل الأداة حتى النهاية. تتولى `finally` عادية التنظيف. +* يعطّل `json_response=True` (الاتصالات الحديثة) و`stateless_http=True` (القديمة) الإلغاء. + +التقدم والإلغاء بين أداة تعمل و*مستدعيها*. أما الأسطر التي تسجّلها *لك*، بوصفك مشغّل الخادم، فهي قناة مختلفة: **[التسجيل](logging.md)**. diff --git a/i18n/ar/pages/handlers/context.md b/i18n/ar/pages/handlers/context.md new file mode 100644 index 0000000000..15be8186ba --- /dev/null +++ b/i18n/ar/pages/handlers/context.md @@ -0,0 +1,134 @@ +--- +translation: + sections: [b50152f05c81e786, b302059b22fb7cb4, 85682a1bf561243a, 53fc48838eb6837a, b24190e0842786ec, 85f93e150fc9b240] + tool: 1 +--- +# السياق {#the-context} + +تأتي وسائط الأداة من النموذج. وكل ما عداها (الطلب الذي تخدمه، والخادم الذي تعمل فيه، ووسيلة التواصل مع العميل) يأتي من كائن واحد: **`Context`**. + +لا تنشئه ولا تعدّه. تطلبه فقط. + +## اطلبه {#ask-for-it} + +أضف مَعلمة ذات تعليق نوع `Context` إلى أي أداة: + +```python title="server.py" hl_lines="2 8" +--8<-- "docs_src/context/tutorial001.py" +``` + +* تبني SDK كائن `Context` جديدًا لكل طلب وتمرّره. +* **لا يهم اسم** المَعلمة. سواء `ctx` أو `context` أو `c`، تعثر عليها SDK من تعليق نوعها. +* تستطيع الموارد وقوالب التوجيه إعلان واحد أيضًا بالطريقة نفسها. +* `ctx.request_id` هو معرّف الطلب الذي تخدمه دالتك الآن. + +!!! info + إذا استخدمت FastAPI، فقد رأيت هذا الأسلوب: أعلن مَعلمة بنوع خاص بإطار العمل + (`Request` هناك و`Context` هنا) فيوفرها الإطار. لا تسجيل ولا + إعداد: تعليق النوع هو الآلية كاملة. + +### غير مرئي للنموذج {#invisible-to-the-model} + +هذه الفكرة التي ينبغي استيعابها. إليك مخطط المدخلات الذي يعرضه `tools/list` لـ`search_books`: + +```json +{ + "type": "object", + "properties": { + "query": {"title": "Query", "type": "string"} + }, + "required": ["query"], + "title": "search_booksArguments" +} +``` + +خاصية واحدة. ليست `ctx` وسيطة: لا تظهر أبدًا في المخطط، ولا يعرف عنها النموذج، ولا يستطيع أي عميل ملأها. إنها عقد بينك وبين SDK، وغير مرئية في البيانات المنقولة. + +### جرّبه {#try-it} + +شغّل الخادم باستخدام MCP Inspector: + +```console +uv run mcp dev server.py +``` + +يملك نموذج `search_books` حقل `query` واحدًا. استدعِه مع `dune`: + +```text +[request 3] Found 3 books matching 'dune'. +``` + +الرقم هو معرّف الطلب الحالي. استدعِ الأداة مجددًا فيتغير: لكل طلب `Context` خاص به. + +## ما الذي يوفّره لك؟ {#what-it-gives-you} + +الكائن المحقون صغير. إلى جانب `request_id`: + +* `await ctx.read_resource(uri)`: اقرأ أحد موارد الخادم **نفسه** من داخل أداة. يشرح ذلك القسم التالي. +* `await ctx.report_progress(progress, total, message)`: أرسل التقدم إلى المستدعي أثناء استدعاء طويل. التفاصيل كاملة في **[التقدم](progress.md)**. +* `await ctx.elicit(message, schema)` و`await ctx.elicit_url(...)`: أوقف الأداة مؤقتًا واسأل المستخدم. هذا موضوع **[استقاء المعلومات](elicitation.md)**. +* `ctx.session`: جانب الخادم من المحادثة مع هذا العميل. توجد هنا الإشعارات التي ترسلها إليه؛ ويستخدمها القسم الأخير. +* `ctx.headers`: ترويسات الطلب التي حملتها وسيلة النقل، أو `None` في stdio. اقرأ ترويسة مخصصة باستخدام `(ctx.headers or {}).get("x-...")`. الترويسات مدخلات يقدّمها العميل؛ تصلح للغة أو لخيار ميزة، ولا تصلح أبدًا لإثبات الهوية. +* `ctx.request_context`: السجل الخام الخاص بكل طلب. الحقل الذي ستحتاج إليه هو `lifespan_context`، أي الكائن الذي أنتجته شيفرة بدء التشغيل (راجع **[دورة الحياة](lifespan.md)**). + +التسجيل غير موجود في هذه القائمة عمدًا. يسجّل الخادم باستخدام وحدة `logging` في Python مثل أي برنامج Python آخر. وتشرح **[التسجيل](logging.md)** السبب باختصار. + +!!! tip + يحدث الحقن للدالة التي سجّلتها فقط. لا تحصل دالة مساعدة تستدعيها أداتك + على `Context` خاص بها؛ مرّر `ctx` إليها كوسيطة عادية. لا يوجد + "سياق حالي" عام تسترجعه من مكان آخر. + +## اقرأ مواردك الخاصة {#read-your-own-resources} + +موارد الخادم ليست للعملاء فقط. تستطيع الأداة قراءتها أيضًا: + +```python title="server.py" hl_lines="16" +--8<-- "docs_src/context/tutorial002.py" +``` + +تحلّ `ctx.read_resource` عنوان URI عبر السجل نفسه الذي يخدم `resources/read`، لذلك تحصل الأداة على ما يحصل عليه العميل: كائن قابل للتكرار من `ReadResourceContents`، واحد لكل كتلة محتوى. لهذا URI كتلة واحدة: + +```python +contents.content # 'fiction, non-fiction, poetry' +contents.mime_type # 'text/plain' +``` + +* `content` هو بالضبط ما أعادته `genres()`. مصدر حقيقة واحد: يتصفح العميل المورد، وتستهلكه أدواتك، ولا ينسخ أحد السلسلة النصية. +* المَعلمة الوحيدة لـ`describe_catalog` هي `Context`، لذا **لا توجد أي خصائص** في مخطط المدخلات. يستدعيها النموذج باستخدام `{}`. + +## أخبر العميل بتغيّر القائمة {#tell-the-client-the-list-changed} + +ما يتيحه الخادم ليس ثابتًا عند الاستيراد. سجّل أداة أثناء التشغيل، ثم أخبر العميل: + +```python title="server.py" hl_lines="15-16" +--8<-- "docs_src/context/tutorial003.py" +``` + +* تسجّل `mcp.add_tool(recommend_book)` دالة عادية كأداة: يُشتق الاسم والوصف والمخطط تمامًا كما يفعل `@mcp.tool()`. +* ترسل `await ctx.session.send_tool_list_changed()` إشعار `notifications/tools/list_changed`. يعيد العميل الذي يتلقاه استدعاء `tools/list` ويرى `recommend_book`. + +العمليات المقابلة هي `send_resource_list_changed()` و`send_prompt_list_changed()` و`send_resource_updated(uri)` لتغيّر مورد محدد. + +في اتصال 2026-07-28، لا يتلقى العملاء إشعارات التغيير إلا على تدفّق `subscriptions/listen` فتحوه، لذلك لا تصل طرق `send_*` أعلاه إلى تلك التدفّقات. توصل طرق النشر في `Context` الإشعارات إلى كل التدفّقات المشتركة دفعة واحدة: `await ctx.notify_tools_changed()` و`await ctx.notify_prompts_changed()` و`await ctx.notify_resources_changed()` و`await ctx.notify_resource_updated(uri)`. التفاصيل كاملة، بما فيها التوسع عبر نسخ الخادم، في **[الاشتراكات](subscriptions.md)**. + +!!! check + قبل أن يشغّل أحد `enable_recommendations`، لا توجد الأداة التي تعد بها. استدعِها + رغم ذلك فتكون النتيجة خطأ يستطيع النموذج قراءته: + + ```text + Unknown tool: recommend_book + ``` + + شغّل `enable_recommendations` فينجح الاستدعاء نفسه. قائمة الأدوات + ديناميكية فعلًا: يعكس `tools/list` كل ما هو مسجّل *الآن*. + +## مراجعة {#recap} + +* أضف تعليق نوع `Context` إلى مَعلمة (في أداة أو مورد أو قالب توجيه) فتحقنها SDK. أنت تختار الاسم. +* هي غير مرئية للنموذج: لا يحتوي مخطط المدخلات إلا على وسائطك الفعلية. +* تحدد `ctx.request_id` الطلب؛ و`ctx.request_context.lifespan_context` هو ما أنتجته شيفرة بدء التشغيل. +* تتيح `await ctx.read_resource(uri)` للأداة قراءة موارد الخادم نفسه. +* `ctx.session` قناة التواصل مع العميل: تخبره `send_tool_list_changed()` والطرق المقابلة بإعادة جلب قائمة غيّرتها. +* يبدأ الإبلاغ عن التقدم واستقاء المعلومات أيضًا من `Context`؛ ولكل منهما صفحة خاصة. + +المَعلمات التي لا يراها النموذج وتملؤها دوالك هي **[الاعتماديات](dependencies.md)**. diff --git a/i18n/ar/pages/handlers/dependencies.md b/i18n/ar/pages/handlers/dependencies.md new file mode 100644 index 0000000000..d064445839 --- /dev/null +++ b/i18n/ar/pages/handlers/dependencies.md @@ -0,0 +1,163 @@ +--- +translation: + sections: [b0389403e98d25ad, e2cf58b43b285e86, a363e1a38e1a5971, 6cfac078feb18013, b4535bd61df337e6, e97ed44207f929fd] + tool: 1 +--- +# الاعتماديات {#dependencies} + +تأتي وسائط الأداة من النموذج. لكن بعض القيم يجب ألّا تأتي منه أبدًا: سعر مأخوذ من سجلاتك، أو تأكيد لا يستطيع تقديمه إلا شخص، أو أي شيء قد يخطئ فيه النموذج باختلاقه. + +**الاعتماديات** مَعلمات تملؤها دوالك. تضيف تعليقًا للمَعلمة وتحدد الدالة، فتستدعيها SDK قبل تشغيل أداتك. + +## أعلن اعتمادية {#declare-one} + +غلّف نوع المَعلمة في `Annotated[...]` وأضف `Resolve(fn)`: + +```python title="server.py" hl_lines="18-19 23" +--8<-- "docs_src/dependencies/tutorial001.py" +``` + +* `check_stock` هي **دالة حل الاعتمادية**: دالة عادية تشغّلها SDK قبل `reserve_book`، وتصبح قيمتها المعادة الوسيطة `stock`. +* المَعلمة `title` فيها هي وسيطة `title` للأداة نفسها، وتُطابَق **بالاسم**. ترى دالة الحل القيمة المتحقق منها نفسها التي سيراها جسم الأداة. +* يبدأ جسم الأداة بكائن `Stock` موجود بالفعل. لا شيفرة بحث في الأداة، ولا مقدمة لمعالجة احتمال غيابه. + +!!! info + إذا استخدمت FastAPI، فهذا هو `Depends`. الأسلوب والسبب نفسيهما: تعلن الدالة ما + تحتاج إليه، ويوفره إطار العمل، ويوجد الربط في تعليق النوع. + +### غير مرئية للنموذج {#invisible-to-the-model} + +إليك مخطط المدخلات الذي يعرضه `tools/list` لـ`reserve_book`: + +```json +{ + "type": "object", + "properties": { + "title": {"title": "Title", "type": "string"} + }, + "required": ["title"], + "title": "reserve_bookArguments" +} +``` + +خاصية واحدة. مثل `Context` في **[السياق](context.md)**، المَعلمة المحلولة عقد بينك وبين SDK: لا توجد `stock` في المخطط، ولا يعرف عنها النموذج، وتُتجاهل قيمة `stock` التي يرسلها العميل رغم ذلك. قيمة دالة الحل هي الوحيدة التي تتلقاها أداتك. + +هذه هي الغاية. المَعلمة التي لا يستطيع النموذج تقديمها هي مَعلمة لا يستطيع الخطأ فيها. + +### جرّبها {#try-it} + +شغّل الخادم باستخدام MCP Inspector: + +```console +uv run mcp dev server.py +``` + +يملك نموذج `reserve_book` حقل `title` واحدًا. لا تظهر `stock` فيه. استدعِه مع `Dune`: + +```text +Reserved 'Dune' (6 copies left). +``` + +لم يبحث جسم الأداة عن شيء: عملت `check_stock` أولًا، ووصلت `Stock` التي أعادتها كوسيطة. جرّب `Neuromancer` فتقدّم دالة الحل نفسها للأداة قيمة صفر. + +!!! tip + يمكنك استدعاء `check_stock(title)` في جسم الأداة مباشرة. أعلنها اعتمادية عندما تستحق + القيمة أكثر من استدعاء مساعد: تعلن كل أداة تحتاج إلى المخزون المَعلمة نفسها، + وتشغّل SDK دالة الحل مرة واحدة كحد أقصى لكل استدعاء مهما بلغ عدد من يعلنها. تضيف + الأقسام التالية الباقي: دوال حل تعتمد على بعضها، ودوال حل تسأل المستخدم. + +## اعتماديات الاعتماديات {#dependencies-of-dependencies} + +تستطيع دالة الحل إعلان اعتمادياتها بالتعليق نفسه: + +```python title="server.py" hl_lines="22 29-30" +--8<-- "docs_src/dependencies/tutorial002.py" +``` + +* تعتمد `estimate_delivery` على `check_stock`. تنفّذ SDK الرسم البياني بالترتيب: المخزون أولًا، ثم التقدير، ثم الأداة. +* تحتاج `stock` و`delivery` في النهاية إلى `check_stock`، لكنها تعمل **مرة واحدة لكل استدعاء**. بحث واحد عن المخزون ومستهلِكان. +* لا شيء لتسجيله. التعليقات *هي* الرسم البياني. + +!!! check + لا تقبل ضمان مرة لكل استدعاء دون تجربة. ضع `print` في `check_stock` واستدعِ `order_book` من + Inspector: سطر واحد لكل استدعاء. مستهلِكان وبحث واحد. + +تحلّل SDK الرسم البياني عند تسجيل الأداة، لا عند استدعائها. تؤدي مَعلمة لا تستطيع تصنيفها (ليست `Context` ولا `Resolve(...)` ولا اسم وسيطة أداة)، أو دورة بين دوال الحل، إلى `InvalidSignature` عند بدء التشغيل. يفشل خادمك قبل اتصال أي عميل، مع ذكر المَعلمة أو دالة الحل المسببة للخطأ. + +تُحل مَعلمات دالة الحل تمامًا كمَعلمات الأداة: `Resolve(...)` أخرى، أو وسائط الأداة نفسها بالاسم، أو `Context`، بما فيه `ctx.headers` وكائن دورة الحياة وكل ما عداه. + +!!! warning + في وسائل نقل HTTP، يتضمن `Context` حقل `ctx.headers`. الترويسات **مدخلات يقدّمها العميل**، + مثل أي وسيطة أداة: تصلح للغة أو خيار ميزة، ولا تصلح لإثبات الهوية. تُحدَّد هوية + المستدعي من طبقة التفويض لديك (**[التفويض](../run/authorization.md)**)، لا من ترويسة يستطيع أي شخص تعيينها. + +!!! tip + تعني *مرة لكل استدعاء* ذلك بالضبط: يشغّل `tools/call` التالي `check_stock` مجددًا. المورد + الذي ينبغي أن يعيش أكثر من طلب (مجموعة اتصالات قاعدة بيانات أو عميل HTTP) مكانه **[دورة الحياة](lifespan.md)**، + وتصل إليه دالة الحل عبر `ctx.request_context.lifespan_context`. + +## اسأل عندما يلزم {#ask-when-you-must} + +ليس على دالة الحل معرفة الإجابة. تستطيع إعادة `Elicit(message, Model)` فتسأل SDK المستخدم، باستخدام آلية **[استقاء المعلومات](elicitation.md)** (elicitation) نيابة عنك: + +```python title="server.py" hl_lines="26-32 39" +--8<-- "docs_src/dependencies/tutorial003.py" +``` + +* عند توفر المخزون: تعيد `confirm_backorder` كائن `Backorder` مباشرة. **لا سؤال ولا جولة تبادل.** لا يُقاطَع المستخدم إلا عندما تهم إجابته. +* عند نفاد المخزون: ترسل SDK طلب استقاء المعلومات، وتتحقق من الإجابة مقابل `Backorder`، ثم تحقنها. لا تتعامل دالة الحل مع البروتوكول. +* تقرأ الأداة `backorder.confirm` مثل أي وسيطة أخرى. الإجابة بـ**لا** تظل إجابة: يُقبل طلب استقاء المعلومات مع `confirm=False`، وتعمل الأداة دون إنشاء طلب شراء. أصبح السؤال شرطًا مسبقًا، بدلًا من شيفرة ربط داخل جسم الأداة. + +وماذا إذا لم يجب المستخدم أصلًا، بأن يرفض السؤال أو يلغيه؟ + +!!! check + شغّل `order_book` لـ`Neuromancer` وارفض السؤال. مع التعليق + `Annotated[Backorder, Resolve(...)]`، لا يعمل جسم الأداة؛ ويفشل الاستدعاء بنتيجة + خطأ يستطيع النموذج قراءتها: + + ```text + Error executing tool order_book: Resolver for parameter 'backorder' could not resolve: elicitation was decline + ``` + +هذا الإعداد الافتراضي المناسب لشرط مسبق: لا إجابة، لا طلب شراء. إذا كان الرفض نتيجة تريد أداتك التعامل معها (تجاوز الطلب المؤجل مع اقتراح عنوان آخر)، فاستخدم تعليق `ElicitationResult[Backorder]` لتتلقى الأداة نتيجة القبول أو الرفض أو الإلغاء كاملة وتتفرع وفقها. تعرض **[استقاء المعلومات](elicitation.md)** هذا الشكل وكل تفاصيل السؤال: قواعد المخطط والإجابات الثلاث وجانب العميل من المحادثة. + +!!! info + يختار إطار العمل وسيلة نقل السؤال من إصدار البروتوكول المتفاوض عليه؛ وتبقى الشيفرة + أعلاه متطابقة في الحالتين. في **2026-07-28** وما بعده، يُحمَل السؤال داخل + `tools/call` متعدد جولات التبادل: يعيده الخادم، وتجيب عنه `elicitation_callback` + لدى العميل، وتعيد `Client` الاستدعاء نيابة عنك (**[الطلبات متعددة جولات التبادل](multi-round-trip.md)**). في + **2025-11-25** وما قبله، يكون طلب استقاء معلومات متزامنًا أثناء الاستدعاء. يُطرح كل سؤال + مرة واحدة بالضبط لكل استدعاء؛ وهذا ضمان للسؤال، لا لدالة الحل. في + الشكل متعدد الجولات، قد تعمل أي دالة حل مجددًا كلما استؤنف الاستدعاء بعد سؤال، + لذلك تعمل الشيفرة قبل `return Elicit(...)` في كل جولة؛ ثم تجيب الإجابة المسجلة + عن السؤال المتكرر دون مطالبة المستخدم مجددًا. لا تُراجَع الإجابة المسجلة + إلا عندما تسأل دالة الحل؛ أما دالة الحل التي تجيب *دون* سؤال، مثل + `check_stock`، فتوفر دائمًا قيمتها المحسوبة. ولأن كل إجابة تُطابَق + مع سؤالها، يجب أن تشتق دالة الحل التي تسأل سؤالها بصورة حتمية من + وسائط الأداة والإجابات السابقة. تُعاد توليد القيمة المنشأة لكل استدعاء (معرّف `default_factory` أو + طابع زمني) في كل جولة، ويجب ألّا تظهر في سؤال يُراد + ربط الإجابة به. يجعل السؤال المبني على بيانات متغيرة كهذه كل إجابة مسجلة تبدو قديمة، + فيعيد الخادم طرحه في كل جولة حتى ينهي حد الجولات لدى العميل الاستدعاء. + +## اسأل العميل، لا المستخدم {#ask-the-client-not-the-user} + +استقاء المعلومات أحد الأسئلة الثلاثة التي تستطيع دالة الحل طرحها، ولا يسمح التدفق متعدد الجولات بغيرها. يذهب الاثنان الآخران إلى **العميل** بدلًا من المستخدم: أعِد `Sample(...)` لتشغيل استدعاء LLM عبر العميل (طلب `sampling/createMessage`)، أو `ListRoots()` لجلب المجلدات الجذرية الحالية للعميل (roots). لا يملك أي منهما نتيجة قبول أو رفض؛ يحدد المستهلك نوع النتيجة مباشرة: `CreateMessageResult` (أو `CreateMessageResultWithTools` عندما يحمل الطلب `tools` أو `tool_choice`) أو `ListRootsResult`: + +```python title="server.py" hl_lines="10-15 21" +--8<-- "docs_src/dependencies/tutorial004.py" +``` + +* يوجّه إطار العمل هذه الطلبات تمامًا مثل `Elicit`: داخل `tools/call` متعدد الجولات في **2026-07-28**، وعبر طلب مستقل من الخادم إلى العميل في **2025-11-25**. تؤدي القدرة غير المعلنة إلى رفض الاستدعاء بخطأ بروتوكول `-32021` (`sampling` أو`roots` أو`elicitation` بنمط النموذج؛ و`sampling.tools` عندما يحمل الطلب `tools` أو `tool_choice`). +* ينطبق كل ما يقوله مربع المعلومات أعلاه عن الأسئلة دون تغيير: يُطابَق طلب `Sample` بنتيجته المسجلة وفق تمثيله الدقيق، فابنه بصورة حتمية من وسائط الأداة والإجابات السابقة؛ وعندها يدفع العميل تكلفة استدعاء LLM مرة لكل استدعاء أداة، لا مرة لكل جولة. تُحمَل النتيجة المسجلة في `request_state` لبقية الاستدعاء، لذلك يزيد الاستكمال الكبير جدًا حجم كل جولة تبادل متبقية. +* أصبحت *ميزتا* أخذ العينات (sampling) والمجلدات الجذرية المستقلتان مهجورتين في 2026-07-28 (SEP-2577). تسأل الخوادم الجديدة التي تحتاج إلى نموذج العميل عبر هذا المسار؛ وينبغي للخوادم التي لا تحتاج إليه التكامل مباشرة مع مزوّد LLM. قيم `include_context` غير `"none"` مهجورة أيضًا؛ تجنّبها. + +## مراجعة {#recap} + +* باستخدام `Annotated[T, Resolve(fn)]` على مَعلمة أداة، تشغّل SDK الدالة `fn` وتحقن قيمتها المعادة. +* المَعلمة المحلولة غير مرئية للنموذج ولا يستطيع العميل توفيرها. هنا مكان القيم التي يجب ألّا يختلقها النموذج، مثل الأسعار والهويات والصلاحيات. +* تُحل مَعلمات دالة الحل بالطريقة نفسها: `Context` أو `Resolve(...)` أخرى أو وسيطة أداة بالاسم. يشغّل الرسم البياني كل دالة حل مرة واحدة كحد أقصى لكل جولة مهما تعدد مستهلكوها؛ ويُطرح كل سؤال مرة واحدة بالضبط، وقد تعمل أي دالة حل مجددًا عند استئناف الاستدعاء بعد سؤال. +* تفشل الرسوم البيانية غير الصالحة عند التسجيل بـ`InvalidSignature`، لا أثناء الاستدعاء. +* أعِد `Elicit(message, Model)` لسؤال المستخدم عند الحاجة فقط. تُجهض التعليقات غير المغلّفة الاستدعاء عند الرفض؛ ويتيح `ElicitationResult[T]` للأداة التفرع. +* أعِد `Sample(...)` أو `ListRoots()` لطلب استكمال يولّده LLM أو قائمة المجلدات الجذرية من العميل؛ وتُحقن النتيجة مباشرة. + +الحالة التي يبنيها خادمك مرة واحدة عند بدء التشغيل، وكيف تصل إليها دالة المعالجة، موضوع صفحة **[دورة الحياة](lifespan.md)**. diff --git a/i18n/ar/pages/handlers/elicitation.md b/i18n/ar/pages/handlers/elicitation.md new file mode 100644 index 0000000000..ecd672c12c --- /dev/null +++ b/i18n/ar/pages/handlers/elicitation.md @@ -0,0 +1,191 @@ +--- +translation: + sections: [335ca2a0b266f003, d1ad562d3fe87bc0, 25b49f89c9e6f9d0, d1cb1235bb9ee267, 833179c09d239c83, e5d6dec2d2e655e8] + tool: 1 +--- +# استقاء المعلومات {#elicitation} + +لا يلزم أن تفشل أداة قطعت نصف عملها وتفتقد إجابة واحدة. + +يتيح لها **استقاء المعلومات** (elicitation) السؤال. يتلقى المستخدم سؤالًا أثناء استدعاء الأداة، وتعود إجابته إلى استدعاء الدالة نفسه. + +يوجد نمطان: + +* **نمط النموذج**: تحتاج إلى قيمة (تأكيد أو تاريخ أو كمية). تصف الحقول ويعرض العميل النموذج. +* **نمط URL**: تحتاج إلى انتقال المستخدم إلى مكان آخر (شاشة موافقة OAuth أو صفحة دفع). لا يمر شيء مما يفعله هناك عبر البروتوكول. + +وتوجد طريقتان للسؤال. الطريقة المفضلة **دالة حل الاعتمادية**: تربط السؤال بمَعلمة، وتسأل SDK على أي اتصال، مهما كان جيل بروتوكول العميل. الطريقة المباشرة، `await ctx.elicit(...)`، طلب من *الخادم* إلى *العميل*، وهي قناة لا توجد إلا لعميل على اتصال قديم (إصدار المواصفة 2025-11-25 أو أقدم). تشرح الصفحة الطريقتين؛ ابدأ بدالة الحل. + +## اسأل باستخدام دالة حل الاعتمادية {#ask-with-a-resolver} + +يمكن نقل سؤال يتوقف عليه تنفيذ الأداة بالكامل (*هل أنت متأكد؟ أي الحسابات الثلاثة المطابقة؟*) من جسم الأداة إلى **دالة حل اعتمادية**، ليسأله إطار العمل نيابة عنك. + +تُملأ مَعلمة ذات التعليق `Annotated[T, Resolve(fn)]` بتشغيل `fn` قبل جسم الأداة. تعيد دالة الحل القيمة مباشرة عندما تعرفها، أو تعيد `Elicit(...)` ليطرح إطار العمل السؤال: + +```python title="server.py" hl_lines="24-30 35-36" +--8<-- "docs_src/elicitation/tutorial004.py" +``` + +* تقرأ `confirm_delete` وسيطة `path` للأداة بالاسم، وتعرض محتويات المجلد، و**لا تسأل إلا عند الحاجة**: يُحل المجلد الفارغ إلى `Confirm(ok=True)` دون جولة تبادل مع العميل. +* تستخدم `delete_folder` تعليق `ElicitationResult[Confirm]`، فيحقن الإطار النتيجة كاملة، وتطابق الأداة باستخدام `match` كل حالة: قبول وتأكيد، وقبول مع الإبقاء (`ok=False`)، ورفض، وإلغاء. +* لا تظهر المَعلمة `confirm` أبدًا في مخطط مدخلات الأداة: يقدّم العميل `path`، وتقدّم دالة الحل `confirm`. + +استخدم تعليق النموذج غير المغلّف (`Annotated[Confirm, Resolve(confirm_delete)]`) عندما لا تحتاج الأداة إلى التفرع: تتلقى النموذج عند القبول، ويُجهَض الاستدعاء بخطأ عند الرفض أو الإلغاء. + +تعمل دالة الحل على **كل** اتصال. لعميل على اتصال قديم، ترسل SDK السؤال مباشرة؛ وعلى اتصال **2026-07-28**، *تعيد* SDK السؤال من الاستدعاء، وتحمل محاولة العميل التالية الإجابة. لا تعرف دالة الحل الفرق؛ وتشرح ما يحدث داخليًا **[الطلبات متعددة جولات التبادل](multi-round-trip.md)**. + +السؤال واحد فقط مما تستطيع دالة الحل فعله. الآلية العامة (اعتماديات تحسب دون سؤال، واعتماديات الاعتماديات، وما يستطيع النموذج تقديمه أو لا يستطيع) في صفحة **[الاعتماديات](dependencies.md)**. + +## اسأل من داخل الأداة {#ask-from-inside-the-tool} + +تستطيع الأداة أيضًا التوقف وسط جسمها والسؤال. + +!!! warning + `ctx.elicit()` و`ctx.elicit_url()` طلبان من *الخادم* إلى *العميل*، وهذه + قناة لا توجد إلا لعميل على اتصال قديم (إصدار المواصفة **2025-11-25** + أو أقدم). لا توجد طلبات يبدأها الخادم على اتصال **2026-07-28**، لذلك + تفشل هذه الاستدعاءات. تعمل دالة الحل على كليهما. تتضمن **[إصدارات البروتوكول](../protocol-versions.md)** + التفاصيل كاملة. + +تأخذ `await ctx.elicit()` رسالة ونموذج Pydantic: + +```python title="server.py" hl_lines="9-11 20-23 25" +--8<-- "docs_src/elicitation/tutorial001.py" +``` + +* مَعلمة **`Context`** هي ما يتيح `ctx.elicit`؛ تستطيع أي أداة أخذها. لذلك الكائن صفحة خاصة: **[السياق](context.md)**. +* `AlternativeDate` **مخطط** الإجابة التي تريدها. +* الأداة `async def`. يجب أن تكون كذلك، لأنها تتوقف أثناء التنفيذ وتنتظر شخصًا. +* في أي تاريخ آخر، تعيد الأداة النتيجة فورًا. لا تسأل إلا عند الحاجة. +* يعود التاريخ الذي يقبله المستخدم عبر `book_table` نفسها. الإجابة مدخل كأي مدخل: إذا كان البديل محجوزًا بالكامل أيضًا، يُسأل عنه مجددًا بدلًا من تأكيده دون تحقق. + +### ما يتلقاه العميل {#what-the-client-receives} + +يتلقى العميل رسالتك وبجانبها JSON Schema مولّدًا من النموذج: + +```json +{ + "properties": { + "accept_alternative": { + "description": "Try another date?", + "title": "Accept Alternative", + "type": "boolean" + }, + "date": { + "default": "2025-12-26", + "description": "Alternative date (YYYY-MM-DD)", + "title": "Date", + "type": "string" + } + }, + "required": ["accept_alternative"], + "title": "AlternativeDate", + "type": "object" +} +``` + +هذا المخطط هو النموذج المعروض. `Field(description=...)` هو عنوان الحقل؛ وتملأ القيمة الافتراضية المدخل مسبقًا وتجعله اختياريًا. إنها آلية Pydantic إلى JSON Schema نفسها التي تشرحها **[الأدوات](../servers/tools.md)** لوسائط الأداة. + +!!! warning + مخطط استقاء المعلومات ليس بمرونة مخطط مدخلات الأداة. يسمح فقط بحقول مسطحة ذات أنواع أولية: + `str` أو`int` أو`float` أو`bool` أو `Literal` من سلاسل نصية (تصبح `enum`). + ضع نموذجًا داخل النموذج فتثير `ctx.elicit` استثناءً قبل إرسال أي شيء إلى العميل. + يفشل استدعاء الأداة بـ`Error executing tool `، ويوجد السبب في سجل خادمك: + + ```text + TypeError: Elicitation schema field 'address' rendered as {'$ref': '#/$defs/Address'}, which is not a valid PrimitiveSchemaDefinition + ``` + + أنت تقاطع شخصًا أثناء مهمة. إذا احتاجت الإجابة إلى تداخل، كان ينبغي أن تكون + وسيطة للأداة. + +### الإجابات الثلاث {#the-three-answers} + +تخبرك `result.action` بما فعله المستخدم، وتوجد ثلاثة احتمالات بالضبط: + +* `"accept"`: أرسل النموذج. تكون `result.data` نسخة `AlternativeDate` جرى التحقق منها مسبقًا. +* `"decline"`: رفض. +* `"cancel"`: أغلق السؤال دون اختيار. + +لا توجد `result.data` إلا مع `"accept"`، لذلك يفحص المثال `result.action` أولًا. يفرض فاحص الأنواع هذا الترتيب: بعد `result.action == "accept"`، تكون `result.data` من نوع `AlternativeDate`؛ وقبل ذلك لا توجد `.data` أصلًا. + +الرفض ليس خطأ. تقرر الأداة معنى الرفض (هنا عدم الحجز) وتجيب النموذج بصورة عادية. + +!!! tip + يُتحقَّق من الإجابة مقابل نموذجك قبل أن تراها شيفرتك. لا يفسد العميل الذي يرسل + `"maybe"` لقيمة `bool` حجزك: تثير `ctx.elicit` الاستثناء `ValueError`، ويفشل + الاستدعاء، ولا تعمل `if` لديك. + +## وجّه المستخدم إلى URL {#send-the-user-to-a-url} + +يجب ألّا تمر بعض الأمور عبر النموذج أو العميل: بيانات الاعتماد وأرقام البطاقات وموافقة OAuth. لهذه الحالات، لا تطلب بيانات، بل تطلب من المستخدم الانتقال إلى مكان: + +```python title="server.py" hl_lines="10-14 23" +--8<-- "docs_src/elicitation/tutorial002.py" +``` + +* تأخذ `ctx.elicit_url()` الرسالة و**URL** المراد زيارته و`elicitation_id` تختاره: أي سلسلة تميّز طلب استقاء المعلومات هذا داخل خادمك. +* لا تحمل النتيجة إلا الإجراء. يعني `"accept"` أن المستخدم وافق على فتح URL، **ولا** يعني أنه أكمل ما يوجد فيه. +* يحدث الدفع خارج قناة البروتوكول، بين متصفح المستخدم ومزوّد الدفع لديك. لا يعود أي محتوى عبر MCP. + +انظر إلى الأداة الثانية. عندما يعرف خادمك اكتمال التدفق الخارجي (عبر webhook أو استطلاع؛ ويُمثَّل هنا بأداة ثانية)، ترسل `ctx.session.send_elicit_complete(...)` إشعار `notifications/elicitation/complete` مع `elicitation_id` نفسه. هكذا يعرف العميل أنه يستطيع إيقاف عرض *"بانتظار الدفع..."*. وبدونه لا يستطيع إلا التخمين. + +## جانب العميل {#the-client-side} + +تسأل الخوادم. ويجيب العملاء بتمرير **`elicitation_callback`** إلى `Client(...)`: + +```python title="client.py" hl_lines="6-7 18" +--8<-- "docs_src/elicitation/tutorial003.py" +``` + +* تعالج دالة رد نداء واحدة النمطين. `params` اتحاد `ElicitRequestFormParams` و`ElicitRequestURLParams`؛ ويُستخدم `isinstance` للتفرع. +* لعنوان URL، تعرض `params.url` للمستخدم وتعيد الإجراء الذي اختاره. لا تعيد أي `content` أبدًا. +* للنموذج، يعرض التطبيق الفعلي `params.requested_schema` ويعيد مدخلات المستخدم كـ`content`. يجيب هذا المثال دائمًا بنعم وإجابة ثابتة، وهي دالة رد النداء المناسبة للاختبار. +* تمرير دالة رد النداء هو أيضًا **إعلان القدرة**: هكذا يعرف الخادم أنه يستطيع سؤال هذا العميل. وتشرح **[دوال رد النداء لدى العميل](../client/callbacks.md)** الأمور الأخرى التي يستطيع العميل الإجابة عنها للخادم. + +!!! info + استقاء المعلومات طلب من *الخادم* إلى *العميل*، ولا توجد هذه الطلبات إلا في + جلسة ذات مصافحة تقليدية، لذلك يمرّر هذا العميل `mode="legacy"`. + على اتصال **2026-07-28**، تسأل الأداة بدلًا من ذلك عبر *إعادة* السؤال من الاستدعاء؛ + وهذا تدفق **[الطلبات متعددة جولات التبادل](multi-round-trip.md)**. + +### جرّبه {#try-it} + +شغّل `server.py` الخاص بنمط النموذج لـ`ctx.elicit` (الذي يضم `book_table`) عبر Streamable HTTP (تقدم **[تشغيل خادمك](../run/index.md)** الأمر في سطر واحد)، ثم شغّل `main()` للعميل واطلب من `book_table` الحجز في يوم عيد الميلاد. + +تطبع دالة رد النداء السؤال الذي تلقّته: + +```text +No tables for 2 on 2025-12-25. Would you like to try another date? +``` + +تجيب بـ`{"accept_alternative": True, "date": "2025-12-27"}`، وتكمل الأداة الحجز بعد انتظارها طوال هذا الوقت داخل `await ctx.elicit(...)`: + +```text +Booked a table for 2 on 2025-12-27. +``` + +استبدل الآن `server.py` بنسخة نمط URL ووجّه `main()` نفسها إلى `pay_deposit`: تأخذ دالة رد النداء الفرع الآخر، وتطبع رابط الدفع، وتعود الأداة بـ*"أكمل الدفع في متصفحك."*. جولة تبادل واحدة أثناء الاستدعاء، في الاتجاهين. + +!!! check + احذف الآن `elicitation_callback=` من `Client` واستدعِ `book_table` ليوم عيد الميلاد + مجددًا. يفشل الاستدعاء كله بخطأ بروتوكول: + + ```text + Elicitation not supported + ``` + + العميل الذي لم يسجّل دالة رد نداء لم يعلن قدرة `elicitation`، لذلك لا يوجد + من تسأله. لم تتلقَ أداتك `"decline"`، بل استثناءً. صمّم لهذا الاحتمال: يحتاج كل + طلب استقاء معلومات إلى جواب مناسب عن "ماذا لو تعذّر عليّ السؤال؟". + +## مراجعة {#recap} + +* تُملأ مَعلمة ذات التعليق `Annotated[T, Resolve(fn)]` بدالة حل تعيد `Elicit(...)` عند الحاجة إلى سؤال. تعمل على كل اتصال. +* المخطط نموذج Pydantic مسطح: حقول أولية فقط، ويُتحقَّق من الإجابة عند عودتها. +* تكون `result.action` هي `"accept"` أو `"decline"` أو `"cancel"`؛ ولا توجد `result.data` إلا عند القبول. +* تسأل `await ctx.elicit(message, schema=Model)` من داخل جسم الأداة، وتُستخدم `await ctx.elicit_url(message, url, elicitation_id)` لكل ما يجب ألّا يمر عبر النموذج (وتعلن `ctx.session.send_elicit_complete(elicitation_id)` اكتمال الجزء الخارجي). كلاهما طلب من الخادم إلى العميل، ويتطلبان اتصالًا قديمًا لدى العميل. +* يجيب العميل بدالة `elicitation_callback` واحدة تتفرع بحسب نوع المَعلمات؛ وتسجيلها هو ما يعلن القدرة. +* على اتصال 2026-07-28، يعيد الخادم السؤال بدلًا من دفعه؛ وتُمرَّر الأسئلة إلى دالة رد النداء نفسها عبر **[الطلبات متعددة جولات التبادل](multi-round-trip.md)**. + +كل ما وراء تلك الإعادة (حلقة إعادة المحاولة وحماية `requestState` وإدارة التدفق بنفسك) في **[الطلبات متعددة جولات التبادل](multi-round-trip.md)**. diff --git a/i18n/ar/pages/handlers/index.md b/i18n/ar/pages/handlers/index.md new file mode 100644 index 0000000000..ac606945e7 --- /dev/null +++ b/i18n/ar/pages/handlers/index.md @@ -0,0 +1,38 @@ +--- +translation: + sections: [22ca41e50cc1b536] + tool: 1 +--- +# داخل دالة المعالجة {#inside-your-handler} + +تأتي وسائط دالة المعالجة من العميل. وكل ما *عدا ذلك* مما تستطيع قراءته، +وكل ما تستطيع فعله أثناء عملها، موجود هنا. + +ما تستطيع قراءته: + +* **[السياق](context.md)** هو المَعلمة الإضافية التي تستطيع أي دالة معالجة + طلبها: الطلب الحالي وترويساته وجلسته وعمليات الإبلاغ عن التقدم + وإرسال إشعارات التغيير. +* **[الاعتماديات](dependencies.md)** مَعلمات لا يراها النموذج أبدًا، + تملؤها دوالك باستخدام `Resolve`. +* تغطي **[دورة الحياة](lifespan.md)** الحالة التي يبنيها خادمك مرة واحدة عند + بدء التشغيل، وكيف تصل إليها دالة المعالجة عبر `Context`. + +ما تستطيع فعله أثناء عملها: + +* طلب مزيد من المدخلات من المستخدم باستخدام **[استقاء المعلومات](elicitation.md)**، و + **[الطلبات متعددة جولات التبادل](multi-round-trip.md)**، وهي نمط + 2026-07-28 الذي يحملها. +* طلب استكمال يولّده LLM أو مجلدات مساحة العمل من العميل باستخدام + **[أخذ العينات والمجلدات الجذرية](sampling-and-roots.md)**، وهي ميزات مهجورة لكنها ما زالت + مدعومة. +* الإبلاغ عن **[التقدم](progress.md)** في عملية بطيئة. +* تنظيف الموارد أو التوقف مبكرًا عندما يتخلى العميل عن الاستدعاء، باستخدام + **[الإلغاء](cancellation.md)**. +* كتابة السجلات (إلى الخطأ القياسي، لمن يشغّل الخادم) باستخدام + **[التسجيل](logging.md)**. +* إبلاغ العملاء المشتركين بأن شيئًا تغيّر باستخدام + **[الاشتراكات](subscriptions.md)**. + +إذا لم تسجّل دالة معالجة بعد، فابدأ بـ +**[الأدوات](../servers/tools.md)**. تفترض كل صفحة هنا أن لديك واحدة. diff --git a/i18n/ar/pages/handlers/lifespan.md b/i18n/ar/pages/handlers/lifespan.md new file mode 100644 index 0000000000..f31a3f2b8a --- /dev/null +++ b/i18n/ar/pages/handlers/lifespan.md @@ -0,0 +1,94 @@ +--- +translation: + sections: [f3ca8ac5f90f2dfa, 48e478ef7bd688b1, 563346d4d5804933, 52ac6a7734d6f581] + tool: 1 +--- +# دورة الحياة {#lifespan} + +تحتفظ معظم الخوادم الفعلية بشيء طوال عملها: مجموعة اتصالات قاعدة بيانات، أو عميل HTTP، أو نموذج محمّل. + +لا تريد بناءه مع كل استدعاء، وتريد إغلاقه بصورة سليمة. هذا دور **دورة الحياة**. + +## دورة حياة محددة النوع {#a-typed-lifespan} + +دورة الحياة دالة `@asynccontextmanager` تتلقى الخادم وتنتج عبر `yield` **كائنًا واحدًا**. كل ما تنتجه متاح لكل دالة معالجة طوال عمل الخادم. + +```python title="server.py" hl_lines="25-31 34 38 40" +--8<-- "docs_src/lifespan/tutorial001.py" +``` + +اقرأ من الأسفل إلى الأعلى: + +* تتصل `app_lifespan` بـ`Database` **قبل** `yield` وتفصل الاتصال **بعده** داخل `finally`. هذان بدء التشغيل والإغلاق. +* تنتج `AppContext`، وهي فئة بيانات عادية تحمل ما أعددته. حقل واحد اليوم، وعشرة غدًا. +* `MCPServer("Bookshop", lifespan=app_lifespan)` هو الربط بالكامل. +* داخل الأداة، يكون الكائن الناتج في `ctx.request_context.lifespan_context`. + +تعمل دورة الحياة **مرة واحدة**. تبدأ عند تشغيل الخادم (قبل أول طلب) وتنتهي عند إيقافه. تشترك جميع الطلبات بينهما في `AppContext` نفسها. + +!!! info + إذا كتبت `lifespan` في FastAPI، فأنت تعرف ذلك بالفعل. المزخرف نفسه و`yield` نفسها و`finally` نفسها. + +### ما يراه النموذج {#what-the-model-sees} + +لا شيء جديد. `ctx` مَعلمة **Context**، لذا تحقنها SDK ولا تصل أبدًا إلى مخطط المدخلات: + +```json +{ + "type": "object", + "properties": { + "genre": {"title": "Genre", "type": "string"} + }, + "required": ["genre"], + "title": "count_booksArguments" +} +``` + +`genre` الوسيطة الوحيدة التي يستطيع النموذج تمريرها. دورة الحياة شأن خادمك. + +تستطيع دوال `@mcp.resource()` و`@mcp.prompt()` أخذ مَعلمة `ctx` أيضًا. كل ما تحمله `ctx` في **[السياق](context.md)**. + +### النوع محدد فعلًا {#it-really-is-typed} + +انظر إلى التعليق مجددًا: `ctx: Context[AppContext]`. + +مَعلمة النوع هذه هي ما يجعل `ctx.request_context.lifespan_context` **كائنًا من نوع** `AppContext` لدى فاحص الأنواع. تُكمَّل `.db` تلقائيًا؛ وتكون `.dbb` خطأ قبل تشغيل الخادم. + +اكتب `Context` مجردة بدلًا من ذلك، فيصبح نوع `lifespan_context` هو `dict[str, Any]`: لا يستطيع فاحص الأنواع معرفة ما أنتجته دورة الحياة. يبقى الكائن موجودًا أثناء التشغيل؛ لكنك تفقد المساعدة. + +!!! tip + توجد دورة حياة دائمًا. إذا لم تمرّر واحدة، تنتج دورة SDK الافتراضية `dict` فارغة، + لذلك تكون `ctx.request_context.lifespan_context` هي `{}`، ولا تكون `None` أبدًا. وهذا الافتراضي سبب + تحديد نوعها بـ`dict[str, Any]` عند استخدام `Context` مجردة. + +## شاهد ما يحدث {#watch-it-happen} + +"يعمل بدء التشغيل قبل أول طلب" عبارة لا ينبغي أن تحتاج إلى قبولها دون تحقق. + +اختصر الخادم إلى دورة حياته: أضف إلى `Database` مؤشر `connected`، وغيّره في `connect()` و`disconnect()`، وأضف أداة تعرضه. + +```python title="server.py" hl_lines="11 14 17 25 44" +--8<-- "docs_src/lifespan/tutorial002.py" +``` + +توجد `database` على مستوى الوحدة لسبب واحد: كي تستطيع فحصها من *خارج* الخادم. + +!!! check + ثلاث لحظات وثلاث قيم: + + * قبل بدء الخادم، تكون `database.connected` هي `False`. لم يُنشئ استيراد الوحدة أي اتصال. + * أثناء عمله، استدعِ `database_status` فتكون النتيجة `"connected"`. + * أوقف الخادم فتعمل كتلة `finally`: تصبح `database.connected` هي `False` مجددًا. + + حدث العمل في المكان الذي وضعته فيه بالضبط: حول `yield`، لا عند الاستيراد ولا لكل طلب. + +## مراجعة {#recap} + +* تأخذ `lifespan=` دالة `@asynccontextmanager` تتلقى الخادم وتنتج كائنًا واحدًا عبر `yield`. +* الشيفرة قبل `yield` لبدء التشغيل. و`finally` بعدها للإغلاق. +* تعمل مرة واحدة حول حياة الخادم كاملة، لا لكل طلب. +* كل ما تنتجه عبر `yield` هو `ctx.request_context.lifespan_context` في كل أداة ومورد وقالب توجيه. +* تجعل `ctx: Context[AppContext]` هذا الوصول محدد النوع بالكامل. +* عدم تحديد `lifespan=` يعني `dict` فارغة، ولا يعني `None` أبدًا. + +دالة المعالجة التي تتوقف أثناء الاستدعاء لسؤال المستخدم عن شيء لا يعرفه سواه موضوع **[استقاء المعلومات](elicitation.md)**. diff --git a/i18n/ar/pages/handlers/logging.md b/i18n/ar/pages/handlers/logging.md new file mode 100644 index 0000000000..3f6a91268c --- /dev/null +++ b/i18n/ar/pages/handlers/logging.md @@ -0,0 +1,88 @@ +--- +translation: + sections: [c93a3e1aefd77955, 7851abd5ec54393b, f49d1ca2f330f9cd, 4cc0a00347c3f534, 4a0391691a674ae4, 2df5cd279eabf9f5] + tool: 1 +--- +# التسجيل {#logging} + +سجّل من الأداة كما تسجّل من أي دالة Python أخرى: باستخدام المكتبة القياسية. + +يملك MCP **قدرة تسجيل** على مستوى البروتوكول: كان يمكن للخادم دفع رسائل سجله إلى العميل كإشعارات، عبر طرق كائن `Context`. **تهجر مراجعة المواصفة 2026-07-28 هذه القدرة ولا تستبدلها**، لذلك لا يشرحها هذا التوثيق. قائمة الميزات المهجورة وما ينبغي فعله بدلًا منها في **[الميزات المهجورة](../deprecated.md)**. + +البديل هو ما تستخدمه في كل برنامج Python آخر: المكتبة القياسية. + +## أداة تكتب سجلات {#a-tool-that-logs} + +```python title="server.py" hl_lines="1 5 13" +--8<-- "docs_src/logging/tutorial001.py" +``` + +* تمنحك `logging.getLogger(__name__)` مسجّلًا باسم وحدتك. أنشئه مرة واحدة في الأعلى. +* داخل الأداة، تستدعي `logger.info(...)` كما في أي دالة أخرى. لا حقن ولا `await` ولا شيء خاص بـMCP. + +!!! check + استدعِ الأداة وانظر إلى النتيجة كاملة: + + ```python + result.content # [TextContent(text="Found 3 books matching 'dune'.")] + result.structured_content # {'result': "Found 3 books matching 'dune'."} + ``` + + لا يوجد سطر السجل فيها. التسجيل **لك**، بوصفك مشغّل الخادم. لا يراه + النموذج أبدًا. إذا كان ينبغي أن يقرأ النموذج شيئًا، فأعِده باستخدام `return`. + +## أين تذهب السجلات؟ {#where-it-goes} + +لخادم **stdio**، تهم هذه المسألة أكثر من المعتاد. شغّل المضيف خادمك كعملية فرعية ويقرأ رسائل MCP من **stdout** الخاص به. أما الخطأ القياسي فهو لك. + +تفعل المكتبة القياسية المطلوب بالفعل: تذهب مخرجات السجل إلى `sys.stderr` افتراضيًا. تصل أسطر `logger.info(...)` إلى الطرفية (أو حيث يجمع المضيف stderr للعملية الفرعية)، ويبقى تدفّق البروتوكول سليمًا. + +!!! tip + لا تستخدم `print()` في خادم stdio. تكتب `print` إلى **stdout**، وهو خاص بالبروتوكول. + أثناء الخدمة، تحوّل SDK مخرجات stdout التي *تُفرَّغ فعليًا* إلى stderr كي لا تفسد + النقل، لكن `print()` في عملية تستخدم تخزينًا مؤقتًا على مستوى الكتل تبقى عادة في مخزن `sys.stdout` + دون تفريغ حتى يفرّغه المفسّر عند الخروج، مباشرة إلى تدفّق البروتوكول. وحتى عند تحويلها، + يصل السطر خامًا بين مخرجات السجل دون مستوى أو اسم مسجّل أو وسيلة لترشيحه. + + تتطلب `logger.debug("got here")` السطر الواحد نفسه وتذهب إلى المكان الصحيح. + +## المستوى {#the-level} + +لا تحتاج إلى استدعاء `logging.basicConfig()` بنفسك. فعل إنشاء `MCPServer` ذلك بالفعل، بدالة معالجة موجّهة إلى الخطأ القياسي، وبالمستوى الذي تمرّره في `log_level=`، لذا تكفي `MCPServer("Bookshop", log_level="DEBUG")` لرؤية أسطر `logger.debug(...)`. + +الافتراضي هو `"INFO"`. + +لا تستبدل `logging.basicConfig()` دوال معالجة موجودة. إذا أعددت التسجيل قبل إنشاء الخادم، تُعتمَد إعداداتك. + +لا تحتاج أيضًا إلى `try`/`except` في كل دالة معالجة لتسجيل الإخفاقات فقط. عندما تثير دالة أداة أو مورد استثناءً، تسجّله SDK نيابة عنك. تشرح **[معالجة الأخطاء](../servers/handling-errors.md#any-other-exception)** ما يُسجَّل وبأي مستوى. + +## جرّبه {#try-it} + +شغّل الخادم باستخدام MCP Inspector: + +```console +uv run mcp dev server.py +``` + +استدعِ `search_books` من تبويب **Tools**. يعرض Inspector النتيجة: القيمة المعادة فقط. ذهب السطر + +```text +Searching for 'dune' +``` + +إلى الخطأ القياسي: الطرفية، لا البيانات المنقولة. + +!!! info + إذا كنت تريد *التتبّع* فعلًا (كل طلب ومدته وما إذا فشل)، + فأنت تحتاج إلى مقاطع تتبّع، لا أسطر سجل. يصدرها خادمك بالفعل: تتتبّع SDK كل + رسالة باستخدام OpenTelemetry افتراضيًا. راجع **[OpenTelemetry](../run/opentelemetry.md)**. + +## مراجعة {#recap} + +* تهجر مواصفة 2026-07-28 قدرة التسجيل في بروتوكول MCP دون بديل. لا تبنِ عليها. +* استخدم `logger = logging.getLogger(__name__)` على مستوى الوحدة، و`logger.info(...)` في الأداة. هذا هو النمط كاملًا. +* لا تصل مخرجات السجل إلى النموذج أبدًا. لا تصله إلا القيمة التي تعيدها باستخدام `return`. +* الخطأ القياسي لك؛ وstdout للبروتوكول. تحوّل SDK مخرجات stdout العرضية المفرَّغة إلى stderr أثناء الخدمة، لكن `print()` غير المفرَّغة قد تخرج إلى النقل عند إغلاق العملية، وتصل الأسطر المحوّلة دون تسميات؛ استخدم `logging` التي تفرّغ دالة معالجتها كل سجل. +* تعيّن `MCPServer(..., log_level="DEBUG")` المستوى، وتبقى إعدادات التسجيل التي أنشأتها أولًا كما هي. + +إبلاغ العملاء المتصلين بتغيّر شيء في خادمك (قائمة الأدوات أو مورد) موضوع **[الاشتراكات](subscriptions.md)**. diff --git a/i18n/ar/pages/handlers/multi-round-trip.md b/i18n/ar/pages/handlers/multi-round-trip.md new file mode 100644 index 0000000000..15c4d67dc6 --- /dev/null +++ b/i18n/ar/pages/handlers/multi-round-trip.md @@ -0,0 +1,191 @@ +--- +translation: + sections: [74011e683045eea9, 9b64cc175c18b6a9, 4b41be4824030397, e3b1502da786ec33, 71e41161f143c6a9, 9ec2c1eeb8c36378, b47667184ca5b516, f81491125dcbfe8b] + tool: 1 +--- +# الطلبات متعددة جولات التبادل {#multi-round-trip-requests} + +قد لا تستطيع الأداة إنهاء عملها في جولة تبادل واحدة. تحتاج إلى شيء لا يملكه إلا المستخدم: اختيار أو تأكيد أو بيانات اعتماد. + +قبل 2026-07-28، كان الخادم يحصل عليه باستدعاء **عكسي**: يفتح طلبًا خاصًا به إلى العميل (استقاء معلومات أو أخذ عينات) أثناء معالجة الطلب الأصلي. تُلغي مواصفة 2026-07-28 هذه القناة العكسية (back-channel). + +بدلًا من ذلك، **يعيد** الخادم نتيجة. + +## أعِد نتيجة بدلًا من الاستدعاء العكسي {#return-dont-call-back} + +يجيب الخادم عن `tools/call` بـ**`InputRequiredResult`** بدلًا من `CallToolResult`. يتولى حقلان فيها العمل: + +* **`input_requests`**: ما لا يزال الخادم يحتاج إليه، كقاموس بمفاتيح اختار الخادم أسماءها. كل قيمة `ElicitRequest` أو `CreateMessageRequest` أو `ListRootsRequest`. +* **`request_state`**: رمز غير قابل للتفسير لدى العميل. يعيده العميل حرفيًا عند إعادة المحاولة. خادمك وحده من يقرؤه. + +يلبّي العميل كل طلب، ثم يستدعي **الأداة نفسها مجددًا**، حاملًا إجاباته في `input_responses` والرمز في `request_state`. أصبح لدى الخادم ما يفتقده، فيعيد `CallToolResult` عادية. + +هذا هو البروتوكول كاملًا. كل مرحلة طلب عادي من العميل إلى الخادم. لا تسير الطلبات في الاتجاه الآخر أبدًا. + +## جانب الخادم {#the-server-side} + +مع `@mcp.tool()`، نادرًا ما تبني ذلك يدويًا: أعلن اعتمادية تسأل المستخدم (`Elicit`)، أو تطلب توليدًا من LLM لدى العميل (`Sample`)، أو تعرض مجلداته الجذرية (`ListRoots`)، فتعيد SDK كائن `InputRequiredResult` نيابة عنك؛ هذا الشكل في صفحة **[الاعتماديات](dependencies.md)**. لا يمكن خلط الشكلين: للاستدعاء قناة `input_responses`/`request_state` واحدة، لذا لا تستطيع أداة تستخدم مَعلمات `Resolve(...)` إعادة `InputRequiredResult` من جسمها أيضًا. يُرفض إعلان نوع إرجاع `InputRequiredResult` عند التسجيل (`InvalidSignature`)، ويفشل النوع غير المعلن الاستدعاء أثناء التشغيل. الشكل اليدوي هو `Server` **منخفض المستوى**، الذي يجوز لدالة معالجته `on_call_tool` إعادة أي من نوعَي النتيجة: + +```python title="server.py" hl_lines="43-46" +--8<-- "docs_src/mrtr/tutorial001.py" +``` + +* نوع `on_call_tool` هو `-> CallToolResult | InputRequiredResult`. إعادة الثاني هي API جانب الخادم كاملة. +* في الاستدعاء الأول، تكون `params.input_responses` هي `None`، فيعمل شرط الحماية وتطلب دالة المعالجة المدخلات بدلًا من الإجابة. +* عند إعادة المحاولة، يوجد `ElicitResult` الذي أرسله العميل تحت **المفتاح نفسه** (`"region"`) الذي استخدمه الخادم في `input_requests`. + +كل ما عدا ذلك في الملف (`input_schema` الصريح و`CallToolResult` المبنية يدويًا) هو `Server` منخفض المستوى المعتاد، الذي تغطيه **[الخادم منخفض المستوى](../advanced/low-level-server.md)**. تضيف هذه الصفحة نوع الإرجاع الثاني فقط. + +## أبعد من الأدوات {#beyond-tools} + +لا يتميز `tools/call` هنا: في 2026-07-28، يستطيع الخادم الإجابة عن `prompts/get` و`resources/read` بالطريقة نفسها. على `MCPServer`، تعيد دالة `@mcp.prompt()` أو دالة **قالب** `@mcp.resource()` كائن `InputRequiredResult` بنفسها وتقرأ إجابات إعادة المحاولة من السياق: + +```python title="server.py" hl_lines="20 22 24" +--8<-- "docs_src/mrtr/tutorial004.py" +``` + +* تعيد الجولة الأولى `InputRequiredResult`. وعند إعادة المحاولة، تحمل `ctx.input_responses` الإجابات تحت المفاتيح نفسها وتعيد الدالة نتيجتها المعتادة: رسائل قالب التوجيه هنا، أو محتوى المورد لقالب المورد. +* تُغلَّف `request_state` التي تعيّنها بحماية تشفيرية قبل النقل، ويُتحقَّق منها عند إعادتها، مثل كل حالة أخرى على الخادم؛ يشرح قسم **[حماية `requestState`](#protecting-requeststate)** أدناه ما توفره الحماية ومتى تحتاج إلى إعداد المفاتيح. +* تستطيع دالة `@mcp.tool()` إعادة النتيجة مباشرة بالطريقة نفسها، عندما لا يناسبك شكل الاعتماديات. +* لا تشارك دوال `@mcp.resource()` الثابتة: فهي لا تأخذ `Context`، فلا تستطيع قراءة إعادة المحاولة. قوالب الموارد فقط تستطيع السؤال. +* تنطبق قواعد أجيال البروتوكول أدناه دون تغيير: إعادة `InputRequiredResult` على جلسة أقدم من 2026 تؤدي إلى `-32603` نفسه الموضح في التحذير. + +## جانب العميل {#the-client-side} + +تشغّل `Client` الحلقة نيابة عنك. + +سجّل دوال رد النداء التي قد يحتاج إليها الخادم (`elicitation_callback` و`sampling_callback` و`list_roots_callback`) واستدعِ الأداة. عندما تصل `InputRequiredResult`، توجّه `Client` كل إدخال في `input_requests` إلى دالة رد النداء المناسبة، وتعيد المحاولة بالإجابات و`request_state` كما وردت، وتواصل حتى تصل `CallToolResult`: + +```python title="client.py" hl_lines="11 12" +--8<-- "docs_src/mrtr/tutorial003.py" +``` + +* `elicitation_callback` هي نفسها التي يستدعيها `elicitation/create` على القناة العكسية لخادم أقدم من 2026. وينطبق الأمر على `sampling_callback` لـ`sampling/createMessage` و`list_roots_callback` لـ`roots/list`: في 2026-07-28 تزول RPC المستقلة من الخادم إلى العميل، لكن حمولات `ElicitRequest` / `CreateMessageRequest` / `ListRootsRequest` نفسها تُحمَل داخل `input_requests` وتُوجَّه إلى دوال رد النداء الثلاث نفسها. مجموعة واحدة تخدم الجيلين. +* تعيد `call_tool` كائن `CallToolResult` عاديًا. لا يرى المستدعي الجولات الوسيطة. +* تدير `get_prompt` و`read_resource` الحلقة نفسها. + +!!! check + إذا لم تسجّل دالة رد النداء، تفشل الحلقة في الجولة الأولى: تجيب الدالة البديلة في SDK + عن كل طلب استقاء معلومات بخطأ، وتثير `call_tool` الاستثناء `MCPError` بالرسالة + *"استقاء المعلومات غير مدعوم"*. + +الحلقة محدودة. `Client(..., input_required_max_rounds=10)` هو الحد الافتراضي؛ إذا استمر الخادم في إعادة `InputRequiredResult` بعده، تثير `call_tool` استثناءً. إذا حملت الجولة `request_state` فقط دون `input_requests`، تنتظر `Client` قليلًا (50ms، وتتضاعف حتى سقف 250ms) قبل إعادة المحاولة، فلا يُستطلَع خادم يقول فقط *"لم أنتهِ بعد"* باستمرار دون انتظار. + +### إدارة الحلقة بنفسك {#driving-the-loop-yourself} + +تكفي الحلقة التلقائية لعميل في عملية واحدة. تولَّ إدارتها عندما: + +* يكون عميلك **موزعًا**: العملية التي تعرض السؤال للمستخدم ليست التي استدعت `call_tool`، لذلك يُصدر عامل آخر إعادة المحاولة. `request_state` هو الرمز القابل للحفظ الذي تنقله عبر هذا الحد باستخدام تخزينك، و`input_responses` ما يعيده الطرف الآخر معه. +* تريد **فحص** كل جولة: تسجيل كل إدخال `input_requests` أو تدقيقه، أو رفض أنواع معينة من الطلبات، أو تطبيق تأخيرك التدريجي بين المراحل. +* تريد حدًا لـ**الوقت الفعلي** بدلًا من عدد الجولات: غلّف حلقتك في `anyio.fail_after(...)` بدلًا من الاعتماد على `input_required_max_rounds`. + +انتقل إلى الجلسة الأساسية، حيث تمنحك `allow_input_required=True` اتحاد الأنواع مباشرة: + +```python title="client.py" hl_lines="12 13 19" +--8<-- "docs_src/mrtr/tutorial002.py" +``` + +* توسّع `client.session.call_tool(..., allow_input_required=True)` نوع الإرجاع إلى `CallToolResult | InputRequiredResult`. ويضيّقه `isinstance` مجددًا. +* أصبحت `request_state` مسؤوليتك. احفظها بين المراحل لتُستأنَف المحادثة من عملية جديدة. +* لكل إدخال في `input_requests`، ضع `InputResponse` تحت **المفتاح نفسه** في `input_responses`. مكان واجهتك هو `fulfil`؛ ويثبت هذا المثال الإجابة في الشيفرة. +* اسم الأداة نفسه و`arguments` نفسها في كل مرحلة. إعادة المحاولة هي تنفيذ الاستدعاء الأصلي مجددًا، وليست طريقة جديدة. + +## حماية `requestState` {#protecting-requeststate} + +يتعامل كل ما سبق مع `request_state` كقيمة تُعاد كما وردت، وهذا كل دورها في النقل. لكن العميل يحتفظ بها بين المراحل (وحفظها عبر العمليات هو ما أقرّه القسم السابق)، لذا فإن ما يعود **مدخلات يقدّمها العميل**: قد تُعدَّل أو تنتهي صلاحيتها أو تُؤخذ من استدعاء مختلف تمامًا. تلزم المواصفة الخوادم بحماية سلامة هذه الحالة ورفض الجولة عند فشل التحقق، متى أمكن للحالة التأثير في التفويض أو الوصول إلى الموارد أو منطق العمل. + +تحميها `MCPServer` افتراضيًا. يغلّف كل خادم `requestState` الخارجة بحماية تشفيرية ويتحقق من كل نسخة مُعادة، سواء حالة دالة حل أو حالة مبنية يدويًا، باستخدام مفتاح مولّد عند بدء العملية. لا تضبط شيئًا، وتكتب نصًا صريحًا وتقرأه؛ ولا يُنقل إلا رمز مشفّر غير قابل للتفسير لدى العميل. + +يعيش المفتاح الافتراضي ويموت مع العملية، وهذه المعلومة التي يجب معرفتها قبل النشر بما يتجاوز عملية واحدة: + +```python +from mcp.server.mcpserver import MCPServer, RequestStateSecurity + +# Multi-instance or restart-surviving: one or more shared secret keys (>= 32 bytes each). +mcp = MCPServer("fleet", request_state_security=RequestStateSecurity(keys=[key])) +``` + +* **الافتراضي (دون إعداد)** مناسب لعملية واحدة: stdio أو عامل HTTP واحد بالضبط. إذا وصلت إعادة المحاولة إلى عامل آخر أو نسخة أخرى خلف موازن حمل أو الخادم نفسه بعد إعادة التشغيل، فهي محمية بمفتاح لا تملكه تلك العملية؛ يتلقى العميل الرفض الثابت أدناه ويجب أن يبدأ التدفق من جديد. +* **`keys=[...]`** مطلوبة متى أمكن لإعادة المحاولة الوصول إلى **نسخة أخرى** (`uvicorn` متعدد العمال أو HTTP خلف موازن حمل)، أو لزم بقاؤها بعد إعادة التشغيل: تتحقق كل نسخة مما أصدرته أي نسخة أخرى. الآلية نفسها، لكن بسرّك بدلًا من مفتاح مولّد. +* لتشفيرك الخاص، مثل KMS أو خدمة رموز موجودة، مرّر `RequestStateSecurity(codec=...)` بدلًا من `keys`؛ يشرح **[استخدام تشفيرك الخاص](#bring-your-own-crypto)** أدناه العقد. + +### ما تحمله الحماية {#what-the-seal-carries} + +سواء بالإعداد الافتراضي أو المخصص، تكون `requestState` أثناء النقل رمزًا مشفّرًا ومصادَقًا عليه. لا تراه شيفرتك: تكتب دوال المعالجة والحل نصًا صريحًا وتقرأه (`ctx.request_state`)؛ وتحميه SDK عند الخروج وتتحقق منه عند الدخول. إلى جانب السلامة، يرتبط كل رمز بما يلي: + +* **نافذة زمنية.** تعيد كل جولة الحماية بوقت انتهاء جديد، لذلك تحد `RequestStateSecurity(ttl=...)` (600 ثانية افتراضيًا) وقت التفكير لكل جولة، لا التدفق كله. +* **الهوية المصادَق عليها.** عندما يحمل الطلب رمز وصول OAuth تحققت منه SDK، ترتبط الحالة بعميل الرمز ومُصدِره وصاحبه: تفشل حالة صدرت لمستخدم عند استخدامها تحت مستخدم آخر، حتى إذا اشتركا في عميل OAuth واحد. إذا لم يوفّر المتحقق هوية صاحب الرمز، يضعف الربط إلى هوية العميل فقط، وهي مشتركة بين جميع مستخدمي برنامج العميل عند استخدام معرّفات عملاء قائمة على URL. عندما تنتهي المصادقة خارج SDK (وكيل أمامي)، أو تكون وسيلة النقل دون مصادقة، لا توجد هوية للربط ولا يؤثر الفحص، ما لم توفر `RequestStateSecurity(bind_principal=...)` هوية من إشارة الهوية الخاصة بك. يجب أن يقدّم متحقق الرموز أي مكونات يوفرها باتساق: إدراج صاحب الرمز في بعض الطلبات وحذفه في أخرى يغيّر الهوية أثناء التدفق، فتُرفض الجولات الجارية. +* **الطلب الأصلي.** الطريقة واسم الأداة أو قالب التوجيه (أو URI المورد)، وبصمة الوسائط. يفشل رمز يُعاد استخدامه مع أداة أو وسائط أو طريقة مختلفة. +* **السؤال المطروح بالضبط.** ترتبط كل إجابة لدالة حل بالسؤال المعروض للعميل، في الجولة التي تصل فيها أولًا وعند إعادة استخدام إجابة مسجلة لاحقًا. أعد النشر برسالة أعيدت صياغتها أو مخطط تغيّر، فيعيد الخادم السؤال بدلًا من استهلاك إجابة قديمة. ويقتضي الربط نفسه اشتقاق الرسائل من وسائط الأداة، لا من بيانات لكل استدعاء. تختلف رسالة مبنية على طابع زمني أو سعر متغير في كل جولة، فتبدو كل إجابة مسجلة قديمة ويعيد الخادم السؤال حتى ينهي حد جولات العميل الاستدعاء. + +كل ذلك مسؤولية SDK، لا مسؤوليتك ولا مسؤولية المرمّز إذا استخدمت واحدًا خاصًا. + +### تدوير المفاتيح {#rotating-keys} + +يحمي `keys[0]` الحالة الجديدة؛ ويتحقق كل مفتاح في القائمة. تدوير المفاتيح دون توقف ثلاث مراحل، تُعمَّم كل منها بالكامل قبل التالية: + +```python +RequestStateSecurity(keys=[OLD, NEW]) # 1: every instance learns to verify NEW; OLD still mints +RequestStateSecurity(keys=[NEW, OLD]) # 2: NEW mints; in-flight OLD state keeps verifying +RequestStateSecurity(keys=[NEW]) # 3: one ttl after phase 2 is fully out, retire OLD +``` + +لا تجعل مفتاح الإصدار الجديد أولًا قبل التحقق: إصدار الرموز بمفتاح لا تستطيع بعض النسخ التحقق منه يُسقط الجولات الجارية أثناء النشر. + +تخص المفاتيح خدمة واحدة. يحمل الغلاف المحمي أيضًا اسم الخادم كحقل الجمهور، لذلك يُرفض رمز صادر عن خدمة أخرى تشترك مصادفة في السر نفسه. لا يتميز الحقل إلا بقدر تميّز الاسم، لذلك يجب أن يملك الخادم ذو السياسة الصريحة اسمًا فعليًا أو يحدد `RequestStateSecurity(audience=...)`؛ ويثير الخادم دون اسم استثناءً عند الإنشاء. يخدم `audience=` أيضًا البنى المقصودة متعددة الخدمات التي يجب فيها أن تقبل خدمة حالة أصدرتها أخرى. (يُستثنى الافتراضي دون إعداد: لا يغادر مفتاحه العملية، فلا يضيف حقل الجمهور شيئًا.) + +### استخدام تشفيرك الخاص {#bring-your-own-crypto} + +تقبل `RequestStateSecurity(codec=...)` أي كائن يوفّر `seal(bytes) -> str` و`unseal(str) -> bytes`، ويثير `InvalidRequestState` لأي رمز لم يصدره. الشكل المعتاد تشفير مغلّف باستخدام KMS، حيث تفك مفتاح بيانات مرة عند بدء التشغيل وتُبقي تشفير كل رمز محليًا: + +```python title="server.py" hl_lines="12 26-27 34-35 38" +--8<-- "docs_src/mrtr/tutorial005.py" +``` + +TTL وربط الهوية وربط الطلب **ليست** مسؤولية المرمّز: تضيفها SDK إلى الحمولة قبل `seal` وتتحقق منها مجددًا بعد `unseal` لكل مرمّز. التزامات المرمّز الوحيدة هي السلامة (العبث يعني إثارة استثناء) والسرية إن أمكن. + +### عندما يفشل التحقق {#when-verification-fails} + +يحصل كل إخفاق وارد على الإجابة نفسها، سواء بسبب العبث أو انتهاء الصلاحية أو إعادة الاستخدام مع طلب أو هوية مختلفة أو الحماية بمفتاح لا يعرفه هذا الخادم: + +```json +{"code": -32602, "message": "Invalid or expired requestState"} +``` + +رسالة ثابتة واحدة لكل الأسباب، كي لا يكشف النقل أي فحص فشل؛ ويذهب السبب الفعلي إلى سجل الخادم. تُفحص كل `requestState` واردة على `tools/call` و`prompts/get` و`resources/read`، حتى إذا وصلت لدالة معالجة لا تصدر حالة أصلًا. الرفض الأكثر شيوعًا عمليًا ليس هجومًا، بل مفتاح افتراضي خاص بالعملية يتلقى إعادة محاولة من قبل إعادة التشغيل أو من نسخة أخرى؛ يعيد العميل التدفق، ويكون `keys=[...]` الحل عندما يهم ذلك. + +### الحالة المبنية يدويًا {#hand-built-state} + +تُحمى `request_state` التي تعيّنها بنفسك (عبر إعادة `InputRequiredResult` من دالة أداة أو قالب توجيه أو قالب مورد) ويُتحقَّق منها بالآلية نفسها لحالة دوال الحل، دون تغيير شيفرة: اكتب نصًا صريحًا واقرأه، وتنطبق جميع الروابط أعلاه. + +الشيء الوحيد الذي لا تستطيع SDK ربطه لك، حتى مع الإعداد، هو هوية السؤال: لا تعرف أيًّا من *أسئلتك* تخصه إجابة في حالتك. إذا خزّنت إجابات بمفاتيح أسئلة، فضمّن معرّف السؤال الخاص بك في الحالة وافحصه عند إعادة المحاولة. + +`Server` منخفض المستوى لا يوفّر هذه الحماية تلقائيًا: بخلاف `MCPServer`، لا تُحمى الحالة حتى تضيف الحد بنفسك، وتُنقل `request_state` كما كُتبت حتى تفعل. يعرض **[الخادم منخفض المستوى](../advanced/low-level-server.md#the-other-handlers)** تفعيلها بسطر واحد. + +## نتيجة 2026-07-28 {#a-2026-07-28-result} + +لا توجد `InputRequiredResult` إلا في إصدار البروتوكول **2026-07-28**. يكتشفها الوضع الافتراضي `mode="auto"` في `Client` على أي اتصال. بعد الاتصال، تخبرك `client.protocol_version` بالإصدار الذي حصلت عليه. + +!!! warning + لا توجد في جلسة أقدم من 2026 طريقة لتمثيل `InputRequiredResult`. أعِدها من دالة المعالجة على + اتصال `mode="legacy"` فلا يستطيع منفّذ الطلبات تسلسلها إلى الإصدار المتفاوض عليه؛ + ويتلقى العميل خطأ `-32603` برسالة *"أعادت دالة المعالجة نتيجة غير صالحة"*. يجب على خادم يخدم + الجيلين فحص `ctx.protocol_version` قبل استخدامها. + +!!! info + يعمل **استقاء المعلومات بنمط URL** بالآلية نفسها على اتصال 2026. الإدخال في + `input_requests` هو `ElicitRequest` بمَعلمات `ElicitRequestURLParams`؛ يكمل المستخدم + التدفق الخارجي ويعيد عميلك الاستدعاء. الحلقة نفسها دون API جديدة. + يشرح جانب الخادم عالي المستوى قسم **[استقاء المعلومات](elicitation.md)**. + +## مراجعة {#recap} + +* في 2026-07-28، **يعيد** الخادم الذي يحتاج إلى مدخلات أثناء الاستدعاء `InputRequiredResult`. ولا يفتح طلبًا إلى العميل أبدًا. +* `input_requests` ما يحتاج إليه. و`request_state` رمز استئناف غير قابل للتفسير لدى العميل لا يقرؤه إلا الخادم. +* تشغّل `Client` حلقة إعادة المحاولة نيابة عنك: سجّل `elicitation_callback` / `sampling_callback` / `list_roots_callback`، فتعيد `call_tool` كائن `CallToolResult` عاديًا. يحدها `input_required_max_rounds` (10 افتراضيًا). +* لفحص الجولات أو حفظها، استخدم `client.session.call_tool(..., allow_input_required=True)` وتولَّ حلقة `while isinstance(result, InputRequiredResult)` بنفسك. +* في `@mcp.tool()`، تنتج اعتمادية تسأل المستخدم هذه النتيجة نيابة عنك (**[الاعتماديات](dependencies.md)**)؛ و`Server` **منخفض المستوى** هو الشكل اليدوي. +* تشارك قوالب التوجيه والموارد أيضًا: تعيد دالة `@mcp.prompt()` أو قالب `@mcp.resource()` كائن `InputRequiredResult` بنفسها وتقرأ `ctx.input_responses` عند إعادة المحاولة. +* تعود `requestState` كمدخلات يقدّمها العميل، لذلك تحميها `MCPServer` افتراضيًا (حالة دوال الحل والحالة اليدوية) بمفتاح خاص بالعملية؛ وتمرّر عمليات النشر متعددة النسخ `RequestStateSecurity(keys=[...])` (أو مرمّزًا مخصصًا) كي تتحقق كل نسخة مما أصدرته الأخرى. تربط الحماية كل رمز بنافذة زمنية والطلب الأصلي والهوية المصادَق عليها عندما يحمل الطلب مصادقة تحققت منها SDK أو يوفر `bind_principal=` إشارة هويتك الخاصة (**[حماية `requestState`](#protecting-requeststate)**). + +هذه الآلية التي تحل محل أخذ العينات الذي يبدأه الخادم وبقية القناة العكسية بأسلوب الدفع؛ راجع **[الميزات المهجورة](../deprecated.md)**. diff --git a/i18n/ar/pages/handlers/progress.md b/i18n/ar/pages/handlers/progress.md new file mode 100644 index 0000000000..280c55749c --- /dev/null +++ b/i18n/ar/pages/handlers/progress.md @@ -0,0 +1,123 @@ +--- +translation: + sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 52d6009a07e770ea, 8534d8dbb4053a70, 2e9aff14d3a882c0] + tool: 1 +--- +# التقدم {#progress} + +تبدو الأداة التي تستغرق ثلاثين ثانية دون أن تقول شيئًا طوالها معطلة. + +تصلح **إشعارات التقدم** ذلك. تبلّغ الأداة عن مدى تقدمها؛ ويقرر العميل ما يعرضه: شريطًا أو مؤشر دوران أو سطر سجل. + +## أبلِغ عنه من الأداة {#report-it-from-the-tool} + +خذ مَعلمة **`Context`** واستدعِ `report_progress`: + +```python title="server.py" hl_lines="8 11" +--8<-- "docs_src/progress/tutorial001.py" +``` + +ثلاث وسائط، وأنت تقرر معناها: + +* `progress`: مقدار ما أنجزته. تشترط المواصفة أن **يزداد** مع كل تقرير؛ لا تكرر قيمة أو تتراجع أبدًا. +* `total`: المقدار الكلي، إذا عرفته. اختياري. +* `message`: سطر مقروء للبشر عن *هذه* الخطوة. اختياري. + +تُحقن `ctx` بسبب تلميح نوعها ولا يراها النموذج: يملك مخطط مدخلات `import_catalog` خاصية واحدة هي `urls`. تتناول صفحة **[السياق](context.md)** هذا الكائن؛ والتقدم أحد ما يوفّره. + +## استمع إليه من العميل {#listen-for-it-from-the-client} + +يختار العميل تلقي التقدم **لكل استدعاء** بتمرير `progress_callback=` إلى `call_tool`: + +```python title="client.py" hl_lines="5 14" +import anyio +from mcp import Client + + +async def show(progress: float, total: float | None, message: str | None) -> None: + print(f"{message} ({progress}/{total})") + + +async def main() -> None: + async with Client("http://localhost:8000/mcp") as client: + result = await client.call_tool( + "import_catalog", + {"urls": ["https://example.com/a.json", "https://example.com/b.json"]}, + progress_callback=show, + ) + print(result.structured_content) + + +anyio.run(main) +``` + +دالة رد النداء `async` وتتلقى بالضبط ما أبلغ عنه الخادم: `progress` و`total` و`message`. + +!!! info + `progress_callback` هي المَعلمة نفسها مهما مرّرت إلى `Client`: عنوان URL كما هنا، أو + `StdioServerParameters`، أو كائن الخادم في اختبار. لكن انتبه للتوقيت عبر وسيلة + نقل فعلية. يُسلَّم كل إشعار مستقلًا بجانب الاستجابة، لذلك قد تستمر دالة + رد نداء بطيئة بعد عودة `call_tool`. اتصال الاختبار داخل العملية فقط + ينفّذ الدالة مباشرة ويضمن وصول كل تقرير أولًا. + +### جرّبه {#try-it} + +أتح `server.py` عبر HTTP، ثم شغّل العميل من نافذة طرفية ثانية: + +```console +uv run mcp run server.py --transport streamable-http +``` + +```console +python client.py +``` + +```text +Imported https://example.com/a.json (1.0/2.0) +Imported https://example.com/b.json (2.0/2.0) +{'result': 'Imported 2 records.'} +``` + +أصبح كل `await ctx.report_progress(...)` على الخادم استدعاءً واحدًا لـ`show` على العميل بالترتيب. لا يُضم التقدم إلى النتيجة، بل يتدفق أثناء استمرار عمل الأداة. + +!!! warning + تخص `progress_callback` **الاستدعاء**، لا `Client`. لا توجد وسيطة لها في المُنشئ، + لأن الاستدعاءات المختلفة تحتاج إلى دوال مختلفة: واحدة تدير شريط تنزيل، وأخرى + سطر سجل. + +!!! check + احذف الآن `progress_callback=show` وشغّل مجددًا: + + ```text + {'result': 'Imported 2 records.'} + ``` + + لا خطأ ولا تحذير والنتيجة نفسها. **لا تفعل `report_progress` شيئًا إذا لم يطلب المستدعي + التقدم**، لذلك تُبلّغ دون شرط ولا تحتاج إلى التساؤل عمّا إذا كان أحد + يستمع. + +## عندما لا تعرف الإجمالي {#when-you-dont-know-the-total} + +`total` للحالات التي تعرف فيها الإجمالي. غالبًا لا تعرفه: تستهلك تدفق بيانات، أو تمر على مؤشر، أو تنزّل شيئًا دون ترويسة طول. + +احذفه: + +```python title="server.py" hl_lines="20" +--8<-- "docs_src/progress/tutorial002.py" +``` + +تتلقى دالة رد النداء `total=None`. يستطيع العميل إظهار *نشاط* ("استُوردت 3 حتى الآن...")، لكنه لا يستطيع عرض نسبة مئوية. لا تختلق إجماليًا لتحسين شكل الشريط. + +!!! tip + لا يلزم أن يعدّ `progress` شيئًا معينًا. بايتات أو صفوف أو صفحات: اختر الوحدة التي + يفهمها المستخدم، ولا تعد إلا بقيمة `total` تستطيع الوفاء بها. + +## مراجعة {#recap} + +* استخدم `await ctx.report_progress(progress, total=None, message=None)` من أي أداة تأخذ `Context`. +* يمرّر العميل `progress_callback=` إلى `call_tool`: لكل استدعاء، وليس إلى `Client` أبدًا. +* دالة رد النداء `async (progress, total, message) -> None` وتعمل أثناء استمرار تشغيل الأداة. +* غياب دالة رد نداء في الاستدعاء يعني أن `report_progress` لا تفعل شيئًا. أبلِغ دون شرط. +* احذف `total` إذا لم تعرفه؛ فتتلقى الدالة `None`. + +التقدم لعميل ما زال ينتظر. ما تراه أداتك عندما يتوقف العميل عن الانتظار موضوع **[الإلغاء](cancellation.md)**. diff --git a/i18n/ar/pages/handlers/sampling-and-roots.md b/i18n/ar/pages/handlers/sampling-and-roots.md new file mode 100644 index 0000000000..f24de86749 --- /dev/null +++ b/i18n/ar/pages/handlers/sampling-and-roots.md @@ -0,0 +1,51 @@ +--- +translation: + sections: [5c82b20cbd65ded0, 9dc22632be79a533, 1fb8f452e990c456, 42666ab914ff0cb1, c4e0cb3667fd5ff9] + tool: 1 +--- +# أخذ العينات والمجلدات الجذرية {#sampling-and-roots} + +تستطيع دالة المعالجة طلب شيئين إضافيين من العميل المتصل: استكمال من نموذج العميل نفسه (**أخذ العينات**، sampling)، ومجلدات مساحة عمل العميل (**المجلدات الجذرية**، roots). + +ما زالا يعملان على كل إصدار بروتوكول تدعمه SDK. لكن اقرأ التحذير قبل أن تبني تصميمك عليهما: + +!!! warning "مهجورتان بموجب مواصفة 2026-07-28" + أصبحت ميزتا أخذ العينات والمجلدات الجذرية مهجورتين منذ `2026-07-28` ([SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/2577)). تظلان تعملان بالكامل وتبقيان في المواصفة اثني عشر شهرًا على الأقل قبل أن يصبح حذفهما ممكنًا، لكن ينبغي ألّا تعتمد عليهما تطبيقات جديدة. الترحيلات المقترحة: التكامل مباشرة مع API مزوّد LLM بدلًا من أخذ العينات، وتمرير المجلدات عبر مَعلمات الأدوات أو عناوين URI للموارد أو إعدادات الخادم بدلًا من المجلدات الجذرية. قائمة SDK الكاملة في **[الميزات المهجورة](../deprecated.md)**. + +## أخذ العينات: استخدم نموذج العميل {#sampling-borrow-the-clients-model} + +تعيد دالة حل الاعتمادية `Sample(...)`، وتتلقى الأداة الاستكمال عبر آلية الاعتماديات نفسها التي تشغّل `Elicit` في **[الاعتماديات](dependencies.md)**: + +```python title="server.py" hl_lines="10-15 19" +--8<-- "docs_src/sampling_and_roots/tutorial001.py" +``` + +* تعكس `Sample(messages, max_tokens=...)` مَعلمات `sampling/createMessage`. القيمة المحقونة هي `CreateMessageResult` لدى العميل؛ وإذا مرّرت `tools` أو `tool_choice`، تصبح `CreateMessageResultWithTools` بدلًا منها. +* يجب أن يكون العميل قد أعلن قدرة `sampling` (و`sampling.tools` إذا مرّرت `tools` أو `tool_choice`). وإلا يفشل الاستدعاء بخطأ بروتوكول `-32021` بدلًا من إرسال طلب لا يستطيع العميل معالجته. تفشل جلسة أقدم من 2026 دون قناة عكسية بخطأ غياب القناة المعتاد، إذ لا توجد قناة للإرسال. +* في `2026-07-28`، يُسلَّم الطلب داخل التدفق متعدد الجولات (**[الطلبات متعددة جولات التبادل](multi-round-trip.md)**)؛ وفي `2025-11-25` يكون طلبًا مستقلًا إلى العميل. الشيفرة نفسها في الحالتين، لكن راعِ قاعدة تعدد الجولات: يجب أن يكون تمثيل الطلب متطابقًا عبر جولات إعادة المحاولة، فابنه فقط من وسائط الأداة وبيانات مستقرة أخرى. +* اترك `include_context` كما هي: القيم غير `"none"` مهجورة أيضًا (SEP-2596) وتحتاج إلى قدرة لا يعلنها تقريبًا أي عميل. + +## المجلدات الجذرية: أين ينبغي أن يعمل الخادم؟ {#roots-where-should-this-go} + +المجلدات الجذرية هي المجلدات التي يقول العميل إن الخادم يستطيع العمل عليها. إنها إرشادات معلوماتية، لا آلية للتحكم في الوصول. تعيد دالة حل الاعتمادية `ListRoots()`: + +```python title="server.py" hl_lines="10-11 15" +--8<-- "docs_src/sampling_and_roots/tutorial002.py" +``` + +* تحمل `ListRootsResult` المحقونة قائمة من كائنات `Root`: URI من نوع `file://` واسم عرض اختياري. +* الشرط نفسه كما في أخذ العينات: دون إعلان قدرة `roots`، يفشل الاستدعاء بـ`-32021` بدلًا من إرسال الطلب. + +على الطرف الآخر، يجيب العميل عن الطلبين بدوال رد النداء الموجودة لديه: `sampling_callback` و`list_roots_callback`، اللتين تغطيهما **[دوال رد النداء لدى العميل](../client/callbacks.md)**. + +## على اتصالات جيل 2025 {#on-2025-era-connections} + +ما زالت `ctx.session.create_message(...)` و`ctx.session.list_roots()` موجودتين للشيفرة التي تدير الجلسة مباشرة. لا تعملان إلا حيث توجد قناة عكسية (اتصالات جيل 2025 التي ليست عديمة الحالة)، ويثير استدعاؤهما تحذير إهمال. علامات دوال الحل أعلاه هي الشكل المدعوم: تختار طريقة التسليم بحسب الإصدار المتفاوض عليه ولا تصدر تحذيرًا. + +## مراجعة {#recap} + +* أعِد `Sample(...)` أو `ListRoots()` من دالة حل؛ فتتلقى الأداة `CreateMessageResult` أو `ListRootsResult` مثل أي اعتمادية أخرى. +* يجب أن يعلن العميل القدرة المطابقة، وإلا يفشل الاستدعاء بـ`-32021` بدلًا من إرسال طلب. +* الميزتان مهجورتان في `2026-07-28`: تعملان بالكامل حاليًا، لكنهما غير مناسبتين لتصاميم جديدة. فضّل API المزوّد على أخذ العينات، والمَعلمات الصريحة على المجلدات الجذرية. + +للإبلاغ عن مدى تقدّم أداة بطيئة: **[التقدم](progress.md)**. diff --git a/i18n/ar/pages/handlers/subscriptions.md b/i18n/ar/pages/handlers/subscriptions.md new file mode 100644 index 0000000000..fcd7d38480 --- /dev/null +++ b/i18n/ar/pages/handlers/subscriptions.md @@ -0,0 +1,192 @@ +--- +translation: + sections: [60a9de8a0bdaa531, 6693607ea56d8bd6, a61d660c8029e04a, 8f7e82fcb88df8a9, b165db51249ff8ed, b8bc624a627ead9b, 2139e68e36d9e621, 7c0e57030b622139, 34ab1af2b9ab5b45] + tool: 1 +--- +# الاشتراكات {#subscriptions} + +فهرس الخادم ليس ثابتًا. تظهر أدوات أثناء التشغيل ويتغير المحتوى وراء URI المورد. + +**الاشتراكات** وسيلة العميل لمعرفة ذلك. يرسل العميل طلب `subscriptions/listen` واحدًا، وتكون استجابة ذلك الطلب *هي* التدفّق: يبقى مفتوحًا ويحمل إشعارات التغيير التي طلبها العميل. + +## انشر التغيير من الأداة {#publish-it-from-the-tool} + +ما تفعله سطر واحد: انشر التغيير. + +```python title="server.py" hl_lines="20 32" +--8<-- "docs_src/subscriptions/tutorial001.py" +``` + +* تصل `await ctx.notify_resource_updated("board://sprint")` إلى كل تدفّق مفتوح اشترك في ذلك URI، ولا تصل إلى غيره. +* تصل `await ctx.notify_tools_changed()` إلى كل تدفّق طلب تغييرات قائمة الأدوات. يعيد العميل الذي يتلقاها استدعاء `tools/list` ويرى `sprint_report` الآن. +* الطريقتان المقابلتان هما `notify_prompts_changed()` و`notify_resources_changed()`. +* لا مشتركون، لا عمل. النشر إلى خادم دون مستمعين لا يفعل شيئًا، فلا تفحص وجودهم. أعلن ما تغيّر فقط. + +تخدم `MCPServer` طلبات `subscriptions/listen` نيابة عنك، ما لم [تعطّلها](#turning-it-off). التزامات النقل (الإقرار كأول إطار، والترشيح لكل تدفّق، ومعرّف الاشتراك على كل إطار) مسؤولية SDK. + +!!! check + أثناء النقل، يبدو التدفّق الذي سمّى مرشّحه `board://sprint` هكذا بعد تشغيل `complete_task`: + + ```json + {"method": "notifications/subscriptions/acknowledged", + "params": {"notifications": {"resourceSubscriptions": ["board://sprint"]}, "_meta": {"io.modelcontextprotocol/subscriptionId": "listen-1"}}} + + {"method": "notifications/resources/updated", + "params": {"uri": "board://sprint", "_meta": {"io.modelcontextprotocol/subscriptionId": "listen-1"}}} + ``` + + لاحظ ما *لا* يحمله التحديث: اللوحة. يحمل كل إطار معرّف JSON-RPC لطلب الاستماع تحت `_meta`، وهذا هو معرّف الاشتراك. يصدره العميل: تستخدم `Client` في Python سلاسل مثل `"listen-1"`؛ وقد تستخدم عملاء أخرى أعدادًا صحيحة. + +## ما طُلب فقط {#only-what-was-asked-for} + +المرشّح عقد. يتلقى تدفّق طلب تغييرات قائمة الأدوات وURI مورد واحد هذين النوعين فقط. انشر تغيير قالب توجيه، فيبقى ذلك التدفّق صامتًا. + +تطابق `MCPServer` عناوين URI للموارد كسلاسل متطابقة تمامًا، لذا لا يسمع تدفّق سمّى `board://sprint` شيئًا عن `board://sprint/tasks/1`. تسمح المواصفة للخادم بالإبلاغ عن تغيّر مورد فرعي لعنوان URI مُشترَك فيه؛ لا تفعل `MCPServer` ذلك أبدًا، لكن العملاء مصممون لتوقعه. + +أمران *لا* يمثلهما التدفّق: + +* **ليس سجلًا لإعادة تشغيل الأحداث.** يزول التدفّق المنقطع، ولا تُصف الأحداث المنشورة حين لا يتصل أحد. يعيد العملاء الاستماع وجلب البيانات. +* **ليس مسار 2025.** تخدم `ctx.session.send_resource_updated(uri)` العملاء الذين استدعوا `resources/subscribe`. تصل طرق `notify_*` إلى تدفّقات `subscriptions/listen` فقط. + +## تحديد من يجوز له المشاهدة {#deciding-who-may-watch} + +افتراضيًا، تُقبَل كل الأنواع وعناوين URI المطلوبة: يستطيع أي مستدعٍ مراقبة أي URI تنشره. لا تُستشار دالة معالجة القراءة لأن أحدًا لا يقرأ؛ فمستدعٍ ترفضه دالة `files://{name}` يستطيع فتح تدفّق على `files://payroll.csv` ومعرفة أنه تغيّر ومتى. لا يعرف المحتوى، ولا يستطيع استكشاف الموجود لأن URI غير المعروف يُقبَل أيضًا ولا يصدر أحداثًا. التسريب محدود لكنه فعلي، فضَع تحققًا قبل نشر عناوين URI خاصة بالمستخدمين من خادم متعدد المستأجرين. + +بوابة التحقق مكوّن وسيط. يرى طلب `subscriptions/listen` قبل إقرار SDK له، ويرفض عندما يطلب المستدعي شيئًا لا يجوز له قراءته: + +```python title="server.py" hl_lines="19-26 29" +--8<-- "docs_src/subscriptions/tutorial006.py" +``` + +* `ctx.params` هو الطلب الخام، لذا يتحقق منه المكوّن الوسيط بنفسه بتحويله إلى `SubscriptionsListenRequestParams` ويقرأ المرشّح الذي طلبه العميل. +* يكون الرفض عبر إثارة `MCPError` قبل `call_next(ctx)`: يتلقى العميل الخطأ دون تدفّق ويستمر الاتصال. اجعل الرسالة موحّدة دون تسمية URI، كي لا يؤكد الرفض أي عناوين محمية. +* تجيب `can_access(user, uri)` واحدة عن السؤالين. تستدعيها دالة المورد على `resources/read`؛ ويستدعيها المكوّن الوسيط على `subscriptions/listen`. استبدل الجدول بقاعدة بيانات أو نظام RBAC لديك، فيبقيان متوافقين. +* يسري القرار طوال حياة التدفّق. لا يُعاد التحقق لكل حدث، لذلك إذا كان وصول المستدعي قد ينتهي أثناء التدفّق (رمز تنتهي صلاحيته)، فأنه اتصاله عند حدوث ذلك. + +العقد الكامل للبرمجيات الوسيطة، بما فيه ما تغلّفه أيضًا ولماذا وُصف بالمؤقت، في **[البرمجيات الوسيطة](../advanced/middleware.md)**. + +## طرف العميل {#the-client-end} + +إليك عميلًا على الطرف الآخر من ذلك التدفّق يتابع اللوحة: + +```python title="client.py" hl_lines="15" +--8<-- "docs_src/subscriptions/tutorial003.py" +``` + +يرسل الدخول إلى `client.listen(...)` الطلب وينتظر إقرارك، لذلك يكون التدفّق نشطًا عند بدء الكتلة، ويكون كل حدث محدد النوع إشارة لإعادة الجلب، وليس حمولة بيانات. هذا هو العقد كاملًا في شاشة واحدة. بقية جانب العميل لها صفحة خاصة: المراقبة بجانب تدفق رئيسي، ونهايات التدفّقات، وإعادة الاستماع. راجع **[الاشتراكات](../client/subscriptions.md)** ضمن *العملاء*. + +## التوسع إلى أكثر من عملية {#scaling-past-one-process} + +تنتقل المنشورات من دالة المعالجة إلى التدفّقات المفتوحة عبر `SubscriptionBus`. الافتراضي داخل الذاكرة: عملية واحدة وكل التدفّقات فيها. هذا مناسب حتى تشغّل نسخًا خلف موازن حمل، لأن تدفّق العميل عندها مثبت على نسخة واحدة ويجب أن يصله نشر من نسخة أخرى. + +لا يمكن ذلك مع الناقل الافتراضي، لأن لكل نسخة ناقلها الخاص: + +```mermaid +flowchart LR + client[Client] --> lb[Load balancer] + lb --> stream + lb ~~~~ gap + lb --> tool + subgraph B [Replica B] + tool[tools/call] -- publishes --> busB[(bus B)] + end + gap[(no shared bus)] + subgraph A [Replica A] + stream[listen stream] -- subscribed --> busA[(bus A)] + end + style A fill:none + style B fill:none + style gap fill:none,stroke-dasharray:4 4 +``` + +لا يفشل شيء: ينجح الاستدعاء ويبقى التدفّق صامتًا. لذا اختر خلف موازن حمل: + +* **تحتاج إلى إشعارات تغيير.** أعطِ كل نسخة الناقل نفسه، كما أدناه. +* **لا تحتاج إليها.** [عطّلها](#turning-it-off)، كي لا يُوعَد عميل بأحداث ستفوته أو يبقي تدفّقًا مفتوحًا لها. + +أنت تنفّذ الناقل المشترك: طريقتان فوق نظام النشر والاشتراك لديك. + +```python +from collections.abc import Callable + +from redis.asyncio import Redis + +from mcp.server.mcpserver import MCPServer +from mcp.server.subscriptions import ServerEvent # SubscriptionBus is a Protocol: no base class + + +class RedisSubscriptionBus: + def __init__(self, redis: Redis) -> None: + self._redis = redis + self._listeners: dict[object, Callable[[ServerEvent], None]] = {} + + async def publish(self, event: ServerEvent) -> None: + await self._redis.publish("mcp-events", encode(event)) # to every replica + + def subscribe(self, listener: Callable[[ServerEvent], None]) -> Callable[[], None]: + token = object() + self._listeners[token] = listener + + def unsubscribe() -> None: + self._listeners.pop(token, None) + + return unsubscribe + + +mcp = MCPServer("Sprint Board", subscriptions=RedisSubscriptionBus(redis)) +``` + +`encode` مسؤوليتك، وكذلك مهمة القراءة على كل نسخة التي تفك الرسائل الواردة وتستدعي كل مستمع مسجّل. المستمعون متزامنون، ويجب ألّا يثيروا استثناءات، ويعملون على حلقة أحداث الخادم. + +يحمل الناقل قيم `ServerEvent` محددة النوع، وهي أربع فئات بيانات صغيرة، وليس JSON-RPC أبدًا. تبقى إضافة المعرّفات والترشيح ودورات حياة التدفّقات في SDK، فلا يستطيع تنفيذ الناقل كسر البروتوكول. يستطيع فقط نقل الأحداث بين العمليات. + +للنشر من خارج طلب، أنشئ الناقل بنفسك كي تحتفظ بالمرجع. تبني `MCPServer` واحدًا داخليًا عندما لا تمرّر شيئًا، ولا تتيحه. + +```python +from mcp.server.subscriptions import InMemorySubscriptionBus, ToolsListChanged + +bus = InMemorySubscriptionBus() +mcp = MCPServer("Sprint Board", subscriptions=bus) + + +async def tools_reloaded() -> None: + await bus.publish(ToolsListChanged()) # from a lifespan task, a webhook, anywhere +``` + +## تعطيل الاشتراكات {#turning-it-off} + +الخادم الذي لا يتغير فهرسه ليس لديه شيء لنشره. صرّح بذلك عند بنائه: + +```python title="server.py" hl_lines="3" +--8<-- "docs_src/subscriptions/tutorial007.py" +``` + +* لا يرى عميل `2026-07-28` إعلانًا عن إشعارات تغيير، ويحصل طلب `subscriptions/listen` على *الطريقة غير موجودة* بدلًا من تدفّق مفتوح. +* تبقى `ctx.notify_*` تعمل دون أن تصل إلى أحد، فلا تتغير دوال المعالجة. +* لا يرى عملاء إصدارات البروتوكول الأقدم فرقًا. + +التدفّق المفتوح طلب لا ينتهي، لذلك يهم هذا أيضًا على مضيف يحاسب بحسب مدة الطلب. + +## التركيب منخفض المستوى {#the-low-level-composition} + +لا شيء مربوط مسبقًا على `Server` منخفض المستوى، وتُجمع الأجزاء نفسها في ثلاثة أسطر: + +```python title="server.py" hl_lines="8-9 47" +--8<-- "docs_src/subscriptions/tutorial002.py" +``` + +* أنت تملك الناقل فتنشر إليه مباشرة: `await bus.publish(ResourceUpdated(uri=...))`. ضعه حيث تستطيع دوال المعالجة الوصول إليه: على مستوى الوحدة هنا، وفي دورة الحياة لتطبيق أكبر. +* `ListenHandler(bus)` هي دالة المعالجة نفسها التي تسجّلها `MCPServer`، و`on_subscriptions_listen=` موضع دالة معالجة عادي. ضع فيه دالتك لسلوك مختلف، وتنتقل إليك التزامات المواصفة: الإقرار أولًا، وإضافة معرّف الاشتراك لكل إطار، وعدم إرسال شيء خارج المرشّح. +* تنهي `ListenHandler.close()` كل التدفّقات المفتوحة بسلاسة. يتلقى كل منها نتيجة طلب الاستماع كإطار أخير، وهي طريقة المواصفة لإعلان إنهاء الخادم للاشتراك عمدًا. تعود قبل اكتمال تفريغ تلك التدفّقات، فامنحها وقتًا قبل إغلاق وسيلة النقل. وبدونها تنتهي التدفّقات عندما يقطع العميل الاتصال. + +## مراجعة {#recap} + +* يختار العميل الاشتراك بطلب `subscriptions/listen` واحد، وتكون الاستجابة هي التدفّق. خدمته مدمجة. +* تنشر باستخدام `ctx.notify_*`، وتتولى SDK إضافة المعرّفات والترشيح ودورة الحياة. +* الأحداث إشارات، وليست حمولات محتوى. يعيد الطرفان جلب البيانات. +* جانب العميل هو `async with client.listen(...)`: تشرحه **[الاشتراكات](../client/subscriptions.md)** ضمن *العملاء*. +* على `Server` منخفض المستوى، تجمع الأجزاء نفسها بنفسك: ناقل و`ListenHandler(bus)` وموضع `on_subscriptions_listen`. +* يعني التوسع تنفيذ `SubscriptionBus` بطريقتين، وتمريره باسم `MCPServer(subscriptions=...)`. +* إذا لم يوجد شيء للنشر، أو كانت النسخ دون ناقل مشترك، فلا تعلن `MCPServer(subscriptions=False)` إشعارات تغيير ولا تُبقي تدفّقًا. + +تشغيل الخادم الذي يتيح كل هذا، سواء بنسخة واحدة أو عشرين، موضوع **[النشر والتوسع](../run/deploy.md)**. diff --git a/i18n/ar/pages/index.md b/i18n/ar/pages/index.md new file mode 100644 index 0000000000..b20f8a991f --- /dev/null +++ b/i18n/ar/pages/index.md @@ -0,0 +1,102 @@ +--- +translation: + sections: [154c4309937b9f85, 3ad8fc6caa76a9b0, a07f3f5b151ab746, bf6e476b712930c0, cf0b1f13978c6623] + tool: 1 +--- +# MCP Python SDK {#mcp-python-sdk} + +!!! info "توثيق v2، سلسلة الإصدارات المستقرة الحالية" + هل تبدأ مع v2، أم تنتقل من v1؟ تقدم **[المستجدات في v2](whats-new.md)** جولة مدتها خمس دقائق للتغييرات، ويغطي **[دليل الترحيل](migration.md)** جميع التغييرات غير المتوافقة مع الإصدارات السابقة. + هل ما زلت تستخدم v1.x؟ ستجد توثيقها في [توثيق v1.x](https://py.sdk.modelcontextprotocol.io/v1/). + هل وجدت شيئًا غير واضح أو يحتاج إلى تحسين؟ [أخبرنا](https://github.com/modelcontextprotocol/python-sdk/issues/new?template=v2-feedback.yaml). + +يتيح **Model Context Protocol (MCP)** للتطبيقات توفير السياق لنماذج LLM بطريقة موحّدة، مع فصل مهمة *توفير* السياق عن التفاعل مع النموذج نفسه. + +هذه هي SDK الرسمية له بلغة Python. يمكنك استخدامها من أجل: + +* **بناء خوادم MCP** تتيح أدوات وموارد وقوالب توجيه لأي تطبيق مضيف يدعم MCP. +* **بناء عملاء MCP** يتصلون بأي خادم MCP. +* التواصل عبر جميع وسائل النقل القياسية: stdio وStreamable HTTP وSSE. + +## المتطلبات {#requirements} + +Python 3.10+. + +## التثبيت {#installation} + +=== "uv" + + ```bash + uv add "mcp[cli]" + ``` + +=== "pip" + + ```bash + pip install "mcp[cli]" + ``` + +تتيح لك الإضافة `[cli]` الأمر `mcp`؛ وستحتاج إليها أثناء التطوير. +راجع [التثبيت](get-started/installation.md) لمعرفة دور كل اعتمادية. + +## مثال {#example} + +### أنشئه {#create-it} + +أنشئ ملفًا باسم `server.py`: + +```python title="server.py" +--8<-- "docs_src/index/tutorial001.py" +``` + +هذا خادم MCP كامل. + +يتيح **أداة** واحدة، هي `add`، و**موردًا** واحدًا يعتمد على قالب، هو `greeting://{name}`. + +### شغّله {#run-it} + +```console +uv run mcp dev server.py +``` + +يشغّل هذا خادمك ويفتح [MCP Inspector](https://github.com/modelcontextprotocol/inspector)، وهي واجهة تفاعلية لاستكشافه. افتح عنوان URL الذي يطبعه. + +!!! note + Inspector تطبيق Node.js، لذلك يحتاج `mcp dev` إلى وجود `npx` ضمن `PATH`. + +### جرّبه {#try-it} + +انتقل في Inspector إلى **Tools** واستدعِ `add` بالقيمتين `a=1` و`b=2`. + +ستحصل على `3`. ✨ + +أنشأ Inspector ذلك النموذج (حقل عدد صحيح مطلوب لـ`a` وآخر لـ`b`) من تلميحات الأنواع لديك. وكذلك سيفعل Claude وكل تطبيق مضيف آخر يدعم MCP. + +انتقل الآن إلى **Resources** واقرأ `greeting://World`: + +```text +Hello, World! +``` + +### مراجعة {#recap} + +انظر مجددًا إلى ما **لم** تكتبه: + +* لم تكتب JSON Schema. فتلميح النوع `a: int, b: int` *هو* المخطط. +* لم تكتب تحليلًا للطلبات أو تسلسلًا للبيانات أو شيفرة تحقق. +* لم تكتب أي معالجة للبروتوكول. + +كتبت دالتين بلغة Python مع تلميحات للأنواع وسلسلة توثيق. تتولى SDK الباقي. + +## إلى أين تتجه بعد ذلك؟ {#where-to-go-next} + +* تنقلك **[ابدأ هنا](get-started/index.md)** من التثبيت إلى خادم يعمل وتُختبر صحته. +* هل تبني تطبيقًا *يستخدم* خوادم MCP؟ ابدأ بصفحة **[العملاء](client/index.md)**. +* هل لديك تطبيق FastAPI أو Starlette بالفعل؟ تشرح **[الإضافة إلى تطبيق موجود](run/asgi.md)** تركيب خادم MCP داخله. +* هل تبحث عن رسالة خطأ بعينها؟ تُنظَّم **[استكشاف الأخطاء وإصلاحها](troubleshooting.md)** حسب النص الحرفي للرسائل. +* هل تتساءل عمّا تغيّر في v2؟ تقدم **[المستجدات في v2](whats-new.md)** جولة مدتها خمس دقائق. +* هل ترحّل من v1؟ ابدأ بـ**[دليل الترحيل](migration.md)**. +* هل تبحث عن توقيع دالة دقيق؟ يُولَّد **[مرجع API](api/mcp/index.md)** من الشيفرة المصدرية. +* هل تقرأ باستخدام LLM؟ يُنشر هذا التوثيق أيضًا بتنسيق [llms.txt](https://llmstxt.org/): + يشكّل [llms.txt](https://py.sdk.modelcontextprotocol.io/llms.txt) فهرسًا للصفحات، بينما + يحتوي [llms-full.txt](https://py.sdk.modelcontextprotocol.io/llms-full.txt) على جميع الصفحات في ملف واحد. diff --git a/i18n/ar/pages/protocol-versions.md b/i18n/ar/pages/protocol-versions.md new file mode 100644 index 0000000000..ca5e3e75c2 --- /dev/null +++ b/i18n/ar/pages/protocol-versions.md @@ -0,0 +1,141 @@ +--- +translation: + sections: [d98e337bf28845e0, dd642acbba36f615, f547b3e76da19c94, 149513378588da5c, 4e149ed992046709, f54974398e43ddef, b24443dd78584870] + tool: 1 +--- +# إصدارات البروتوكول {#protocol-versions} + +لـMCP جيلان. + +تبدأ الخوادم الصادرة قبل 2026-07-28 كل اتصال بـ**مصافحة `initialize`**: يقترح العميل إصدارًا، ويرد الخادم باقتراحه، ويؤكد العميل، وكل ذلك قبل أول طلب مفيد. تلغي خوادم **2026-07-28** المصافحة. يرسل العميل فحص **`server/discover`** واحدًا، ويجيب الخادم بكل شيء في نتيجة واحدة. + +لا تحتاج غالبًا إلى الاهتمام بذلك، لأن `Client` يتفاوض نيابة عنك. تتناول هذه الصفحة وسيطة المُنشئ الوحيدة التي تتحكم فيه، `mode=`، والحالات الثلاث التي تغيّرها فيها. + +كل مقطع في هذه الصفحة هو `client.py` يتواصل مع `server.py` الخاص بـBookshop في **[العميل](client/index.md)**. شغّل ذلك الخادم في طرفية: + +```console +uv run mcp run server.py --transport streamable-http +``` + +ثم شغّل كل مقطع في طرفية ثانية باستخدام `python client.py`. + +## `mode="auto"` {#modeauto} + +```python title="client.py" hl_lines="7-8" +--8<-- "docs_src/protocol_versions/tutorial001.py" +``` + +لم تمرّر `mode`، فحصلت على الافتراضي: `"auto"`. يرسل الدخول في `async with` فحص `server/discover` واحدًا بأحدث إصدار تدعمه SDK هذه. ثم: + +* يجيب **الخادم الحديث** عنه. يعتمد العميل النتيجة. جولة طلب ورد واحدة، ويكتمل الاتصال. +* لم يسمع **الخادم الأقدم** بـ`server/discover`، فيعيد خطأ. يعود العميل إلى مصافحة `initialize` التقليدية ويقبل الإصدار الذي تتفاوض عليه. + +في الحالتين، تصبح متصلًا، وتخبرك `client.protocol_version` بالإصدار: + +```text +2026-07-28 +``` + +هذه الميزة كاملةً. `Client` واحد لأي جيل من الخوادم، دون تفرّع في شيفرتك. + +!!! info + يجيب `MCPServer` عن `server/discover` في كل وسائل النقل — Streamable HTTP وstdio + والاتصال داخل العملية الذي تستخدمه اختباراتك — ولذلك يصل `auto` دائمًا مع خادمك + إلى `2026-07-28`. لا يحدث الرجوع إلا مع خادم فعلي يسبق 2026، وهذا بالضبط + متى تحتاج إليه. + +## `mode="legacy"` {#modelegacy} + +```python title="client.py" hl_lines="7" +--8<-- "docs_src/protocol_versions/tutorial002.py" +``` + +لا تنفّذ `mode="legacy"` أي فحص. تجري مصافحة `initialize`، وهي الاتصال نفسه الذي يفتحه عميل يسبق 2026. + +```text +2025-11-25 +``` + +الخادم نفسه. يدعم `2026-07-28` تمامًا؛ لكنك طلبت من العميل ألا يسأل عنه. + +تحتاج إلى ذلك لميزات **الدفع**. + +الطلب الذي يبدأه الخادم هو استدعاء الخادم *لك*: مثل `ctx.elicit(...)` التي تعرض نموذجًا للمستخدم، أو أخذ العينات الذي يطلب من نموذجك استكمالًا أثناء استدعاء أداة. لا توجد هذه القناة إلا في جلسة من جيل المصافحة. + +تختفي في 2026-07-28. *يعيد* الخادم أسئلته، وتعيد أنت الاستدعاء مع الإجابات (**[الطلبات متعددة جولات الطلب والرد](handlers/multi-round-trip.md)**). + +لا تعطيك `mode="auto"` مصافحة إلا عندما يكون الخادم أقدم من أن يدعم غيرها. تضمنها `mode="legacy"`. استخدمها عندما تمرّر إلى `Client(...)` قيمة `sampling_callback`، أو `elicitation_callback` تريد استدعاءها بطلب، أو `message_handler`. تشرح **[دوال رد نداء العميل](client/callbacks.md)** كلًا منها. + +## تثبيت إصدار {#pinning-a-version} + +تقبل `mode` أيضًا نص إصدار بروتوكول حديث. حاليًا، هذه المجموعة هي `["2026-07-28"]` فقط. + +```python title="client.py" hl_lines="7" +--8<-- "docs_src/protocol_versions/tutorial003.py" +``` + +لا يرسل التثبيت **أي شيء**. لا فحص ولا مصافحة. يعتمد العميل `2026-07-28` محليًا، ويصبح الاتصال جاهزًا بمجرد عودة `async with`. + +التثبيت وعد تقدّمه *أنت*: تعرف بالفعل أن الخادم يدعم ذلك الإصدار. لا يتحقق العميل منه. + +!!! check + التثبيت ليس اكتشافًا. اطبع `client.server_info` وسترى المقابل: + + ```text + None + ``` + + لم يسأل العميل الخادم عن هويته، فتكون `server_info` مساوية لـ`None`. وينطبق الأمر نفسه على `client.server_capabilities`: + كل قدرة تساوي `None`. تظل استدعاءات الأدوات تعمل (لا يحتاج البروتوكول إلى تلك المعلومات)؛ + أما الشيفرة التي تقرأ `server_capabilities` لتحديد ما تعرضه فلا تعمل. + + يقدّم القسم التالي الحل. + +لا يمكن تثبيت سوى الإصدارات الحديثة. يُرفض نص إصدار من جيل المصافحة عند الإنشاء، قبل أي إدخال أو إخراج، ويخبرك الخطأ بما تكتبه بدلًا منه: + +```text +ValueError: mode must be 'legacy', 'auto', or one of ['2026-07-28']; got '2025-06-18' ('2025-06-18' is a handshake-era version; use mode='legacy') +``` + +## إعادة الاتصال باستخدام `prior_discover` {#reconnecting-with-prior_discover} + +الفحص قليل التكلفة، لكنه يظل جولة طلب ورد تتحملها في كل إعادة اتصال، ولا تتغير الإجابة تقريبًا. + +لذلك احفظها. بعد اتصال `auto`، تحمل `client.session.discover_result` قيمة `DiscoverResult` التي أرسلها الخادم بالضبط: `supported_versions` و`capabilities` و`instructions` والهوية التي وضعها الخادم في `_meta` للنتيجة. مرّرها مجددًا في `prior_discover=` في المرة التالية: + +```python title="client.py" hl_lines="8 10" +--8<-- "docs_src/protocol_versions/tutorial004.py" +``` + +```text +2026-07-28 +Bookshop +``` + +نفّذ الاتصال الثاني **صفرًا** من جولات التفاوض، وظل يعرف الطرف الذي يتحدث إليه تمامًا. هذا هو التثبيت الصحيح: تسمّي `mode=` الإصدار، وتوفر `prior_discover=` الهوية. ✨ + +`DiscoverResult` نموذج Pydantic. تُحفظ `saved.model_dump_json()` في ملف أو ذاكرة مؤقتة؛ وتستعيدها `DiscoverResult.model_validate_json(...)` في العملية التالية. + +!!! tip + لا تؤثر `prior_discover=` إلا عندما تكون `mode` تثبيتًا لإصدار. تحت `"auto"`، يفحص العميل + الخادم على أي حال، وتحت `"legacy"` تُهمل. + +## الأوضاع الأربعة {#the-four-modes} + +| ما تكتبه | حركة التفاوض | ما تحصل عليه | +| --- | --- | --- | +| `Client(target)` | فحص `server/discover` واحد؛ ثم مصافحة `initialize` إن فشل | أحدث إصدار يدعمه الطرفان، أيًا كان الجيل | +| `Client(target, mode="legacy")` | مصافحة `initialize` | إصدار من جيل المصافحة؛ تعمل الطلبات التي يبدأها الخادم | +| `Client(target, mode="2026-07-28")` | لا شيء | ذلك الإصدار مثبتًا، مع `server_info` تساوي `None` | +| `Client(target, mode="2026-07-28", prior_discover=saved)` | لا شيء | ذلك الإصدار مثبتًا، *مع* الهوية التي حفظتها سابقًا | + +## مراجعة {#recap} + +* لـMCP جيل مصافحة (حتى `2025-11-25`، مع مصافحة `initialize`) وجيل حديث (`2026-07-28`، مع `server/discover`). يصل `Client` بينهما. +* `mode="auto"` هي الافتراضية: فحص ثم رجوع. اتركها ما لم يصفك أحد الصفوف الثلاثة الأخرى. +* تجيب `client.protocol_version` دائمًا عن «أي إصدار حصلت عليه؟». +* تفرض `mode="legacy"` المصافحة. تحتاجها للطلبات التي يبدأها الخادم: أخذ العينات، واستقاء المعلومات بالدفع، و`message_handler`. +* لا يرسل تثبيت الإصدار (`mode="2026-07-28"`) أي حركة تفاوض، ومقابله أن تكون `client.server_info` مساوية لـ`None`. +* تعوّض `prior_discover=` ذلك: احفظ `client.session.discover_result`، وأعد الاتصال بها، واحصل على الأمرين. + +لا توجد قناة دفع في الاتصال الحديث، فكيف يسألك خادم 2026 أثناء الاستدعاء؟ يعيد السؤال في النتيجة: **[الطلبات متعددة جولات الطلب والرد](handlers/multi-round-trip.md)**. diff --git a/i18n/ar/pages/run/asgi.md b/i18n/ar/pages/run/asgi.md new file mode 100644 index 0000000000..da7eb107c8 --- /dev/null +++ b/i18n/ar/pages/run/asgi.md @@ -0,0 +1,145 @@ +--- +translation: + sections: [1062ef792791488a, 4be2b831547184a9, 374b049e770385f2, b72f6947089e6de0, b172c9db7831bb31, 10394c6f16601638, cba78e052898c3f6, f06bdb541cb0b469, 6dc898ccc5a903f9] + tool: 1 +--- +# الإضافة إلى تطبيق موجود {#add-to-an-existing-app} + +تشغّل `mcp.run("streamable-http")` خادم ويب نيابة عنك. قد لا تريد ذلك أحيانًا: خادم MCP جزء من تطبيق ويب أكبر، أو لديك نشر ASGI بالفعل. + +لهذا، تعيد `mcp.streamable_http_app()` **تطبيق Starlette**. + +تطبيق Starlette هو تطبيق ASGI، لذا يستطيع كل ما يستضيف ASGI (uvicorn أو Hypercorn أو Starlette آخر أو FastAPI) استضافة خادم MCP لديك. + +## التطبيق {#the-app} + +```python title="server.py" hl_lines="12" +--8<-- "docs_src/asgi/tutorial001.py" +``` + +`app` تطبيق ASGI عادي. سلّمه لأي خادم ASGI: + +```console +uvicorn server:app +``` + +توجد نقطة نهاية MCP على `/mcp`، فيتصل العميل بـ`http://127.0.0.1:8000/mcp`. + +يحمل التطبيق شيئين بالفعل: + +* مسارًا واحدًا، `/mcp`: نقطة نهاية Streamable HTTP. +* **دورة حياة** تبدأ `mcp.session_manager`، وهو الكائن الذي يملك العمل الخلفي لكل جلسة نشطة. + +شغّل التطبيق منفردًا (`uvicorn server:app`) ولن تحتاج إلى التفكير في أي منهما. + +!!! tip + تأخذ `streamable_http_app()` الوسائط المسمّاة نفسها في `mcp.run("streamable-http", ...)`، + عدا `port`: يخص المنفذ ما يخدم التطبيق. ما زالت `host` مقبولة لكنها لا تربط + أي عنوان هنا؛ وتشرح **[النشر والتوسع](deploy.md)** ما تتحكم فيه فعلًا. + تغطي **[تشغيل خادمك](index.md)** الخيارات نفسها. + +تفعل `mcp.sse_app()` الشيء نفسه لوسيلة نقل SSE المستبدلة. + +## localhost فقط، حتى تصرّح بغير ذلك {#localhost-only-until-you-say-otherwise} + +يجيب التطبيق افتراضيًا عن الطلبات الموجّهة إلى localhost **فقط**. لا تستطيع `streamable_http_app()` +معرفة اسم المضيف الذي سيُتاح خلفه، لذلك تفعّل الحماية من إعادة ربط DNS بأكثر +قوائم السماح أمانًا؛ وهذا مناسب تمامًا على جهازك. عند النشر خلف اسم مضيف فعلي، +يعني ذلك **رفض كل طلب بـ`421 Misdirected Request`** حتى تمرّر إلى +`transport_security=` قائمة سماح لما تتيحه فعلًا. لا يُستشار أي شيء بنيته +قبل ذلك. قائمة السماح وكل ما بين تطبيق يعمل واسم مضيف فعلي +موضوع **[النشر والتوسع](deploy.md)**. + +## التركيب داخل تطبيق {#mounting-it} + +عندما يصبح خادم MCP *جزءًا* من تطبيق أكبر، تضع التطبيق داخل `Mount`. وعند فعل ذلك، تصبح دورة الحياة مسؤوليتك: + +```python title="server.py" hl_lines="18-21 25-26" +--8<-- "docs_src/asgi/tutorial002.py" +``` + +* يحتفظ `Mount("/", ...)` مع مسار `/mcp` الافتراضي بنقطة النهاية على `/mcp`. تجرّب Starlette المسارات بالترتيب، ويطابق `Mount("/")` **كل** مسار، لذلك يجب وضع مساراتك *قبله* في القائمة. لا يمكن الوصول إلى شيء بعده. +* تدخل دالة `lifespan` سياق `mcp.session_manager.run()` طوال حياة التطبيق **المستضيف**. هذا هو السطر الذي ينساه الجميع. +* لا توجد `mcp.session_manager` إلا *بعد* استدعاء `streamable_http_app()`. لذلك تُبنى المسارات على مستوى الوحدة، ولا يُستخدم المدير إلا داخل دورة الحياة. + +يعمل مسار `Host` في Starlette بالطريقة نفسها: استبدل `Mount("/", ...)` بـ`Host("mcp.example.com", ...)` للتوجيه حسب اسم المضيف بدلًا من المسار. لا تتغير قاعدة دورة الحياة ولا أمان النقل. لا يتلقى مسار `Host("mcp.example.com", ...)` إلا الطلبات الموجّهة لذلك الاسم، لكن قائمة السماح الخاصة بترويسة Host في وسيلة النقل (**[النشر والتوسع](deploy.md)**) تعمل أولًا. دون `"mcp.example.com"` فيها، يجيب المسار عن جميعها بـ`421`. + +!!! warning "التطبيق المستضيف مسؤول عن دورة الحياة" + تربط `streamable_http_app()` الدالة `session_manager.run()` بدورة حياة تطبيق Starlette الذي + تعيده، لكن **دورة حياة تطبيق فرعي مركّب لا تعمل أبدًا**. بعد تركيب التطبيق، تصبح + تلك الدورة المدمجة شيفرة لا تُنفَّذ. يجب أن يدخل التطبيق الموجود أعلى سلسلة ASGI + سياق `mcp.session_manager.run()` في دورة حياته الخاصة. + +!!! check + احذف سطر `lifespan=lifespan` وشغّل الخادم. يبدأ، ويُحَل المسار. + ثم يفشل أول طلب إلى `/mcp` بالرسالة: + + ```text + RuntimeError: Task group is not initialized. Make sure to use run(). + ``` + + لا يبدأ مدير الجلسات إلا عبر `run()` الخاص به. + +## خادمان وتطبيق واحد {#two-servers-one-app} + +كل `MCPServer` تطبيق مستقل بمدير جلسات خاص به. ركّب ما تشاء؛ وادخل سياق كل مدير من دورة حياة واحدة للمستضيف: + +```python title="server.py" hl_lines="27-30 35-36" +--8<-- "docs_src/asgi/tutorial003.py" +``` + +* يدخل `AsyncExitStack` سياق المديرين؛ يبدآن معًا ويُغلقان بترتيب عكسي. +* نقطتا النهاية هما `/notes/mcp` و`/tasks/mcp`: بادئة التركيب مع المسار الافتراضي. + +## تغيير المسار {#changing-the-path} + +الجزء `/mcp` في النهاية هو `streamable_http_path`. عيّنه إلى `"/"` فتصبح بادئة التركيب المسار العام كاملًا: + +```python title="server.py" hl_lines="25" +--8<-- "docs_src/asgi/tutorial004.py" +``` + +يتصل العملاء الآن بـ`/notes/`، لا بـ`/notes/mcp`. + +## CORS لعملاء المتصفح {#cors-for-browser-clients} + +يحتاج العميل القائم على المتصفح إلى إذنين منك: **إرسال** ترويسات طلب MCP، و**قراءة** الترويسة التي يعيدها MCP. كلاهما إعداد CORS على التطبيق المستضيف، ويجب أن تتوافق معه قائمة سماح أمان النقل أعلاه: + +```python title="server.py" hl_lines="27-30 33 35-49" +--8<-- "docs_src/asgi/tutorial005.py" +``` + +* `allow_headers` هو النصف الذي ينساه الجميع. يرسل المتصفح **طلب تحقق مسبقًا** لكل طلب MCP، لأن `Content-Type: application/json` وترويسات الطلب `Mcp-*` ليست في قائمة CORS الآمنة، وإذا لم يُسمَح بترويسة في التحقق المسبق فلن يرسل المتصفح الطلب. (تعمل `allow_headers=["*"]` أيضًا: تجيب Starlette عن التحقق المسبق بما طلبه.) +* `expose_headers=["Mcp-Session-Id"]` هو نصف القراءة. تعيد Streamable HTTP معرّف الجلسة في ترويسة الاستجابة هذه، وتخفي المتصفحات ترويسات الاستجابة عن JavaScript ما لم يعلنها CORS بالاسم. وبدونه لا يستطيع العميل إجراء طلبه الثاني. +* `allow_origins` قرارك، لا قرار MCP. كن دقيقًا، وطابقه مع `allowed_origins=` أعلاه: يفرض المتصفح CORS، لكن الخادم يفحص `Origin` بنفسه، ويحصل أصل لا تثق به وسيلة النقل على `403` حتى بعد تحقق مسبق ناجح. +* يسرد `allow_methods` الطرق الثلاث التي تستخدمها Streamable HTTP: `POST` لإرسال الرسائل، و`GET` لفتح تدفّق الخادم إلى العميل، و`DELETE` لإنهاء الجلسة. + +## المسارات المخصصة {#custom-routes} + +تسجّل `@mcp.custom_route()` نقطة نهاية HTTP عادية على التطبيق نفسه، للأمور التي تحتاجها كل خدمة منشورة ولا علاقة لها بـMCP: فحص الصحة أو رد نداء OAuth. + +```python title="server.py" hl_lines="15-17" +--8<-- "docs_src/asgi/tutorial006.py" +``` + +* دالة المعالجة Starlette عادية: دالة `async` من `Request` إلى `Response`. +* تلتقط `streamable_http_app()` كل مسار مخصص. أصبحت `app.routes` هي `/mcp` و`/health`. +* يجيب `GET /health` بـ`{"status": "ok"}` دون أي MCP. + +!!! warning + **لا تُفرَض المصادقة على المسارات المخصصة أبدًا**، حتى إذا كانت بقية الخادم محمية. هذا + مقصود: يجب الوصول إلى فحوص الصحة وردود نداء OAuth قبل وجود أي رمز. + لا تضع بيانات خاصة خلفها. + +## مراجعة {#recap} + +* تعيد `mcp.streamable_http_app()` تطبيق Starlette بمسار واحد `/mcp`. يستطيع أي خادم ASGI تشغيله. +* يجيب التطبيق افتراضيًا عن طلبات localhost فقط، ويرفض كل شيء خلف اسم مضيف فعلي بـ`421` حتى تمرّر قائمة سماح إلى `transport_security=`. تشرح **[النشر والتوسع](deploy.md)** ذلك وبقية الطريق إلى الإنتاج. +* يضعه `Mount` (أو `Host`) داخل تطبيق Starlette أو FastAPI أكبر. +* **يعطّل التركيب دورة الحياة المدمجة.** يجب أن تدخل دورة حياة التطبيق المستضيف سياق `mcp.session_manager.run()`، وإلا يفشل أول طلب. +* عدة خوادم في تطبيق واحد تعني عدة تركيبات ودورة حياة واحدة تدخل سياق كل مدير جلسات. +* تنقل `streamable_http_path="/"` نقطة النهاية إلى بادئة التركيب نفسها. +* تحتاج عملاء المتصفح إلى CORS: `allow_headers` لترويسات الطلب `Mcp-*`، و`expose_headers=["Mcp-Session-Id"]` للاستجابة. +* تضيف `@mcp.custom_route()` نقاط نهاية HTTP عادية دون مصادقة بجانب `/mcp`. + +بعد أن يصبح الخادم متاحًا على URL فعلي، تتصل به **[العميل](../client/index.md)** باستخدام ذلك العنوان. diff --git a/i18n/ar/pages/run/authorization.md b/i18n/ar/pages/run/authorization.md new file mode 100644 index 0000000000..a8fb95c61e --- /dev/null +++ b/i18n/ar/pages/run/authorization.md @@ -0,0 +1,134 @@ +--- +translation: + sections: [d62c13457fc4a534, 80e73abaca6e0652, 128a492d18295f64, 14ad3bc7904036bb, 54a6697833fcc1d9, fe1626fdd5aad1da, 811d083c1da8bcf6] + tool: 1 +--- +# التفويض {#authorization} + +عبر Streamable HTTP، خادم MCP خدمة ويب عادية، وتحميه كما تحمي أي خدمة ويب: برموز حامل OAuth 2.1. + +بمصطلحات OAuth، خادمك **خادم موارد**. لا يسجّل دخول أحد ولا يصدر رمزًا أبدًا. يفعل شيئًا واحدًا: يفحص ترويسة `Authorization` في كل طلب ويقرر صلاحية الرمز فيها. + +هذه الصفحة لجانب الخادم. أما العميل الذي يكتشف خادم التفويض ويجلب الرمز، ففي **[عملاء OAuth](../client/oauth-clients.md)**. + +## الأطراف الثلاثة {#the-three-parties} + +* يسجّل **خادم التفويض** دخول المستخدمين ويصدر رموز الوصول. لا تكتبه أنت. إنه مزوّد الهوية لديك (Auth0 أو Keycloak أو Entra أو مزوّدك الخاص). +* **خادم الموارد** هو خادم MCP لديك. يتحقق من الرمز في كل طلب. +* يكتشف **العميل** خادم التفويض الذي تثق به، ويحصل منه على رمز، ويرسله إليك بصيغة `Authorization: Bearer `. + +هذا هو المثلث كاملًا. تتناول هذه الصفحة النقطة الوسطى فقط. + +## متحقق الرموز {#a-token-verifier} + +لا تفرض SDK شكلًا للرمز الصالح. تحدده أنت بتنفيذ **`TokenVerifier`**: + +```python title="server.py" hl_lines="14-16 21-27" +--8<-- "docs_src/authorization/tutorial001.py" +``` + +* `TokenVerifier` بروتوكول بطريقة غير متزامنة واحدة. تتلقى `verify_token` الرمز الخام من ترويسة `Authorization` وتعيد **`AccessToken`** إذا كان صالحًا، أو `None` إذا لم يكن. لا شيء آخر لتنفيذه. +* يبحث هذا المثال عن الرمز في جدول؛ ويسجّل كل إدخال المورد الذي صدر له. يتحقق التنفيذ الفعلي من توقيع JWT أو يستدعي نقطة نهاية استبطان الرموز لخادم التفويض، ويحدد لمن صدر الرمز (`aud` الخاص به) في `AccessToken.resource`. تلك الشيفرة مسؤوليتك؛ ولا تفعل SDK إلا استدعاءها. +* يأتي `token_verifier=` و`auth=` معًا دائمًا. مرّر أحدهما دون الآخر فتثير `MCPServer(...)` الاستثناء `ValueError` قبل خدمة أي طلب. + +`AuthSettings` هي الواجهة العامة لخادم مواردك: + +* `issuer_url`: خادم التفويض الذي يصدر رموزك. +* `resource_server_url`: عنوان URL العام لنقطة نهاية MCP هذه. يحدد *أي* مورد يخصه الرمز، وهو موضع مستند الاكتشاف. +* `required_scopes`: يجب أن يحمل كل رمز جميع هذه النطاقات. +* `validate_token_resource`: ارفض أي رمز لا تساوي فيه `AccessToken.resource` القيمة `resource_server_url`. تركه دون تعيين مع تعيين `resource_server_url` يصدر تحذيرًا (`MCPDeprecationWarning`) ويتصرف كـ`False`؛ ويجعل 3.0 القيمة `True` الافتراضية لخوادم الموارد. + * فعّله عندما يربط خادم التفويض الرموز بـ`resource` الذي طلبه العميل، وهو ما ترسله عملاء MCP دائمًا. أبقِ `resource_server_url` مطابقًا تمامًا لعنوان اتصال العملاء. + * اتركه معطّلًا عندما يستخدم خادم التفويض معرّفات جمهور خاصة به (معرّف API في Auth0 أو معرّف تطبيق Entra)، وافحص `aud` في متحققك بدلًا من ذلك، مع إعادة `None` للرمز الذي لا يخص هذا الخادم. + * إذا كان `aud` قائمة، فضع الإدخال الذي يساوي `resource_server_url` في `resource`. + +!!! tip + يتضمن `examples/servers/simple-auth/` في مستودع SDK كائن `IntrospectionTokenVerifier` يستدعي + نقطة نهاية [RFC 7662](https://datatracker.ietf.org/doc/html/rfc7662) لخادم تفويض فعلي. هذا شكل معظم متحققات الإنتاج. + +## ما تحصل عليه عبر HTTP {#what-you-get-over-http} + +يوجد التفويض في ترويسات HTTP، لذلك لا يوجد إلا على وسائل نقل HTTP. شغّله على الوسيلة التي تنشرها: تضعه `mcp.run(transport="streamable-http")` على `http://127.0.0.1:8000/mcp`، وتشرح **[تشغيل خادمك](index.md)** الباقي. أصبح للتطبيق مساران: + +```text +/mcp +/.well-known/oauth-protected-resource/mcp +``` + +سجّلت أداة واحدة. أما المسار الثاني فمن SDK. + +### الاكتشاف {#discovery} + +أرسل `GET` إلى ذلك المسار المعروف فتحصل على **بيانات وصفية للمورد المحمي وفق [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728)**، مبنية مباشرة من `AuthSettings`: + +```json +{ + "resource": "http://127.0.0.1:8000/mcp", + "authorization_servers": ["https://auth.example.com/"], + "scopes_supported": ["notes:read"], + "bearer_methods_supported": ["header"] +} +``` + +هذا المستند وسيلة دخول عميل لم يسمع بخادمك: يقرأ `authorization_servers` ويتجه إليها للحصول على رمز. لم تكتب شيئًا من ذلك. + +!!! check + استدعِ `/mcp` دون رمز (أو برمز أعاد متحققك له `None`)، فيُوقَف + الطلب عند المدخل: + + ```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"} + ``` + + لم يُحلَّل شيء ولم تعمل أداة. ومرجع `resource_metadata` في `WWW-Authenticate` هو + ما يجعل الاكتشاف تلقائيًا: 401 -> مستند البيانات الوصفية -> خادم التفويض -> رمز -> إعادة المحاولة. + +!!! warning + لا يحمي شيء من ذلك `stdio`. لا تملك القناة ترويسة `Authorization`، لذلك لا يُستشار + `token_verifier` فيها أبدًا. الحد الأمني لخادم `stdio` هو العملية التي شغّلته. وينطبق + الأمر على `Client(mcp)` داخل الذاكرة في الاختبارات: يتصل مباشرة بكائن الخادم + ويتجاوز طبقة HTTP، بما فيها التفويض. + +## هوية المستدعي {#the-callers-identity} + +داخل أي دالة معالجة، تعيد **`get_access_token()`** كائن `AccessToken` الذي أعاده متحققك للطلب الحالي: + +```python title="server.py" hl_lines="4 35-38" +--8<-- "docs_src/authorization/tutorial002.py" +``` + +* تعمل في الأدوات والموارد وقوالب التوجيه، ولا تحتاج إلى تمرير شيء: تخزّن البرمجيات الوسيطة للتفويض الكائن في متغير سياق لكل طلب. +* تحصل على **الكائن نفسه الذي بناه متحققك**: `client_id` و`scopes` و`subject` و`expires_at` وأي `claims` إضافية أرفقتها. هذه نقطة تطبيق قواعد لكل أداة: اقرأ النطاقات وارفض عند الحاجة. +* خارج طلب HTTP مصادَق عليه، تعيد `None`. داخل الذاكرة وعبر `stdio`، تكون دائمًا `None`. + +استدعِ `whoami` مع `Authorization: Bearer alice-token` فيقرأ النموذج: + +```text +alice (scopes: notes:read) +``` + +## النصف الذي لا تنفّذه SDK {#the-half-the-sdk-doesnt-do} + +توفر SDK جانب خادم الموارد: التحقق والإعلان والرفض. لا توفر صفحة دخول أو شاشة موافقة أو رمزًا. + +لمشاهدة عمل الأطراف الثلاثة، شغّل `examples/servers/simple-auth/` من مستودع SDK (خادم تفويض صغير وخادم موارد معدّان كما في هذه الصفحة)، ثم وجّه `examples/clients/simple-auth-client/` إليه لتجربة تدفق الاكتشاف والحصول على الرمز كاملًا. + +!!! info + توجد وسيطة مُنشئ ثانية، `auth_server_provider=`، تُضمّن خادم تفويض كاملًا + داخل خادم MCP. تسبق فصل AS/RS الذي تقوم عليه + مواصفة تفويض MCP. ينبغي ألّا تستخدمها الخوادم الجديدة. + +يستطيع خادم التفويض أيضًا قبول إفادة موقعة من مزوّد هوية مؤسسة بدلًا من نقر المستخدم عبر شاشة الموافقة، وتدعم SDK جانبي التبادل. المنحة والعميل الذي يقدمها في **[إفادة الهوية](../client/identity-assertion.md)**. + +## مراجعة {#recap} + +* عبر Streamable HTTP، خادمك **خادم موارد** OAuth 2.1: يتحقق من الرموز ولا يصدرها أبدًا. +* `TokenVerifier` واجهة التكامل كاملة: طريقة غير متزامنة واحدة، يدخل الرمز وتخرج `AccessToken | None`. +* يأتي `token_verifier=` و`auth=AuthSettings(issuer_url=..., resource_server_url=..., required_scopes=[...])` معًا دائمًا. +* تنشر SDK بيانات المورد المحمي وفق [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) على `/.well-known/oauth-protected-resource/...`، وتجيب الطلبات دون مصادقة بـ401 تشير ترويسة `WWW-Authenticate` فيها إلى ذلك المستند. هذا الاكتشاف بالكامل. +* توضح `get_access_token()` في أي دالة معالجة من يستدعيها. +* التفويض شأن HTTP. لا يراه `stdio` ولا عميل الاختبار داخل الذاكرة أبدًا. + +جانب العميل (اكتشاف خادم التفويض وجلب الرمز نيابة عنك) في **[عملاء OAuth](../client/oauth-clients.md)**. والعميل الذي *يفيد* بهوية بدلًا من سؤال المستخدم عنها في **[إفادة الهوية](../client/identity-assertion.md)**. diff --git a/i18n/ar/pages/run/deploy.md b/i18n/ar/pages/run/deploy.md new file mode 100644 index 0000000000..22e5987477 --- /dev/null +++ b/i18n/ar/pages/run/deploy.md @@ -0,0 +1,197 @@ +--- +translation: + sections: [28221886b198784f, f88ea1f1614f3a1d, 2e76f5cb9df15042, ce926d686730b6d0, 3be24f8ad8bb5ab9, aaf489e944ecf5d1, f25a7f860e579ecb, 697b01d95080880d] + tool: 1 +--- +# النشر والتوسع {#deploy-scale} + +خادمك يعمل. والآن يحتاج إلى اسم مضيف فعلي وأكثر من عامل خلفه. + +معظم ذلك خارج مسؤولية MCP. أنت توفر خادم ASGI ومدير العمليات وموازن الحمل. تعرض هذه الصفحة القائمة القصيرة لما *يخص* MCP: إعداد واحد يحكم كل نشر، وموضعان يغيّر فيهما تعدد العمال سلوك SDK. + +## قبل كل شيء: قائمة السماح لترويسة Host {#before-anything-else-the-host-allowlist} + +لا تستطيع `streamable_http_app()` معرفة اسم المضيف الذي ستُتاح خلفه، فتفترض الإجابة الأكثر أمانًا: localhost. دون `transport_security=`، يفعّل التطبيق **الحماية من إعادة ربط DNS** ولا يقبل طلبًا إلا إذا كانت ترويسة `Host` هي `127.0.0.1:` أو `localhost:` أو `[::1]:`. ويجب أن تكون ترويسة `Origin`، إن وُجدت، بصيغة `http://` للعنوان نفسه. هذا مناسب تمامًا على جهازك: يمنع صفحة خبيثة من التحكم بخادمك المحلي عبر اسم DNS أعادت ربطه بـ`127.0.0.1`. + +عند النشر خلف اسم مضيف فعلي، يرفض الإعداد نفسه **كل طلب** حتى تصرّح بغير ذلك. يعمل الفحص قبل أي معالجة 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=`. أدرج ما تتيحه فعلًا في قائمة السماح: + +```python title="server.py" hl_lines="2 13-17" +--8<-- "docs_src/deploy/tutorial001.py" +``` + +* إدخالات `allowed_hosts` سلاسل دقيقة: يطابق `"mcp.example.com"` ترويسة `Host` دون منفذ، ويطابق `"mcp.example.com:*"` أي منفذ. أدرج كليهما. +* لا تهم `allowed_origins` إلا للمتصفحات، فلا يرسل غيرها `Origin`. وهي النظير من جانب الخادم لإعداد CORS في **[الإضافة إلى تطبيق موجود](asgi.md)**. +* خلف وكيل عكسي يتحكم في ترويسة `Host` بالفعل، يكون تعطيل الفحص إعدادًا يعكس واقع البنية: `TransportSecuritySettings(enable_dns_rebinding_protection=False)`. +* تمرير `host=` غير localhost (مثل `host="mcp.example.com"`) **لا** يضيف الاسم إلى قائمة السماح. يمنع فقط افتراضي localhost من تفعيل الحماية، فيُقبَل كل Host وOrigin. صرّح بما تقصده باستخدام `transport_security=` بدلًا من ذلك. + +!!! check + احذف الوسيطة `transport_security=security` وانشر التطبيق رغم ذلك. يبدأ، ويعمل توجيه `/mcp`، + لكن كل طلب (حتى باستخدام `curl` عادي) يعيد: + + ```text + HTTP/1.1 421 Misdirected Request + + Invalid Host header + ``` + + لن تجد تلك الكلمات لدى العميل. `421` استجابة HTTP بنص عادي، لا + خطأ JSON-RPC، لذلك يثير عميل MCP خطأ نقل عامًا؛ ولا يظهر اسم المضيف + الذي رُفض إلا في سجل **الخادم** كتحذير واحد. إذا رفض خادم منشور حديثًا + كل اتصال، فافحص قائمة السماح لترويسة Host أولًا حتى يثبت سبب آخر. + تبدأ **[استكشاف الأخطاء وإصلاحها](../troubleshooting.md)** هنا أيضًا. + +## خلف وكيل ينهي TLS {#behind-a-tls-terminating-proxy} + +إذا انتهى TLS عند وكيل (نقطة دخول أو موازن حمل أو Caddy أو nginx) وكان uvicorn يخدم HTTP عاديًا خلفه، فأخبر uvicorn بالثقة بترويسات الوكيل `X-Forwarded-*`: + +```console +uvicorn server:app --proxy-headers --forwarded-allow-ips='' +``` + +دون ذلك، يعتقد التطبيق أنه يُتاح عبر `http://`، ويشير أي تحويل يصدره (المعتاد `/mcp` → `/mcp/`) إلى `http://…`. يرفض عميل Python الانتقال من نقطة نهاية HTTPS إلى HTTP عادي، ويصرّح بذلك: + +```text +MCPError: Redirect to http://mcp.example.com/mcp/ not followed: it would downgrade this HTTPS endpoint to plain HTTP. +``` + +الحل المؤقت لدى العميل هو ضبط URL الدقيق الذي يخدمه الخادم (`https://mcp.example.com/mcp/`، مع الشرطة النهائية) كي لا يحدث تحويل. أما الإصلاح فهو الخيار أعلاه. `FORWARDED_ALLOW_IPS` صيغة متغير البيئة؛ ويثق `*` بكل وسيط، وهذا مناسب فقط عندما لا يستطيع الوصول إلى uvicorn إلا الوكيل. + +## العمال ومتى يلزم ثبات التوجيه {#workers-and-who-has-to-be-sticky} + +بعد أن يجيب اسم المضيف، ضع أكثر من عامل خلفه. لا يوجد خيار SDK لذلك؛ توسّع تطبيق Starlette مثل أي تطبيق ASGI، بتسليم الكائن إلى أداة تعرف إنشاء عمليات متعددة: + +```console +uvicorn server:app --workers 4 +``` + +أربع عمليات ومقبس واحد. والآن السؤال الذي يجب أن يجيب عنه كل نشر: **هل يجب أن يصل الطلب إلى العامل الذي رأى الطلب السابق؟** + +لعميل يتحدث بروتوكول **2026-07-28**، لا. الطلب الحديث POST واحد مكتفٍ بذاته: لا مصافحة `initialize` قبله، ولا `Mcp-Session-Id` على الاستجابة، ولا شيء يعود *إليه* الطلب الثاني. وجّهه لأي عامل. + +هذا ليس وضعًا تفعّله. تبدو `stateless_http=True` كذلك، لكن وسيلة النقل توجّه حسب ترويسة الطلب `MCP-Protocol-Version`، وتسلّم الطلب الحديث إلى دالة معالجته، ثم **تعود**. السطر الذي يقرأ `stateless_http` يأتي *بعد* تلك العودة. لا يُتجاهَل الخيار في مسار 2026-07-28 فحسب؛ بل لا يُوصَل إليه أصلًا. `stateless_http` خيار للمسار **القديم** فقط، والمسار الحديث دون جلسات بطبيعته. + +لعميل قديم بإصدار مواصفة 2025-11-25 أو أقدم، تعتمد الإجابة على ذلك الخيار: + +| إصدار بروتوكول العميل | الجلسة | ما يجب أن يفعله موازن الحمل | +| --- | --- | --- | +| **2026-07-28** | لا توجد. لا يُعيَّن `Mcp-Session-Id` أبدًا. | لا شيء. يخدم أي عامل أي طلب. | +| **2025-11-25 وما قبله** (الافتراضي) | `Mcp-Session-Id` محفوظ في ذاكرة عامل واحد. | **جلسات ثابتة التوجيه.** يحصل طلب لاحق يصل إلى عامل مختلف على `404` برسالة *"الجلسة غير موجودة"*. | +| **2025-11-25 وما قبله** مع `stateless_http=True` | لا توجد. | لا شيء. التكلفة فقدان القناة العكسية من الخادم إلى العميل (أخذ العينات ودفع استقاء المعلومات و`roots/list`) وإمكانية الاستئناف. | + +للجلسات ثابتة التوجيه وتكلفة المسار القديم صفحة خاصة، **[خدمة العملاء القدامى](legacy-clients.md)**؛ وللجيلين نفسيهما **[إصدارات البروتوكول](../protocol-versions.md)**. المهم هنا شكل الإجابة: *في 2026-07-28، أنت عديم الحالة بالفعل دون إعداد.* + +بقية الصفحة تتناول شيئين **لا** يمنحك إياهما انعدام الحالة. + +## `requestState` عبر العمال {#requeststate-across-workers} + +تحتاج أداة **[متعددة جولات التبادل](../handlers/multi-round-trip.md)** إلى شيء يجب أن يجلبه العميل (تأكيد أو اختيار أو بيانات اعتماد)، فتعيد سؤالًا بدلًا من إجابة وتنتهي عند إعادة المحاولة. بين الجولتين، يحتفظ العميل برمز `request_state` غير قابل للتفسير أصدره الخادم. وعند إعادة المحاولة، يجب أن يفتحه الخادم مجددًا. + +*بأي مفتاح حُمي؟* افتراضيًا، بمفتاح ولّده الخادم باستخدام `os.urandom(32)` عند الإنشاء. مع `--workers 4`، يحدث الإنشاء أربع مرات في أربع عمليات: أربعة مفاتيح مختلفة لا تُكتب في مكان ولا تُشارك وتزول عند إعادة التشغيل. + +إليك أداة تسأل قبل التنفيذ على خادم لا يضبط شيئًا: + +```python title="server.py" hl_lines="14 20" +--8<-- "docs_src/deploy/tutorial002.py" +``` + +تصل الجولة الأولى إلى العامل A. يحمي A القيمة `refund:120` بمفتاحه **الخاص** ويعيد الرمز. يعرض العميل السؤال على شخص ويتلقى نعم ويعيد المحاولة. وهي طلب HTTP جديد تمامًا. + +!!! check + دع إعادة المحاولة تصل إلى العامل B. يحاول B فتح رمز لم يصدره، فلا يستطيع ويرفض + الجولة كلها. لا تُستدعى `refund` أبدًا؛ ويتلقى العميل خطأ JSON-RPC: + + ```json + { + "code": -32602, + "message": "Invalid or expired requestState", + "data": {"reason": "invalid_request_state"} + } + ``` + + تلك الرسالة **ثابتة**. انتهاء الصلاحية أو العبث أو إعادة الاستخدام مع وسائط مختلفة أو + الحماية بمفتاح عامل آخر (وهو السبب الأكثر شيوعًا بفارق كبير في النشر الفعلي): يتلقى العميل + الرسالة نفسها كل مرة، فلا يكشف النقل أي فحص فشل. السبب الحقيقي يظهر كتحذير + `WARNING` واحد في سجل الخادم: + + ```text + requestState rejected on tools/call: unknown key + ``` + + إذا عملت أداة متعددة الجولات مع عامل واحد وبدأت تفشل *أحيانًا* عند + استخدام عاملين، فهذا السبب. ما زال يجب أن تصل الجولتان إلى العملية نفسها، لذا تفشل كلما + فرّق موازن الحمل بينهما. + +الجولتان طلبا HTTP مستقلان، وقد تفصل بينهما أمور عادية عديدة: وكيل يوازن كل طلب، أو اتصال انقطع بينهما، أو نشر أو إعادة تشغيل، أو عميل حفظ `request_state` ويستأنف من عملية مختلفة تمامًا (**[إدارة الحلقة بنفسك](../handlers/multi-round-trip.md#driving-the-loop-yourself)**). كل ذلك يعادل "عاملًا مختلفًا". + +الإصلاح وسيطة واحدة. وله **نصفان**. + +```python title="server.py" hl_lines="1 12 14" +--8<-- "docs_src/deploy/tutorial003.py" +``` + +* **`keys=[...]`** هو النصف الذي يجده الجميع. أعطِ كل نسخة السر نفسه (32 بايتًا على الأقل)، فتستطيع كل نسخة فتح ما أصدرته الأخرى. يحمي `keys[0]` وتفتح كل المفاتيح في القائمة، وهي حلقة التدوير؛ وتشرح **[تدوير المفاتيح](../handlers/multi-round-trip.md#rotating-keys)** إدارتها دون توقف. +* **اسم الخادم** هو النصف الذي يكاد لا ينتبه إليه أحد، وسبب استمرار إخفاق إعادة المحاولة بين النسخ بعد مشاركة المفتاح. يحمل كل رمز محمي `name` للخادم كـ**حقل الجمهور**، ويُفحص بدقة عند عودته. تملك نسختان مبنيتان من الشيفرة نفسها الاسم نفسه، فلا تلاحظان ذلك. سمّهما بصورة مختلفة (`MCPServer(f"billing-{POD}")` يبدو مناسبًا للرصد)، فتُرفض كل إعادة محاولة بينهما كما سبق، سواء تشاركتا المفتاح أم لا. يقول السجل `audience` بدلًا من `unknown key`؛ ولا يستطيع العميل تمييز الفرق. + +أنشئ السر مرة واحدة وسلّم القيمة نفسها لكل نسخة. هذا هو الأمر الذي تطلب منك رسالة خطأ SDK تشغيله إذا مرّرت أقل من 32 بايتًا: + +```console +python -c "import secrets; print(secrets.token_hex(32))" +``` + +!!! warning "المفاتيح نفسها *والاسم نفسه*" + يجب أن تشترك نسخ النشر في كليهما. إذا كانت الأسماء الخاصة بكل نسخة ضرورية لديك، + فامنح المجموعة جمهورًا صريحًا واحدًا: `RequestStateSecurity(keys=[...], audience="billing")`. + تصدر كل نسخة وتقبل تحت `"billing"` مهما كان اسمها. + +كل ما عدا ذلك عن الحماية في **[حماية `requestState`](../handlers/multi-round-trip.md#protecting-requeststate)**: ما تربطه، و`ttl` لكل جولة (600 ثانية افتراضيًا)، واستخدام مرمّزك، ولماذا يناسب الافتراضي دون إعداد `stdio` تمامًا. إسهام هذه الصفحة قائمة من عنصرين: *المفاتيح نفسها، والاسم نفسه.* + +!!! info + أنت تستخدم هذا المسار حتى إذا لم تكتب `InputRequiredResult` أبدًا. الأداة التي تستخدم مَعلماتها + `Resolve(...)` (**[الاعتماديات](../handlers/dependencies.md)**) أداة متعددة الجولات، + وتصدر SDK حقل `request_state` وتحميه نيابة عنها. المفتاح الافتراضي نفسه، والإخفاق نفسه عبر + العمال، والإصلاح نفسه. + +## إشعارات التغيير عبر النسخ {#change-notifications-across-replicas} + +تدفّق `subscriptions/listen` لدى العميل استجابة واحدة طويلة العمر، لذلك يبقى مرتبطًا بنسخة واحدة طوال حياته. يجب أن تصله `ctx.notify_resource_updated(...)` المنشورة على نسخة **أخرى**. + +الصلة بينهما `SubscriptionBus`. الناقل الذي تعطيه للخادم هو ما يستقبل كل نشر ويستمع إليه كل تدفّق مفتوح، فأعطِ كل نسخة الناقل نفسه: + +```python title="server.py" hl_lines="2 7 9" +--8<-- "docs_src/deploy/tutorial004.py" +``` + +لا يهم توزيع الأحداث أي كائن خادم يرتبط به التدفّق. يتصرف خادمان يملكان `InMemorySubscriptionBus` واحدة بهذه الطريقة: افتح تدفق استماع على أحدهما، ونفّذ `edit_note` على الآخر، فيسمع التدفّق عنه. لا يشمل الناقل داخل الذاكرة إلا كائنات خوادم في عملية واحدة، لذا فهو نموذج توضيحي وليس بنية نشر: + +* عبر عمليات فعلية، **لا توفر SDK ناقلًا يساعدك.** `SubscriptionBus` بروتوكول `Protocol` بطريقتين (`publish` و`subscribe`) تنفّذه فوق نظام النشر والاشتراك لديك (Redis أو NATS أو ما تستخدمه بالفعل)، وتمرّره بصيغة `MCPServer(subscriptions=...)`. تحتوي **[الاشتراكات](../handlers/subscriptions.md#scaling-past-one-process)** على المثال والعقد. +* يحمل الناقل أربعة أحداث صغيرة محددة النوع، لا JSON-RPC أبدًا. يبقى الإقرار والترشيح ودورة حياة التدفّق في SDK، فلا يستطيع ناقلك كسر البروتوكول؛ بل ينقل الأحداث بين العمليات فقط. +* التدفّقات **غير** قابلة للاستئناف والأحداث **لا** تُعاد. فقدان نسخة يقطع تدفّقاتها؛ يعيد العملاء الاستماع والجلب. لا مخزن أحداث لمشاركته ولا إعداد آخر. هذا الموضع الذي يكون فيه التوسع فعلًا تكرارًا للبنية نفسها. +* يتجاوز الخادم الذي لا يحتاج إلى إشعارات تغيير الناقل: **[عطّلها](../handlers/subscriptions.md#turning-it-off)**. + +## ما لا توفره SDK {#what-the-sdk-does-not-give-you} + +`MCPServer` تنفيذ بروتوكول، وليس خادم تطبيقات. خيارات النشر التي تبحث عنها لاحقًا غائبة عمدًا: + +* **لا `workers=`.** تبدأ `mcp.run("streamable-http")` عملية uvicorn واحدة بالضبط، ولن تبدأ غيرها. تعدد العمليات هو تسليم `streamable_http_app()` إلى ما تنشر به ASGI بالفعل: `uvicorn --workers` أو gunicorn أو مدير عمليات منصتك. هذه الصفحة ليست دليلًا لأي منها عمدًا؛ فتوثيقها أفضل من نسخه هنا. +* **لا مسار فحص صحة جاهز.** `@mcp.custom_route("/health", methods=["GET"])` هو الحل كاملًا، ولا يُفرَض عليه التحقق من المصادقة حتى عندما تُحمى بقية الخادم. هذا مناسب لفحص الحياة، وغير مناسب لأي شيء خاص. تعرض **[الإضافة إلى تطبيق موجود](asgi.md#custom-routes)** مثالًا. +* **لا كائن إعدادات إنتاج.** لا موضع على `MCPServer` لتحديد المهل أو TLS أو الإغلاق السلس أو حدود الاتصالات، لأنها ليست مسؤوليته. تخص خادم ASGI وتُضبط فيه. تغطي **[تشغيل خادمك](index.md)** الإعدادات القليلة التي *يقبلها* المُنشئ. +* **لا `EventStore` جاهز، ولا حاجة إليه في 2026-07-28.** الاستئناف ميزة للمسار القديم ذي الحالة؛ أما التبادل الحديث فـPOST واحد واستجابة واحدة ولا شيء لاستئنافه. + +## مراجعة {#recap} + +* يجيب التطبيق افتراضيًا عن طلبات localhost فقط. `transport_security=TransportSecuritySettings(allowed_hosts=[...], allowed_origins=[...])` شرط الانتقال إلى النشر الفعلي: حتى تمرّره، يكون كل طلب خلف اسم مضيف فعلي `421` ولا يظهر السبب إلا في سجل الخادم. +* خلف وكيل ينهي TLS، شغّل uvicorn مع `--proxy-headers --forwarded-allow-ips=...`، وإلا تشير تحويلاته إلى `http://` ويرفضها العميل. +* في 2026-07-28، لا توجد جلسة يحتاج موازن الحمل إلى تثبيت توجيهها. `stateless_http=True` خاص بالبروتوكول القديم، لأن الطلب الحديث يُوجَّه ويُجاب عنه قبل قراءة الخيار أصلًا. +* مفتاح `requestState` الافتراضي هو `os.urandom(32)`، ويُنشأ لكل عملية. تفشل إعادة محاولة متعددة الجولات تصل إلى عامل مختلف بـ`-32602` ورسالة *"requestState غير صالحة أو منتهية الصلاحية"*. +* الإصلاح `RequestStateSecurity(keys=[...])` **مع** اسم الخادم نفسه على كل نسخة. الاسم حقل الجمهور الافتراضي للرمز. المفاتيح نفسها والاسم نفسه. +* تعبر إشعارات التغيير النسخ عبر `SubscriptionBus` مشتركة واحدة. تنفيذ SDK الوحيد داخل العملية؛ وأنت تكتب `Protocol` بطريقتين فوق نظام النشر والاشتراك لديك. +* لا `workers=` ولا مسار صحة ولا كائن إعدادات إنتاج. وفّر خادم ASGI الخاص بك. + +الشيء الآخر الذي يحتاج إليه اسم مضيف فعلي أمامه هو رمز: **[التفويض](authorization.md)**. diff --git a/i18n/ar/pages/run/index.md b/i18n/ar/pages/run/index.md new file mode 100644 index 0000000000..5015691de3 --- /dev/null +++ b/i18n/ar/pages/run/index.md @@ -0,0 +1,161 @@ +--- +translation: + sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 9fd2357154a5b7e7, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0] + tool: 1 +--- +# تشغيل خادمك {#running-your-server} + +تبدأ `mcp.run()` تشغيل الخادم. + +القرار الوحيد الذي تتخذه هو **وسيلة النقل**: كيف تنتقل البايتات فعليًا بين خادمك وعميله. + +## اختر وسيلة نقل {#pick-a-transport} + +| وسيلة النقل | ماهيتها | متى تستخدمها؟ | +|---|---|---| +| `stdio` | يشغّل المضيف ملفك كعملية فرعية ويتواصل عبر stdin وstdout الخاصين بها. | الخوادم المحلية. وهي الافتراضية. | +| `streamable-http` | خادم HTTP فعلي يستمع على منفذ. | كل ما تنشره. | +| `sse` | وسيلة نقل HTTP الأقدم. | لا تستخدمها في تطوير جديد. | + +!!! warning + حلّت Streamable HTTP محل SSE في مراجعة البروتوكول 2025-03-26. + ما زالت `mcp.run(transport="sse")` تعمل، مع خياري `sse_path=` و`message_path=` + الخاصين بها، لكنها موجودة للعملاء الذين لم ينتقلوا بعد. لا تبنِ شيئًا جديدًا عليها. + +## `mcp.run()` {#mcprun} + +```python title="server.py" hl_lines="12-13" +--8<-- "docs_src/run/tutorial001.py" +``` + +* `run()` متزامنة. تحجب التنفيذ طوال حياة الخادم. +* دون وسيطة، تكون وسيلة النقل `stdio`. +* توضع تحت `if __name__ == "__main__":` لأن كل ما يحمّل خادمك (`mcp dev` و`mcp run` و`mcp install` واختباراتك) **يستورد** هذا الملف. يمنع شرط الحماية تحوّل الاستيراد إلى خادم يعمل. + +### stdio {#stdio} + +لا شيء لإعداده. يبدأ المضيف ملفك كعملية ابنة، ويكتب الطلبات إلى stdin، ويقرأ الاستجابات من stdout. + +شغّله بنفسك لترى النتيجة: + +```console +python server.py +``` + +لا تُطبع مخرجات ولا ينتهي الأمر. ينتظر على stdin أن يبدأ المضيف الحديث. + +هذا يعني أيضًا أن stdout **هو قناة النقل**. أثناء الخدمة، تنقل SDK البروتوكول إلى واصف خاص وتحول المخرجات *المفرَّغة* إلى stdout (عملية فرعية تكتب إلى stdout الموروث أو `print()` مفرَّغة) إلى stderr، فلا تفسد التدفّق. أما المخرجات المفرَّغة إلى stdout *قبل* بدء الخدمة (سكربت تغليف يطبع نصًا أو طباعة دون تخزين مؤقت أثناء الاستيراد)، فتصل إلى النقل، وكذلك `print()` التي تبقى مخزّنة حتى يفرّغها المفسّر عند الخروج. للمخرجات التي تريدها فعلًا، استخدم وحدة `logging`: تفرّغ دالة معالجتها كل سجل إلى stderr فورًا. تشرح ذلك **[التسجيل](../handlers/logging.md)**. + +### جرّبه {#try-it} + +```console +uv run mcp dev server.py +``` + +يفعل Inspector ما يفعله مضيف فعلي بالضبط: يشغّل `server.py` كعملية فرعية ويتصل به عبر stdio. + +لم تعطه منفذًا. لا يوجد منفذ أصلًا. + +## Streamable HTTP {#streamable-http} + +لوضع الخادم نفسه على منفذ بدلًا من ذلك، حدد وسيلة النقل وخياراتها في `run()`: + +```python title="server.py" hl_lines="13" +--8<-- "docs_src/run/tutorial002.py" +``` + +يبني هذا السطر تطبيق Starlette ويخدمه باستخدام uvicorn. يتصل العملاء بـ`http://127.0.0.1:3001/mcp`. + +لكل وسيلة نقل وسائط مسمّاة خاصة بها، جميعها على `run()`: + +* `host` / `port`: موضع الاستماع. الافتراضيان `127.0.0.1` و`8000`. +* `streamable_http_path`: موضع نقطة نهاية MCP. الافتراضي `/mcp`. +* `json_response=True`: أجب عن كل POST بجسم JSON واحد بدلًا من تدفّق SSE. لا يتسع ذلك الجسم إلا للاستجابة، لذا تثير الأداة التي تستدعي العميل أثناء الطلب (`ctx.elicit()` أو أخذ العينات) الاستثناء `NoBackChannelError` في هذه المرحلة، وتُهمَل الإشعارات المرتبطة بالاستدعاء الجاري (تقدم `ctx.report_progress()` ورسائل السجل لكل استدعاء)؛ وما زال تدفّق `GET` المستقل يحمل الإشعارات غير المرتبطة به. +* `stateless_http=True`: وسيلة نقل جديدة لكل طلب، دون تتبّع جلسات. +* `max_request_body_size`: أكبر جسم طلب مقبول بالبايتات. الافتراضي 4 MiB؛ تتلقى الطلبات الأكبر + HTTP 413 قبل التحليل أو إنشاء جلسة. ارفعه فقط عندما تتجاوز رسائل MCP المشروعة + هذا الحجم. +* `session_idle_timeout`: عدد الثواني التي يجوز لجلسة قديمة بقاؤها دون طلبات جارية قبل + أن يغلقها الخادم. الافتراضي 1800. تعطلها `None`. راجع + [عمر الجلسة وحدودها](legacy-clients.md#session-lifetime-and-limits). +* `max_sessions`: عدد الجلسات القديمة التي تحتفظ بها عملية واحدة في الوقت نفسه. الافتراضي 10 000. تزيل `None` + الحد. ويغطيها القسم نفسه. +* `event_store` و`retry_interval` و`transport_security`: إمكانية الاستئناف والحماية من إعادة ربط DNS. يمكنك تأجيلها حتى النشر خارج localhost؛ وتغطي **[النشر والتوسع](deploy.md)** الخيار `transport_security`. + +!!! warning + خيارات النقل تُمرَّر إلى `run()`، **لا** إلى `MCPServer(...)`. يصف المُنشئ + *هوية* خادمك: اسمه وإصداره وتعليماته. وتصف `run()` كيفية إتاحته. إذا عكست ذلك، + تجيب Python قبل أن يدخل MCP في الأمر: + + ```text + TypeError: MCPServer.__init__() got an unexpected keyword argument 'port' + ``` + +`run()` هي الطريق المختصر. عندما تحتاج إلى أكثر (تركيب الخادم داخل تطبيق موجود، أو خادمين في عملية واحدة، أو CORS لعملاء المتصفح)، تبني تطبيق ASGI بنفسك وتسلّمه لأي مضيف ASGI. هذا موضوع **[الإضافة إلى تطبيق موجود](asgi.md)**. + +## إعدادات الخادم {#server-settings} + +لا يتعلق بعض ما يخص التشغيل بوسيلة النقل. هذه وسائط للمُنشئ: + +```python title="server.py" hl_lines="3" +--8<-- "docs_src/run/tutorial003.py" +``` + +* `log_level`: يُمرَّر إلى `logging.basicConfig()` عند إنشاء `MCPServer(...)`. يضبط ذلك المسجّل **الجذري**، فيحدد مستوى مسجّلاتك أيضًا، وليس مسجّلات SDK فقط. الافتراضي `"INFO"`. +* `debug`: يُمرَّر إلى تطبيق Starlette الذي تبنيه وسائل نقل HTTP. الافتراضي `False`. + +يوجد كلاهما على `mcp.settings`، التي يمكنك قراءتها أثناء التشغيل. + +## الأمر `mcp` {#the-mcp-command} + +تثبّت الإضافة `[cli]` أداة سطر أوامر صغيرة لكل ذلك. + +يشغّل `mcp dev` خادمك ضمن **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` حزمًا إلى البيئة التي يبنيها؛ ويثبّت `--with-editable` حزمتك فيها. يحتاج إلى `npx` ضمن `PATH`: Inspector تطبيق Node.js. + +يستورد `mcp run` الملف، ويعثر على كائن الخادم (`mcp` أو `server` أو `app` على مستوى الوحدة)، ويستدعي `run()` عليه: + +```console +uv run mcp run server.py +uv run mcp run server.py:bookshop +``` + +تسمّي لاحقة `:` الكائن عندما لا يكون اسمه `mcp` أو `server` أو `app`. + +لا تُنفَّذ كتلة `if __name__ == "__main__":` هنا: يستدعي `mcp run` الدالة `run()` بنفسه، والخيار الوحيد الذي يمرّره هو `--transport`. + +يسجّل `mcp install` الخادم في **Claude Desktop** كي يشغّله التطبيق نيابة عنك: + +```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` متغيرات البيئة في الإدخال. يبدأ Claude Desktop خادمك في عملية خاصة به. لا توجد فيها بيئة صدفتك. + +Claude Desktop هو المضيف الوحيد الذي يعرفه `mcp install`. يأخذ كل مضيف آخر (Claude Code أو Cursor أو VS Code) أمر التشغيل نفسه في ملف إعداداته، وتشرح **[الاتصال بتطبيق مضيف فعلي](../get-started/real-host.md)** كلًّا منها. + +يطبع `mcp version` إصدار SDK المثبّت. + +!!! tip + لا يفهم `mcp dev` و`mcp run` إلا `MCPServer`. إذا بنيت باستخدام `Server` منخفض المستوى، + فتشغّله بنفسك. راجع **[الخادم منخفض المستوى](../advanced/low-level-server.md)**. + +## مراجعة {#recap} + +* **وسيلة النقل** كيفية وصول البايتات إلى خادمك: `stdio` لعملية فرعية محلية، و`streamable-http` لمنفذ. استُبدلت SSE. +* تختار `mcp.run()` وسيلة النقل. دون وسيطة، تستخدم `stdio` وتحجب التنفيذ. +* كل خيار نقل (`host` و`port` و`streamable_http_path`، ...) وسيطة لـ`run()`، لا لـ`MCPServer(...)` أبدًا. +* أبقِ `run()` تحت `if __name__ == "__main__":`. يستورد كل ما يحمّل خادمك الملف أولًا. +* `log_level=` و`debug=` وسيطتان للمُنشئ؛ وتوجدان على `mcp.settings`. +* استخدم `mcp dev` لـInspector، و`mcp run` لتنفيذ ملف، و`mcp install` لـClaude Desktop، و`mcp version` للإصدار. +* لا تغيّر وسيلة النقل *ماهية* خادمك: تتيح الملفات الثلاثة في هذه الصفحة الأداة نفسها. + +عندما تصل إلى حدود `run()` (خادمك داخل تطبيق موجود)، راجع **[الإضافة إلى تطبيق موجود](asgi.md)**. واسم المضيف الفعلي وأكثر من عامل واحد موضوع **[النشر والتوسع](deploy.md)**. وإذا كان بعض عملائك ما زال على إصدار المواصفة 2025-11-25 أو أقدم، فهناك أخبار جيدة في **[خدمة العملاء القدامى](legacy-clients.md)**. diff --git a/i18n/ar/pages/run/legacy-clients.md b/i18n/ar/pages/run/legacy-clients.md new file mode 100644 index 0000000000..a2132cb6e4 --- /dev/null +++ b/i18n/ar/pages/run/legacy-clients.md @@ -0,0 +1,176 @@ +--- +translation: + sections: [3d1663c18edc824c, 90956965ae6a1ca1, af9f398a5a8b679a, 5ce83b1f9d88da62, 0d9b5d13fffc94e5, 8e45827e6d24e8c8, 91dfd0ce98ebb03c] + tool: 1 +--- +# خدمة العملاء القدامى {#serving-legacy-clients} + +لـMCP جيلان من البروتوكول: جيل مصافحة `initialize` حتى إصدار المواصفة `2025-11-25`، والجيل الحديث `2026-07-28`. تشرح **[إصدارات البروتوكول](../protocol-versions.md)** هذا الفصل. + +تتناول هذه الصفحة جانب الخادم منه، والإجابة جملة واحدة: **تخدم `streamable_http_app()` التي تنشرها بالفعل الجيلين.** + +توجّه SDK كل طلب بحسب ترويسة `MCP-Protocol-Version`. يذهب طلب يحدد `2026-07-28` إلى دالة المعالجة الحديثة. ويذهب طلب يحدد إصدار جيل المصافحة أو لا يحمل ترويسة أصلًا (وهو شكل وصول `initialize` من عميل أقدم من 2026) إلى وسيلة النقل التي يتوقعها هؤلاء العملاء: مصافحة `initialize` والجلسات وكل ما معها. يحدث ذلك لكل طلب قبل شيفرتك على التطبيق الواحد. + +لذلك لا تبني خادمًا *خصيصًا* للعميل القديم. بل يتصل العميل القديم *بالخادم* الذي كتبته بالفعل. لا تضبط شيئًا. + +!!! note + لا شيء حرفيًا. لا خيار `legacy=`، ولا قائمة سماح للإصدارات، ولا وسيلة لرفض جيل + أو تعطيله: لا على `streamable_http_app()` ولا `run()` ولا مدير الجلسات. + الجيلان مفعّلان دائمًا. أقرب شيء إلى خيار لكل جيل في ذلك التوقيع هو + `stateless_http`، وهو موضوع معظم الصفحة. + +## دالة معالجة واحدة للجيلين {#one-handler-both-eras} + +إليك أداة تحتاج إلى سؤال المستخدم: + +```python title="server.py" hl_lines="21" +--8<-- "docs_src/legacy_clients/tutorial001.py" +``` + +تحتاج `reserve` إلى شيء لم يقدّمه النموذج: عدد النسخ. `Annotated[..., Resolve(ask_quantity)]` طريقة إعلان الأداة ذلك (تشرح **[الاعتماديات](../handlers/dependencies.md)** الآلية كاملة). لا شيء في `reserve` يسمّي إصدارًا أو يفحص قدرة أو يتفرع. + +أتحها عبر HTTP، وإليك عميلين من الجيلين يستدعيانها: + +```console +uv run mcp run server.py --transport streamable-http +``` + +```python title="client.py" hl_lines="14-15" +--8<-- "docs_src/legacy_clients/tutorial001_client.py" +``` + +العميلان مفتوحان **في الوقت نفسه** على الخادم الجاري نفسه. ينفّذ `mode="legacy"` مصافحة `initialize`: الاتصال نفسه الذي يفتحه عميل أقدم من 2026. يأخذ الآخر الافتراضي ويصل إلى `2026-07-28`. شغّل `python client.py` من نافذة طرفية ثانية: + +```text +2025-11-25 {'result': "Reserved 2 of 'Dune'."} +2026-07-28 {'result': "Reserved 2 of 'Dune'."} +``` + +الخادم نفسه ودالة المعالجة نفسها والإجابة نفسها. هذه الميزة كاملة. + +من المفيد فهم *الكيفية*، لأن العميلين تلقيا السؤال نفسه عبر مسارين مختلفين تمامًا. لا يملك اتصال `2026-07-28` قناة يرسل الخادم طلبًا عليها، لذلك أعادت `Resolve` السؤال داخل نتيجة الأداة وأعاد العميل الاستدعاء بالإجابة (**[الطلبات متعددة جولات التبادل](../handlers/multi-round-trip.md)**). لا يملك اتصال `2025-11-25` هذه الآلية؛ أرسلت فيه `Resolve` طلب `elicitation/create` مباشرًا أثناء الاستدعاء وانتظرت. لم تكتب أيًّا منهما. تقرأ `Resolve` الإصدار المتفاوض عليه وتختار؛ ويرى جسم أداتك `AcceptedElicitation` في الحالتين. + +!!! tip + هذه القابلية للعمل عبر الأجيال هي *سبب* اعتماد `Resolve` للبناء. لا ترسل الطريقة الأقدم `ctx.elicit()` + (**[استقاء المعلومات](../handlers/elicitation.md)**) إلا `elicitation/create`، لذا لا تعمل + إلا على اتصال قديم. تفشل على اتصال `2026-07-28`. إذا ظلت أداة تستخدمها، + فالإصلاح هو ما تراه أعلاه، لا فحص الإصدار. + +## تكلفة الجلسة القديمة {#what-a-legacy-session-costs-you} + +التوجيه بلا تكلفة. الجلسة ليست كذلك. + +اتصال `2026-07-28` **دون جلسة**: كل طلب مستقل، ولا تصدر دالة المعالجة الحديثة `Mcp-Session-Id` أبدًا. الاتصال القديم عكس ذلك. بمجرد إرسال عميل أقدم من 2026 طلب `initialize`، تصدر SDK معرّف `Mcp-Session-Id` وتعيده في ترويسة الاستجابة، وتحتفظ خلفه بسجل نشط تعثر عليه طلبات العميل اللاحقة: الإصدار المتفاوض عليه والتدفّقات المفتوحة ومهمة خلفية تدير الجلسة. + +ذلك السجل **`dict` عادية داخل العملية**. لا مخزن جلسات موزع ولا وسيلة لإضافة واحد. + +مع عامل واحد لا تلاحظ ذلك. ومع عاملين يصبح المشكلة كلها: لا يعثر طلب يحمل `Mcp-Session-Id` ويصل إلى عامل لم يصدره على شيء في ذلك القاموس، فتكون الإجابة `404` (`Session not found`) لا نتيجة الأداة. لذلك بمجرد تشغيل أكثر من عامل، **يحتاج العملاء القدامى إلى توجيه ثابت**: يجب أن يصل كل طلب في الجلسة إلى العملية التي بدأتها. لا يحتاج العملاء الحديثون إلى ذلك؛ فلا جلسة لديهم لتثبيت التوجيه عليها. تغطي **[النشر والتوسع](deploy.md)** ثبات التوجيه وبقية تشغيل عدة نسخ. + +!!! warning + تبدو `event_store=` الحل، لكنها ليست كذلك. إنها **إمكانية الاستئناف** (إعادة أحداث SSE الفائتة + إلى عميل يعود إلى *الجلسة نفسها*)، وليست مخزن جلسات. لا تجعل الجلسة + متاحة من عملية أخرى أبدًا. + +## عمر الجلسة وحدودها {#session-lifetime-and-limits} + +لا تعيش الجلسة القديمة إلى الأبد، ولا تحتفظ عملية واحدة بعدد غير محدود +منها. يتحكم إعدادان بذلك. كلاهما وسيطتان مسمّاتان في `run()` و`streamable_http_app()` +و`Server.streamable_http_app()`. لا توجد جلسات في الاتصالات الحديثة (`2026-07-28`) أو مع `stateless_http=True`، +لذلك لا ينطبق عليهما أي إعداد. + +| الإعداد | الافتراضي | ما يفعله | ما يراه العميل | تعطيله | +|---|---|---|---|---| +| `session_idle_timeout` | `1800` (30 دقيقة) | يغلق جلسة لم يوجد فيها عمل جارٍ طوال هذه المدة. | `404 Session not found`. يجب تنفيذ `initialize` مجددًا. | `None` | +| `max_sessions` | `10_000` | يرفض فتح جلسة تتجاوز هذا العدد. تبقى الجلسات الموجودة دون تغيير ولا تُطرَد أي منها. | `503 Too many open sessions` مع رمز JSON-RPC `-32603`. | `None` | + +ما يُعد "عملًا جاريًا": + +* تدفّق `GET` مفتوح. تُبقي عملاء SDK واحدًا مفتوحًا، لذا لا تنتهي صلاحية جلسة عميل + متصل. +* طلب لا تزال إجابته تُعالَج. لا تُقاطَع أداة تستغرق أكثر من المهلة، + ولا يبدأ العد التنازلي إلا بعد انتهائها. +* لا شيء غير ذلك. يعمل العداد بين الطلبات. يعيد أي طلب على الجلسة تشغيله، + بما فيه `ping`. بعد انتهاء الجلسة، لا يعيدها شيء. + +يحرر عميل ينهي جلسته باستخدام `DELETE` الجلسة فورًا. وكذلك عميل +رُفض طلب افتتاح جلسته. + +```python +mcp.run(transport="streamable-http", session_idle_timeout=None, max_sessions=50_000) +``` + +يظهر الحدثان في سجل الخادم. انتهاء الصلاحية هو `Session idle timeout` عند `INFO`. ورفض +الافتتاح هو `Refusing to open a new session: sessions are already open` عند `WARNING`. + +الحدود لكل عملية. مع أربعة عمال، يصبح السقف أربعة أضعاف `max_sessions`، وينهي كل +عامل جلساته الخاصة. + +## الخيار الوحيد: `stateless_http` {#the-one-knob-stateless_http} + +إذا رفضت تكلفة ثبات التوجيه، فهناك شيء واحد بالضبط يمكنك تغييره. + +```python title="server.py" hl_lines="28" +--8<-- "docs_src/legacy_clients/tutorial002.py" +``` + +هذا خادم بداية الصفحة مع خيار إضافي واحد. تجعل `stateless_http=True` المسار القديم يبني جلسة مؤقتة لكل طلب: لا يُصدَر `Mcp-Session-Id` ولا يُحفَظ شيء بين الطلبات، فيستطيع أي عامل خدمة أي طلب ويفعل موازن الحمل ما يشاء. + +يهم أمران فيه أكثر مما يفعله. + +**لا يؤثر إلا في المسار القديم.** تُوجَّه الطلبات حسب ترويسة الإصدار *قبل* قراءة `stateless_http`، لذلك لا يراه المسار الحديث. اتصال `2026-07-28` دون جلسة بالفعل، ويتطابق سلوكه مع أي قيمة. + +**يفقدك قناتَي الخادم إلى العميل في ذلك المسار.** الجلسة التي تعيش لـ`POST` واحد لا تملك تدفّقًا يدفع الخادم طلبًا عبره ولا تدفقًا مستقلًا لدفع الإشعارات. يثير كل طلب يبدأه الخادم `NoBackChannelError`: سواء `ctx.elicit()` أو استدعاءات أخذ العينات والمجلدات الجذرية المهجورة (**[الميزات المهجورة](../deprecated.md)**)، وكذلك `Resolve` التي تسأل عميلًا *قديمًا*. لا تحصل الإشعارات حتى على خطأ؛ بل تُهمَل بصمت. + +!!! note + `json_response=True` ليس ذلك الخيار، لكنه يفرض نصف التكلفة نفسها على *كل* جلسة + قديمة: لا توجد قناة خاصة بالطلب عندما يُجاب عن `POST` بجسم JSON واحد، + فتثير `ctx.elicit()` أثناء الطلب `NoBackChannelError` نفسه، وتُهمَل الإشعارات المرتبطة + بالطلب. يبقى التدفّق المستقل للجلسة دون تغيير: ما زالت الإشعارات غير المرتبطة + تصل. + +!!! check + جرّب الاختيار الخاطئ. `reserve` هي الأداة نفسها التي خدمت العميلين للتو. انشرها مع + `stateless_http=True`، وصِل العميلين نفسيهما، واستدعِها من كل منهما. + + ما زال العميل الحديث يتلقى `Reserved 2 of 'Dune'.` لم يتغير المسار الحديث. + + لا يعود استدعاء العميل القديم كنتيجة `is_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`، *يجب* أن ترسل `elicitation/create`، + والقناة التي تحتاج إليها هي بالضبط ما أزالته `stateless_http=True`. الشيفرة القابلة للعمل عبر الأجيال + ليست شيفرة مستغنية عن القناة العكسية. + +هذه مفاضلة فعلية، ولا توجد إلا في المسار القديم: **جلسات مع توجيه ثابت، أو انعدام حالة واتجاه واحد.** إذا لم تستدعِ أدواتك العميل عكسيًا أبدًا، تكون `stateless_http=True` دون تكلفة وينبغي استخدامها. وإلا، فاحتفظ بالجلسات وبثبات التوجيه. + +## أين تتفرع شيفرتك فعلًا؟ {#where-your-code-actually-forks} + +تقريبًا لا تتفرع. + +لا تهتم الأدوات والموارد وقوالب التوجيه والمخرجات المنظّمة والتقدم والأخطاء بالجيل المستدعي. مصافحة `initialize` و`Mcp-Session-Id` والتدفّق المستقل و`DELETE` المنهي للجلسة: تتولى SDK كل ذلك، ولا تراه دالة المعالجة. المدخلات التفاعلية هي الموضع الذي يختلف فيه الجيلان فعلًا أثناء النقل، وتوجد `Resolve` كي لا يكون ذلك مسؤوليتك: رأيت أداة واحدة تخدم الاثنين. + +يبقى شيء واحد بالضبط، وهو **إشعارات التغيير**، لأن الجيلين يستمعان عبر قنوات مختلفة: + +* يفتح عميل `2026-07-28` تدفق `subscriptions/listen` ويقرأ ناقل الاشتراكات. تنشر `ctx.notify_resource_updated()` (و`notify_tools_changed()` و`notify_prompts_changed()` و`notify_resources_changed()`) هناك *فقط*. هذه صفحة **[الاشتراكات](../handlers/subscriptions.md)**. +* يقرأ العميل القديم التدفّق المستقل الذي تُبقيه جلسته مفتوحًا. تكتب `ctx.session.send_resource_updated()` (و`send_tool_list_changed()` وما شابه) إلى *الاتصال* الذي حمل الطلب: أي التدفّق المستقل للجلسة القديمة. لا يوجد موضع لذلك في الاتصال الحديث: عبر HTTP لا توجد تلك القناة، وعبر stdio لا تنتقل أنواع إشعارات التغيير الأربعة إلا على تدفّقات `subscriptions/listen`، لذلك يُهمَل الإشعار بصمت على اتصال حديث. + +عبر HTTP، لا يصل أي استدعاء إلى عملاء الجيل الآخر. لإبلاغ الجميع، استدعِ كليهما: + +```python title="server.py" hl_lines="19-20" +--8<-- "docs_src/legacy_clients/tutorial003.py" +``` + +سطران دون `if` أو فحص إصدار، وينتهي الأمر. هذه القائمة الكاملة لما تفعله دالة المعالجة بصورة مختلفة بسبب وجود عميل قديم. + +## مراجعة {#recap} + +* تخدم `streamable_http_app()` واحدة جيلي البروتوكول. توجّه SDK كل طلب حسب ترويسة `MCP-Protocol-Version`؛ لا إعداد ولا خيار جيل تبحث عنه. +* يكلّفك العميل القديم جلسة: سجل `Mcp-Session-Id` داخل العملية دون مخزن موزع. أكثر من عامل يعني **توجيهًا ثابتًا**، وإلا يجيب العامل الخاطئ بـ`404 Session not found`. تشرح **[النشر والتوسع](deploy.md)** تعدد العمال. +* `stateless_http=True` الخيار الوحيد، وهو **للمسار القديم فقط**. يمنح موازنة حمل دون قيود للعملاء القدامى مقابل فقدان قناتَي الخادم إلى العميل: تثير طلبات الخادم `NoBackChannelError` (خطأ أعلى المستوى لدى العميل، لا نتيجة `is_error`)، وتُهمَل الإشعارات. +* اتصال `2026-07-28` دون جلسة في الحالتين. لا تؤثر فيه `stateless_http` أبدًا. +* تتفرع شيفرة المعالجة بحسب الجيل في موضع واحد فقط: إشعارات التغيير. تصل `ctx.notify_*` إلى عملاء `subscriptions/listen`؛ وتصل `ctx.session.send_*` إلى الجلسات القديمة. استدعِ كليهما. +* كل ما عدا ذلك (بما فيه سؤال المستخدم عبر `Resolve`) قابل للعمل عبر الأجيال بطبيعته. اكتب الشكل الحديث مرة واحدة. diff --git a/i18n/ar/pages/run/opentelemetry.md b/i18n/ar/pages/run/opentelemetry.md new file mode 100644 index 0000000000..45803a1ba6 --- /dev/null +++ b/i18n/ar/pages/run/opentelemetry.md @@ -0,0 +1,112 @@ +--- +translation: + sections: [bc0227014724fa49, 15738c2f7fd67d86, a2c17bbe3f707e2f, d0d853376f162c06, b6368643fcc1c8d8, 902e33e17564a607] + tool: 1 +--- +# OpenTelemetry {#opentelemetry} + +خادمك مجهّز بالتتبّع بالفعل. لا تحتاج إلى إضافة شيء. + +يصدر كل خادم تنشئه مقطع تتبّع [OpenTelemetry](https://opentelemetry.io/) لكل +رسالة يعالجها. لم تكتب ذلك ولا تستورده. يوجد بمجرد +استدعاء `MCPServer(...)`. + +```python title="server.py" +--8<-- "docs_src/opentelemetry/tutorial001.py" +``` + +هذا خادم كامل مزود بالتتبّع. استدعِ `search_books` فيُنشأ له مقطع. وينطبق +الأمر على `Server` منخفض المستوى: يوجد التتبّع في كليهما. + +## ما تحصل عليه {#what-you-get} + +تصبح كل رسالة واردة مقطع `SERVER` يُسمّى بالطريقة والهدف. لذا يكون +مقطع `tools/call` لـ`search_books` هو `tools/call search_books`، ويكون `tools/list` المجرد +هو `tools/list` فقط. + +يحمل كل مقطع بعض الخصائص: + +* `mcp.method.name` و`mcp.protocol.version` على كل مقطع. +* `jsonrpc.request.id` على الطلب (لا يملك الإشعار معرّفًا). +* تضبط دالة معالجة تثير استثناءً حالة المقطع إلى خطأ. وكذلك نتيجة أداة فيها `is_error=True`. + +ولأن تتبّع استدعاء أداة حاجة شائعة، تستخدم مقاطع `tools/call` +[الاتفاقيات الدلالية لـGenAI](https://opentelemetry.io/docs/specs/semconv/gen-ai/) في OpenTelemetry: + +* `gen_ai.operation.name` بقيمة `"execute_tool"`. +* `gen_ai.tool.name` بقيمة الأداة المستدعاة. + +يحصل مقطع `prompts/get` على `gen_ai.prompt.name` بالطريقة نفسها. لا تحمل طرق عرض القوائم +مفاتيح `gen_ai.*`، إذ لا يوجد عنصر لتسميته. + +!!! tip + خصائص GenAI هذه هي ما يجعل واجهة التتبّع تجمع استدعاءات أدواتك كما تجمع + أدوات أي وكيل آخر. تحصل على ذلك دون شيفرة إضافية. + +## بلا تكلفة حتى تحتاج إليه {#it-costs-nothing-until-you-want-it} + +هذا ما يجعل "مفعّل افتراضيًا" إعدادًا مريحًا. + +تعتمد SDK على `opentelemetry-api` فقط، النصف الخفيف من OpenTelemetry. دون تثبيت SDK +ومكوّن تصدير، لا يفعل إنشاء مقطع شيئًا. لذا لا تكلفك المقاطع التي يصدرها خادمك +حاليًا إلا القليل جدًا، ولا يجمعها أحد. + +عندما تريد *رؤيتها*، ثبّت النصف الآخر ووجّهه إلى وجهة: + +```console +uv add opentelemetry-sdk opentelemetry-exporter-otlp +``` + +اضبط مكوّن تصدير بالطريقة المعتادة في OpenTelemetry، فتظهر كل المقاطع التي كانت SDK +تنشئها بصمت. لا تتغير شيفرة خادمك، ولا سطر واحد. + +!!! info + [Pydantic Logfire](https://logfire.pydantic.dev/) أحد هذه الأنظمة، ويتولى + الإعداد عنك: `pip install logfire` ثم `logfire.configure()`، فتظهر مقاطع MCP + في العرض المباشر. وهو مبني على OpenTelemetry، لذا ينطبق عليه ما يلي أيضًا. + +## تتبّعات تعبر النقل {#traces-that-cross-the-wire} + +يكون التتبّع أكثر فائدة حين يتابع الطلب من العميل إلى الخادم في صورة +مترابطة واحدة. + +عندما يستخدم العميل والخادم SDK، يكون الربط تلقائيًا. يحقن العميل +[سياق التتبّع وفق W3C](https://www.w3.org/TR/trace-context/) في الطلب، ويقرأه الخادم، +فيتداخل مقطع الخادم تحت مقطع العميل ضمن التتبّع نفسه. هذا هو +[SEP-414](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/414)، وتحصل عليه دون +طلب. + +إذا لم تحمل الرسالة الواردة سياق تتبّع، مثل طلب من عميل لا يستخدم +SDK، يرتبط مقطع الخادم بالمقطع الحالي على الخادم إن وُجد، بدلًا من +بدء تتبّع منفصل جديد. + +## تعطيله {#turning-it-off} + +التتبّع مكوّن وسيط، وهو الأول في قائمة خادمك. إذا أردت فعلًا خادمًا +لا يصدر مقاطع، فأزله: + +```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 + يبدأ ذلك الاستيراد بشرطة سفلية عمدًا. الفئة مؤقتة مثل + [`Server.middleware`](../advanced/middleware.md)، لذا ينبغي أن تتوقع + تغيّر مسار الاستيراد. لن تحتاج إلى ذلك غالبًا: دون مكوّن تصدير لا تكلف المقاطع + شيئًا، لذا المعتاد تركها مفعّلة وعدم تثبيت مكوّن تصدير. + +## مراجعة {#recap} + +* يصدر كل `MCPServer` وكل `Server` منخفض المستوى مقطع `SERVER` لكل رسالة واردة + افتراضيًا. لا تكتب شيئًا. +* تحمل المقاطع `mcp.method.name` و`mcp.protocol.version`؛ وتحمل `tools/call` و`prompts/get` أيضًا + خصائص GenAI لتجميع استدعاءات أدواتك مثل أدوات أي وكيل آخر. +* لا يكلف ذلك شيئًا حتى تثبّت OpenTelemetry SDK ومكوّن تصدير، ثم تظهر المقاطع + دون تغيير خادمك. +* ينتشر سياق التتبّع من العميل إلى الخادم تلقائيًا عندما يستخدم الطرفان SDK. + +ما يقرر أصلًا إن كان الطلب سيُنفَّذ هو **[التفويض](authorization.md)**. diff --git a/i18n/ar/pages/servers/completions.md b/i18n/ar/pages/servers/completions.md new file mode 100644 index 0000000000..141fd2a015 --- /dev/null +++ b/i18n/ar/pages/servers/completions.md @@ -0,0 +1,130 @@ +--- +translation: + sections: [72f9c964769076dd, 9a2c14e10935b515, 235299eb78ab12d7, 8aee1e78c8237fb8, 9bd86acd4112138f, 55343cb7f250dc7b] + tool: 1 +--- +# الإكمالات {#completions} + +يستطيع العميل إكمال قيم الوسائط تلقائيًا بينما يكتب المستخدم: أسماء لغات أو مستودعات أو مسارات ملفات. + +**الإكمالات** هي طريقة خادمك لتوفير هذه الاقتراحات. + +## ما يستفيد من الإكمال {#something-worth-completing} + +تنطبق الإكمالات على شيئين فقط: وسائط **قالب توجيه** (prompt) ومَعلمات **قالب مورد**. لذا ابدأ بخادم فيه واحد من كل نوع: + +```python title="server.py" hl_lines="6 12" +--8<-- "docs_src/completions/tutorial001.py" +``` + +لا يتعلق شيء هنا بالإكمالات بعد. + +* تأخذ `review_code` قيمة `language`. لا ينبغي أن يضطر المستخدم إلى تخمين الصيغ التي تقبلها. +* تأخذ `github_repo` قيمتي `owner` و`repo`. لا يشكّل حقلا نص حر لهما نموذجًا جيدًا. + +## دالة معالجة الإكمال {#the-completion-handler} + +أضف دالة **واحدة** بالمزخرف `@mcp.completion()`: + +```python title="server.py" hl_lines="21-29" +--8<-- "docs_src/completions/tutorial002.py" +``` + +* توجد دالة معالجة واحدة لكل خادم. يصل كل طلب إكمال إليها، وتتفرع حسب ما يجري إكماله. +* يجب أن تكون `async def`: تنتظرها SDK. +* تتلقى ثلاث وسائط: + * `ref`: *أي* قالب توجيه أو قالب مورد، بصيغة `PromptReference` أو `ResourceTemplateReference`. تميّز بينهما باستخدام `isinstance`. + * `argument`: تمثل `argument.name` الوسيطة التي يجري إكمالها، و`argument.value` ما كتبه المستخدم حتى الآن. + * `context`: الوسائط التي حُددت قيمها بالفعل. تجاهلها الآن. +* تعيد `Completion(values=[...])`، أو `None` عندما لا تملك اقتراحًا. + +!!! tip + `argument.value` هي البادئة التي كتبها المستخدم. **لا** ترشّح SDK النتائج نيابة عنك: كل ما + تضعه في `values` تعرضه الواجهة. أنت من يكتب `startswith`. + +### جرّبها {#try-it} + +استخدم `Client` داخل الذاكرة من **[الاختبار](../get-started/testing.md)**. استدعِ +`client.complete()` مع `ref=PromptReference(name="review_code")` و +`argument={"name": "language", "value": "py"}`: + +```python +result.completion.values # ['python'] +``` + +* `ref` هو نوع المرجع نفسه الذي تتلقاه دالة المعالجة. +* `argument` قاموس عادي بمفتاحين فقط، `name` و`value`. + +أرسل `value` فارغة وستتلقى القائمة كاملة. تكون `lang.startswith("")` صحيحة لكل لغة: + +```python +result.completion.values # ['go', 'javascript', 'python', 'rust', 'typescript'] +``` + +اسأل عن `code` (وسيطة لا تعرفها دالة المعالجة) وستعيد `None`، التي تحوّلها SDK إلى قائمة فارغة: + +```python +result.completion.values # [] +``` + +تعني `None` *"لا اقتراحات"*، ولا تعني خطأ أبدًا. تعود الواجهة إلى حقل نص عادي. + +## قدرة لم تعلنها بنفسك {#a-capability-you-never-declared} + +تسجيل دالة المعالجة هو الإعلان. صِل عميلًا وانظر: + +```python +client.server_capabilities.completions # CompletionsCapability() +``` + +لم تدرج `completions` في أي مكان. رأت SDK دالة المعالجة وأعلنت القدرة نيابة عنك. تعمل كل قدرة *اختيارية* بهذه الطريقة: دالة المعالجة هي الإعلان. (العناصر الثلاثة ليست اختيارية: تعلنها `MCPServer` دائمًا، سواء وُجدت دوال معالجة أم لا.) + +!!! check + عُد إلى `server.py` الأول (دون دالة معالجة) واطلب الإكمال منه رغم ذلك. يفشل الاستدعاء + بخطأ JSON-RPC: + + ```text + Method not found + ``` + + وتكون `client.server_capabilities.completions` هي `None`. هذه غاية القدرة: يفحصها + العميل الملتزم ولا يرسل طلبًا لا تستطيع الإجابة عنه. + +## الوسائط المعتمدة على غيرها {#dependent-arguments} + +يملك `github://repos/{owner}/{repo}` مَعلمتين، وتعتمد القيم المفيدة لـ`repo` على `owner` الذي اختير أولًا. + +هذا دور `context`. يحمل الوسائط التي **حدد المستخدم قيمها بالفعل**: + +```python title="server.py" hl_lines="8-11 34-38" +--8<-- "docs_src/completions/tutorial003.py" +``` + +* يعمل الفرع الجديد لمَعلمة `repo` في القالب. +* `context.arguments` هي `dict[str, str] | None` للقيم المختارة حتى الآن (هنا `owner`). +* عدم وجود `owner` بعد يعني عدم وجود اقتراحات مناسبة، فتُعيد دالة المعالجة `None`. + +يرسل العميل القيم المحددة باستخدام `context_arguments=`. هذه المرة يكون `ref` هو +`ResourceTemplateReference(uri="github://repos/{owner}/{repo}")`. اطلب `repo` مع +`value` فارغة ومرّر `context_arguments={"owner": "modelcontextprotocol"}`: + +```python +result.completion.values # ['python-sdk', 'typescript-sdk', 'inspector'] +``` + +احذف `context_arguments=` فيعيد الاستدعاء نفسه `[]`. لا تستطيع دالة المعالجة معرفة المستودعات التي تقترحها حتى تعرف المالك. + +!!! info + تقبل `Completion` أيضًا `total=` و`has_more=`. عيّنهما عندما تكون `values` جزءًا من قائمة + أطول، كي تعرض الواجهة *"و200 أخرى"*. لا تحتاج إليهما معظم دوال المعالجة. + +## مراجعة {#recap} + +* الإكمالات اقتراحات لـ**وسائط قوالب التوجيه** و**مَعلمات قوالب الموارد** فقط. +* تسجّل `@mcp.completion()` دالة المعالجة الوحيدة. وهي `async def (ref, argument, context) -> Completion | None`. +* تفرّع حسب `isinstance(ref, ...)` و`argument.name`. ورشّح النتائج حسب `argument.value` بنفسك. +* تصبح `None` قائمة فارغة. ولا تكون خطأ أبدًا. +* تحمل `context.arguments` القيم المحددة مسبقًا؛ ويقدّمها العميل باسم `context_arguments=`. +* تظهر قدرة `completions` بمجرد تسجيل دالة المعالجة. وبدونها يفشل الطلب بـ`Method not found`. + +تساعد الاقتراحات بينما لا يزال المستخدم *يملأ* قالب توجيه أو مورد؛ أما لسؤاله *أثناء* استدعاء أداة، فتحتاج إلى **[استقاء المعلومات](../handlers/elicitation.md)**. وتغطي **[الصور والصوت والأيقونات](media.md)** كل ما تعيده الأداة غير النص. diff --git a/i18n/ar/pages/servers/handling-errors.md b/i18n/ar/pages/servers/handling-errors.md new file mode 100644 index 0000000000..6220c506be --- /dev/null +++ b/i18n/ar/pages/servers/handling-errors.md @@ -0,0 +1,162 @@ +--- +translation: + sections: [7be05607887e6853, e7375894888d9750, c36f73fc7e3af13b, 2fec2d7e129e62fe, 809b0e0a7c27295a, b4395a04d2a5d906, 1a436007f5f54779, c6b2078ed1e63ba5] + tool: 1 +--- +# معالجة الأخطاء {#handling-errors} + +قد تفشل الأداة بثلاث طرق، وتتعامل SDK مع كل منها بصورة مختلفة. + +أثِر `ToolError` فيرى **النموذج** رسالتك. وأثِر `MCPError` فيراها **البروتوكول**. وأثِر أي استثناء آخر فيُعد انهيارًا: لا يعرف النموذج إلا أن الاستدعاء فشل، ويحصل سجلك على تتبّع الاستثناء. + +تساعدك هذه الصفحة على الاختيار. + +## خطأ يستطيع النموذج إصلاحه {#an-error-the-model-can-fix} + +خذ أداة تبحث عن شيء، واجعل البحث لا يعثر عليه: + +```python title="server.py" hl_lines="2 12-13" +--8<-- "docs_src/handling_errors/tutorial001.py" +``` + +`ToolError` من `mcp.server.mcpserver.exceptions` هو وسيلة الأداة لإخبار النموذج بأن شيئًا أخفق. + +استدعِها بعنوان غير موجود في الفهرس وانظر إلى النتيجة: + +```python +result.is_error # True +result.content # [TextContent(text="Error executing tool get_author: No book titled 'Nothing' in the catalog.")] +result.structured_content # None +``` + +* **نجح** الطلب. توجد نتيجة؛ ولم يُثر شيء لدى المستدعي. +* تكون `is_error` هي `True`، وتوجد رسالتك (مسبوقة باسم الأداة) في `content`، حيث يقرأها النموذج. +* تكون `structured_content` هي `None`. لا يملك الاستدعاء الفاشل قيمة إرجاع لتنظيمها. + +هذا **خطأ أداة**، وهو تقريبًا دائمًا ما تريده. + +النموذج هو من يستدعي أداتك. وهو من اختار الوسائط. لذا يشكّل خطأ الأداة دورًا في المحادثة: يقرأ النموذج *"لا يوجد كتاب بعنوان 'Nothing' في الفهرس."*، ويدرك أنه خمّن العنوان خطأ، ثم يستدعيها بعنوان أفضل. كتبت `raise` واحدة وحصلت على وكيل يصحح خطأه. + +على الخادم، يظهر `ToolError` كسطر `INFO` واحد في السجل دون تتبّع استثناء. توقعت الحالة، فلا شيء للتحقيق فيه. + +!!! tip + لا تستخدم `return` لإعادة رسالة خطأ من أداة. تحمل السلسلة المعادة `is_error=False`، لذا + يبدو للنموذج (ولكل واجهة عميل) أن الأداة نجحت وأن السلسلة هي الإجابة. + استخدم `raise`. هذا الحقل هو الإشارة. + +## خطأ لا يستطيع النموذج إصلاحه {#an-error-the-model-cannot-fix} + +استبدل الآن `ToolError` بـ`MCPError`. + +```python title="server.py" hl_lines="1 3 14" +--8<-- "docs_src/handling_errors/tutorial002.py" +``` + +`MCPError` هو **خطأ البروتوكول** في SDK. وهو الاستثناء الوحيد الذي *لا* يلتقطه غلاف الأداة: ينتشر، ويفشل طلب `tools/call` كله بخطأ JSON-RPC بدلًا من نتيجة. + +```json +{ + "code": -32602, + "message": "No book titled 'Nothing' in the catalog." +} +``` + +* **لا توجد نتيجة**. لا `content` ولا `is_error`: لا شيء يقرؤه النموذج. +* يتلقى **التطبيق المضيف** الخطأ بدلًا من ذلك، كما لو أن الأداة غير موجودة أصلًا. +* تصل `code` و`message` و`data` دون تغيير. `INVALID_PARAMS` يساوي `-32602`؛ وتصدّره `mcp.types` مع رموز أخطاء JSON-RPC الأخرى (`INVALID_REQUEST` و`INTERNAL_ERROR`، ...) كثوابت، فلا تكتب رقمًا غامضًا. + +!!! check + البحث نفسه والإخفاق نفسه، لكن الاستدعاء الآن *يثير استثناءً* لدى العميل بدلًا من إعادة نتيجة: + + ```text + mcp.shared.exceptions.MCPError: No book titled 'Nothing' in the catalog. + ``` + + أعطت النسخة الأولى النموذج جملة يستطيع التفاعل معها. وهذه لا تعطيه شيئًا. + بالنسبة إلى `get_author`، فهذا أسوأ قطعًا، وهو ما يوضحه القسم التالي. + +## أي استثناء تثير؟ {#which-one-to-raise} + +يجيب المساران عن سؤالين مختلفين. + +* **أثِر `ToolError`** لإخفاق *التنفيذ*: لم ينجح ما حاولت أداتك فعله. اختار النموذج الاستدعاء، لذلك ينبغي أن يرى النتيجة ويحصل على فرصة للتعافي. عنوان مكتوب خطأ، أو انتهاء مهلة API خارجية، أو صف غير موجود: كلها أخطاء أدوات. +* **أثِر `MCPError`** عندما ينبغي رفض *الطلب نفسه*: يفتقد العميل قدرة تعتمد عليها أداتك، أو ليس الخادم في حالة تسمح بالخدمة، أو تجاوز المستدعي خطوة مطلوبة. لا تصلح إعادة المحاولة من النموذج أيًّا منها، فلا فائدة من إعطائه الرسالة. + +يحسم الأمر سؤال واحد: **هل كان يمكن لنموذج أذكى تجنّب هذا؟** نعم -> `ToolError`. لا -> `MCPError`. + +وفق هذا المعيار، اختارت النسخة الثانية من `get_author` خطأ: يصلح عنوان أفضل المشكلة، لذا كان ينبغي أن يرى النموذج الرسالة. وُجد المثال لعرض الآلية، لا للتوصية بها. + +!!! info + تُستورد `MCPError` باستخدام `from mcp import MCPError`، وتأخذ `code` و`message` وحمولة + `data` اختيارية. يتلقى العميل ما تضعه فيها: تمرّر SDK + `MCPError` المُثار حرفيًا بدلًا من إخفاء تفاصيله. + +## أي استثناء آخر {#any-other-exception} + +احذف الآن التحقق ودع البحث في القاموس يفشل بنفسه: + +```python title="server.py" hl_lines="11" +--8<-- "docs_src/handling_errors/tutorial004.py" +``` + +تثير `CATALOG[title]` الاستثناء `KeyError`. لم تخطط له، لذلك تعتبره SDK انهيارًا: + +```python +result.is_error # True +result.content # [TextContent(text="Error executing tool get_author")] +``` + +ما زال الاستدعاء يعيد `is_error=True`، فيعرف النموذج أنه فشل ويمكنه المتابعة. لكنه لا يتلقى نص الاستثناء: قد يصف `KeyError` من شيفرتك أو تتبّع SQL من مشغّل داخل عدة مكتبات تفاصيل الخادم الداخلية، لذلك لا يغادر الخادم أبدًا. + +أنت من يتلقاه. يسجّل الخادم الانهيار بمستوى `ERROR` مع التتبّع الكامل، بالرسالة `Tool 'get_author' raised an unexpected exception`. لذلك يبقى سجل الإنتاج عند مستوى `WARNING` صامتًا مع كل `ToolError`، ويظهر فيه شيء بمجرد حدوث عطل فعلي. + +## مورد غير موجود {#a-resource-that-doesnt-exist} + +ترسم الموارد الحد نفسه، وتوفر استثناءً مسمّى للحالة الشائعة. + +```python title="server.py" hl_lines="2 13" +--8<-- "docs_src/handling_errors/tutorial003.py" +``` + +`books://{title}` **قالب**. يطابق *أي* عنوان، لذلك يختلف سؤال "هل URI صحيح الصياغة؟" عن "هل الكتاب موجود؟"، ولا تستطيع الإجابة عن الثاني إلا دالتك. + +عندما لا تجده، أثِر `ResourceNotFoundError`. تحوّله SDK إلى خطأ البروتوكول الذي تخصصه المواصفة للمورد المفقود: `-32602` مع URI المطلوب في `data`، فيعرف العميل *أي* قراءة فشلت. + +```json +{ + "code": -32602, + "message": "No book titled 'Nothing' in the catalog.", + "data": {"uri": "books://Nothing"} +} +``` + +لاحظ أنه لا توجد نتيجة جزئية مع `is_error=True` هنا. قراءة المورد إما تعيد المحتويات أو تفشل: لا تملك الموارد إلا مسار البروتوكول. يؤدي `ResourceError` الدور نفسه لإخفاق غير "غير موجود" (`-32603` ورسالتك)، ويظهر كلاهما كسطر `INFO` واحد في سجلك. أي استثناء آخر عدا `MCPError` انهيار: يتلقى العميل `-32603` لا يذكر إلا URI، ويذهب التتبّع إلى سجلك عند `ERROR`. تغطي **[الموارد](resources.md)** القوالب وكل ما يتعلق بالموارد. + +## أخطاء لا تثيرها بنفسك {#errors-you-never-raise} + +لا تصل الوسيطة غير الصالحة إلى دالتك أصلًا. + +أرسل إلى `get_author` قيمة `title` ليست سلسلة نصية، فترفضها SDK مقابل مخطط المدخلات **قبل** استدعاء دالتك، كخطأ أداة من نوع `is_error=True` يستطيع النموذج قراءته وتصحيحه. تعرض **[الأدوات](tools.md)** الرفض نفسه باستخدام القيد `Field(le=50)`. + +هذا يعني فئة كاملة من عبارات `raise` لا تحتاج إلى كتابتها: لا تعِد التحقق من تلميحات الأنواع لديك. + +!!! info + كل ما يراه **العميل** في هذه الصفحة يراه أيضًا `Client` داخل الذاكرة الذي تكتب به اختباراتك. + حتى `raise_exceptions=True` لا يعيد استثناء الأداة الفاشلة + إلى المستدعي: عندما يصبح لهذا الخيار أثر، يكون استثناؤك قد أصبح بالفعل + نتيجة `is_error=True`. تحقّق من النتيجة. إذا احتجت إلى تتبّع انهيار، فهو في + سجل الخادم، وتلتقطه `caplog` في pytest. تشرح **[الاختبار](../get-started/testing.md)** النمط. + +## مراجعة {#recap} + +* أثِر **`ToolError`** داخل أداة -> يعيد الاستدعاء `is_error=True` مع رسالتك في `content`. يقرأها النموذج ويمكنه إعادة المحاولة. +* أثِر **`MCPError`** -> يفشل الاستدعاء نفسه بخطأ JSON-RPC. لا يرى النموذج شيئًا؛ ويتعامل المضيف معه. تبقى `code` و`message` و`data` دون تغيير. +* السؤال الحاسم: *هل كان يمكن لنموذج أذكى تجنّب هذا؟* نعم -> `ToolError`. لا -> `MCPError`. +* أي **استثناء آخر** انهيار -> `is_error=True` مع `Error executing tool ` فقط للنموذج، وسجل `ERROR` مع التتبّع لك. +* `ResourceNotFoundError` من دالة معالجة مورد -> رمز البروتوكول `-32602`، مع URI في `data`. +* تُرفض الوسائط غير الصالحة مقابل المخطط قبل تشغيل دالتك؛ ولا تكتب `raise` لها. +* الاستيرادات: `from mcp import MCPError` و`from mcp.server.mcpserver.exceptions import ToolError, ResourceError, ResourceNotFoundError`، وثوابت رموز الأخطاء من `mcp.types`. + +انتهت معالجة الأخطاء. هذا كل ما *يتيحه* الخادم. أما ما تستطيع كل دالة معالجة قراءته وإرساله إلى العميل أثناء عملها، فهو القسم التالي: **[داخل دالة المعالجة](../handlers/index.md)**. + +النص الدقيق لأخطاء SDK التي يُرجّح أن تواجهها، ومعنى كل منها وحلّه بخطوة واحدة، في **[استكشاف الأخطاء وإصلاحها](../troubleshooting.md)**. diff --git a/i18n/ar/pages/servers/index.md b/i18n/ar/pages/servers/index.md new file mode 100644 index 0000000000..ee01624f70 --- /dev/null +++ b/i18n/ar/pages/servers/index.md @@ -0,0 +1,35 @@ +--- +translation: + sections: [09defc170a0da89d] + tool: 1 +--- +# الخوادم {#servers} + +تتيح `MCPServer` ثلاثة عناصر أساسية لعميل متصل. تختلف بحسب من +يقرر استخدامها: + +* **[الأداة](tools.md)** إجراء يختاره *النموذج* ويستدعيه. هذه + أول صفحة يحتاج إليها معظم القراء، + و**[المخرجات المنظّمة](structured-output.md)** مرجعها المرافق: + كل ما يتعلق بشكل القيمة التي تعيدها الأداة. +* **[المورد](resources.md)** بيانات للقراءة فقط يختار *التطبيق* + قراءتها. وتُعد **[قوالب URI](uri-templates.md)** المرجع + المرافق له: صياغة العناوين الكاملة وقواعد أمان المسارات. +* **[قالب التوجيه](prompts.md)** (prompt) قالب رسائل يستدعيه *شخص* + بالاسم، من قائمة أو أمر يبدأ بشرطة مائلة. + +إلى جانب العناصر الثلاثة، يعلن الخادم عن إمكانات أخرى: + +* **[الإكمالات](completions.md)** إكمال تلقائي من جانب الخادم لوسائط قوالب التوجيه + وقوالب الموارد. +* تغطي **[الصور والصوت والأيقونات](media.md)** كل ما يمكن للأداة + إعادته غير النص، والأيقونات التي يعرضها العميل بجانب خادمك. +* تشرح **[معالجة الأخطاء](handling-errors.md)** الفرق بين + خطأ يستطيع النموذج التعافي منه وآخر يجب ألّا يراه أبدًا. + +كل صفحة هنا مستقلة؛ انتقل مباشرة إلى ما تحتاج إليه. إذا لم +تبنِ خادمًا بعد، فابدأ بـ**[الخطوات الأولى](../get-started/first-steps.md)**. + +ما يحدث *داخل* الدوال التي تسجّلها (`Context` وحقن الاعتماديات +وطلب مزيد من المدخلات من المستخدم أثناء الاستدعاء) هو موضوع القسم التالي، +**[داخل دالة المعالجة](../handlers/index.md)**. diff --git a/i18n/ar/pages/servers/media.md b/i18n/ar/pages/servers/media.md new file mode 100644 index 0000000000..74e8154fcb --- /dev/null +++ b/i18n/ar/pages/servers/media.md @@ -0,0 +1,141 @@ +--- +translation: + sections: [496394d24d221bf1, 4ceb4591180dc6c3, 0fd63e4682d02e0c, 969ede0bd3686a16, 864137b5e9c61e91, 043f526230dd243d, db1ef91db7d6b3f3] + tool: 1 +--- +# الوسائط {#media} + +النص ليس الشيء الوحيد الذي تستطيع الأداة إعادته. + +توفّر SDK كائنين مساعدين للنتائج الثنائية (**`Image`** و**`Audio`**) ونوع **`Icon`** لمنح خادمك وأدواتك ومواردك وقوالب توجيهك مظهرًا في واجهة العميل. + +## إعادة صورة {#returning-an-image} + +أعلن نوع الإرجاع `Image`، وحدد ملفًا لها، ثم أعِدها: + +```python title="server.py" hl_lines="8 12 14" +--8<-- "docs_src/media/tutorial001.py" +``` + +* تقبل `Image` واحدًا فقط من `path` (ملف للقراءة) أو `data` (بايتات خام). +* يُستنتَج نوع MIME الذي يراه العميل من اللاحقة: يُعلَن `logo.png` كـ`image/png`. +* لا شيء هنا خاص بالشعارات. تعمل أي صورة PNG بجانب `server.py`: رسم بياني أنتجته شيفرتك أو مخطط أو صورة فوتوغرافية. + +`Image` وسيلة مساعدة في SDK، وليست نوعًا في البروتوكول. أثناء النقل، تصبح القيمة المعادة كتلة **`ImageContent`** (بايتات الملف بترميز base64 مع نوع MIME): + +```python +result.content # [ImageContent(type="image", data="iVBORw0KGgoAAAANSUhEUg...", mime_type="image/png")] +result.structured_content # None +``` + +لاحظ أمرين: + +* `data` بترميز base64. لم تتعامل مع البايتات؛ قرأت SDK الملف ورمّزته. +* تكون `structured_content` هي `None`. فـ`Image` محتوى ينظر إليه النموذج، لا بيانات يحلّلها التطبيق: لا يوجد مخطط مخرجات. (قارن بـ**[المخرجات المنظّمة](structured-output.md)**، حيث تعليق الإرجاع *هو* المخطط.) + +!!! info + توجد `ImageContent` و`AudioContent` في `mcp.types`، بجانب `TextContent` + التي تنتج عن قيمة `str` عادية (**[الأدوات](tools.md)**). نتيجة الأداة قائمة كتل محتوى؛ و`Image` و`Audio` + أقصر طريقة لإنتاج النوعين الثنائيين. + +### جرّبها {#try-it} + +ضع أي صورة PNG بجانب `server.py` باسم `logo.png`، وشغّل: + +```console +uv run mcp dev server.py +``` + +افتح تبويب **Tools** واستدعِ `logo`. النتيجة ليست سلسلة نصية: إنها كتلة محتوى `image`، ويعرض Inspector صورتك. تتولى SDK كل ما بين الملف على القرص والبكسلات على الشاشة. + +## إعادة صوت {#returning-audio} + +لـ`Audio` الشكل نفسه. اترك `logo.png` في مكانه، وضع بجانبه أي ملف WAV باسم `chime.wav`: + +```python title="server.py" hl_lines="18-21" +--8<-- "docs_src/media/tutorial002.py" +``` + +تكون النتيجة كتلة **`AudioContent`**: + +```python +result.content # [AudioContent(type="audio", data="UklGR...", mime_type="audio/wav")] +result.structured_content # None +``` + +الفكرة نفسها: ملف على القرص يدخل، وbase64 ونوع MIME يخرجان، دون مخطط مخرجات. + +## بايتات أو ملف {#bytes-or-a-file} + +يقبل الكائنان المساعدان أيضًا `data=` (بايتات خام) بدلًا من `path=`. هذا مناسب لبايتات لم تأتِ من ملف مستقل: عمود قاعدة بيانات أو استجابة HTTP أو رسم أنتجته Pillow للتو: + +```python title="server.py" hl_lines="14 15" +--8<-- "docs_src/media/tutorial003.py" +``` + +مع `path=`، لا تحتاج إلى إعلان شيء: يُقرأ الملف عند بناء النتيجة، ويُستنتَج نوع MIME من اللاحقة: + +* `Image`: `.png`، `.jpg`، `.jpeg`، `.gif`، `.webp`. +* `Audio`: `.wav`، `.mp3`، `.ogg`، `.flac`، `.aac`، `.m4a`. + +إذا لم تُعرف اللاحقة، يُستخدم `application/octet-stream`. + +!!! check + مع `data=` لا يوجد اسم ملف للاستنتاج منه. إذا نسيت `format=`، + تعود SDK إلى قيمة افتراضية: `image/png` للصور و`audio/wav` للصوت. إذا بنيت + `Audio` من بايتات MP3 بهذه الطريقة، يُبلَّغ العميل بأن `mime_type="audio/wav"`، + فيفشل في فك الترميز رغم اتباعه التعليمات. عند تمرير `data=`، مرّر `format=`. + +## تضمين مورد {#embedding-a-resource} + +تستطيع الأداة أيضًا إعادة مستند: نص أو بايتات مع URI الذي يوجد عليه ونوع MIME. هذا **`EmbeddedResource`**، نوع آخر من كتل المحتوى. بخلاف `str` العادية، يخبر العميل بماهية المحتوى، فيستطيع عرضه كمرفق أو التعرف على مورد يعرفه مسبقًا. + +```python title="server.py" hl_lines="7 14 16-18" +--8<-- "docs_src/media/tutorial005.py" +``` + +* `brand://guidelines` مورد عادي (تغطي **[الموارد](resources.md)** ذلك). تعطي الأداة المستند نفسه للنموذج عند الطلب، ويحافظ استدعاء `guidelines()` مباشرة على مصدر حقيقة واحد. +* تأتي `EmbeddedResource` و`TextResourceContents` من `mcp.types`. لا يوجد كائن مساعد مثل الصور: تدخل الكتلة التي تبنيها في النتيجة دون تغيير، ولا توجد `structured_content`. +* استخدم URI الذي سُجّل المورد عليه، كي يعرف العميل أن المرفق و`brand://guidelines` المستند نفسه. أي URI صالح، سواء كان مسجلًا أم لا. + +```python +result.content # [EmbeddedResource(type="resource", resource=TextResourceContents(uri="brand://guidelines", mime_type="text/markdown", text="# Brand guidelines\n\n..."))] +``` + +للمحتوى الثنائي، استخدم `BlobResourceContents(uri=..., mime_type=..., blob=...)` مع البايتات بترميز base64 في `blob`، بدلًا من `TextResourceContents`. لإرسال مرجع فقط يستطيع العميل قراءته لاحقًا باستخدام `resources/read`، أعِد `ResourceLink(name=..., uri=...)`؛ فهو كتلة محتوى أيضًا. + +## الأيقونات {#icons} + +`Icon` بيانات وصفية، لا محتوى. لا تحمل الصورة؛ بل تشير إليها باستخدام URI، ويمكن للعميل جلبها وعرضها بجانب اسم خادمك أو أداة أو مورد أو قالب توجيه. + +```python title="server.py" hl_lines="4-5 7 10 16" +--8<-- "docs_src/media/tutorial004.py" +``` + +* `src` عنوان URI يستطيع العميل حله: `https:`، أو URI من نوع `data:` إذا أردت تضمين الأيقونة دون جلب إضافي. +* تتيح `mime_type` و`sizes` (`"48x48"`، أو `"any"` لتنسيق قابل للتحجيم) للعميل اختيار المناسب عند توفير عدة أيقونات. +* يحدد `theme="light"` أو `theme="dark"` أيقونة لنظام ألوان معين. + +تقبل `MCPServer(...)` و`@mcp.tool()` و`@mcp.resource()` و`@mcp.prompt()` جميعًا الوسيطة `icons=[...]` نفسها. + +### أين يراها العميل؟ {#where-a-client-sees-them} + +تنتقل الأيقونات مع العناصر التي تزيّنها. تصل أيقونات الخادم عند اتصال العميل، في `client.server_info` (اختيارية في اتصالات جيل 2026، فتحقق من توفرها أولًا): + +```python +assert client.server_info is not None # python-sdk servers identify themselves by default +client.server_info.icons # [Icon(src="https://example.com/brand-kit.png", mime_type="image/png", sizes=["48x48"])] +``` + +توجد أيقونات الأداة على كائن `Tool` من `tools/list`، وأيقونات المورد على `Resource` من `resources/list`، وأيقونات قالب التوجيه على `Prompt` من `prompts/list`. يُسمّى الحقل دائمًا `icons`. + +## مراجعة {#recap} + +* أعِد `Image` أو `Audio` من أداة، فيتلقى العميل كتلة `ImageContent` / `AudioContent`: بايتاتك بترميز base64 مع نوع MIME. +* ابنِ الكائن من `path=` ودع اللاحقة تحدد نوع MIME، أو من `data=` داخل الذاكرة مع `format=` صريح. +* أعِد `EmbeddedResource` لوضع مستند (نص أو كتلة ثنائية base64، مع URI ونوع MIME) في النتيجة، أو `ResourceLink` لإرسال المرجع فقط. +* لا تحمل نتائج الوسائط `structured_content` أو مخطط مخرجات. +* `Icon` مرجع: URI في `src` مع `mime_type` و`sizes` و`theme` اختيارية. +* تعمل `icons=[...]` على الخادم والأدوات والموارد وقوالب التوجيه، ويجدها العملاء على الكائنات المقابلة. + +هذا كل ما تستطيع الأداة وضعه *داخل* نتيجة. أما ما يحدث عندما *تفشل* الأداة (ومن ينبغي أن يعرف)، فتشرحه **[معالجة الأخطاء](handling-errors.md)**. diff --git a/i18n/ar/pages/servers/prompts.md b/i18n/ar/pages/servers/prompts.md new file mode 100644 index 0000000000..4982623acc --- /dev/null +++ b/i18n/ar/pages/servers/prompts.md @@ -0,0 +1,202 @@ +--- +translation: + sections: [d65c098f37f5b6c3, dd0c2724d6f2877e, 6835bb3570c6714c, d30d3c20168b88b2, f5ef38dad59d6f76, 6e38a699ba57fbdf, 2b984a3bf37a0ddd] + tool: 1 +--- +# قوالب التوجيه {#prompts} + +**قالب التوجيه** (prompt) قالب رسائل يختاره المستخدم. + +الأدوات للنموذج. أما قالب التوجيه فعلى العكس: يختاره المستخدم من قائمة في عميله (أمر يبدأ بشرطة مائلة أو زر)، ويملأ وسائطه، ثم تدخل الرسائل الناتجة في المحادثة كما لو أنه كتبها. + +تعلن عنه بوضع `@mcp.prompt()` على دالة تعيد النص. + +## قالب توجيهك الأول {#your-first-prompt} + +```python title="server.py" hl_lines="6-9" +--8<-- "docs_src/prompts/tutorial001.py" +``` + +تقرأ SDK الأشياء الثلاثة نفسها التي تقرؤها من الأداة: + +* **الاسم** هو اسم الدالة: `review_code`. +* **الوصف** الذي يعرضه العميل هو سلسلة التوثيق: `Review a piece of code.` +* تأتي **الوسائط** من المَعلمات. لا تملك `code` قيمة افتراضية، لذا فهي مطلوبة. + +هذا ما يتلقاه العميل من `prompts/list`: + +```json +{ + "name": "review_code", + "description": "Review a piece of code.", + "arguments": [ + {"name": "code", "required": true} + ] +} +``` + +لا يوجد JSON Schema هنا. وسائط قالب التوجيه قائمة مسطحة من **قيم نصية مسمّاة**: نموذج يملؤه شخص، لا حمولة يبنيها نموذج لغوي. + +### توليد الرسائل منه {#rendering-it} + +يولّد العميل رسائل القالب باستخدام `prompts/get`، مع تمرير الوسائط. تعمل دالتك وتصبح `str` التي تعيدها **رسالة مستخدم واحدة**: + +```json +{ + "description": "Review a piece of code.", + "messages": [ + { + "role": "user", + "content": { + "type": "text", + "text": "Please review this code:\n\ndef add(a, b): return a + b" + } + } + ], + "resultType": "complete" +} +``` + +هذه دورة حياة قالب التوجيه كاملة: يُعرض بالاسم، وتُولَّد رسائله عند الطلب، وتُضاف إلى المحادثة. + +!!! check + يُفرَض `required` قبل تشغيل دالتك. اطلب رسائل `review_code` دون `code` فيفشل + الطلب نفسه بخطأ JSON-RPC (رمزه `-32603`): + + ```text + mcp.shared.exceptions.MCPError: Internal server error + ``` + + لا توجد نتيجة خطأ على نمط الأدوات لتُعاد إلى النموذج، لأن النموذج ليس طرفًا في العملية: + يثير الاستدعاء استثناءً. ويظهر السبب (`Missing required arguments: {'code'}`) في سجل خادمك. + +### جرّبه {#try-it} + +شغّل الخادم باستخدام MCP Inspector: + +```console +uv run mcp dev server.py +``` + +افتح تبويب **Prompts** واختر `review_code`. ينشئ Inspector نموذجًا بحقل `code` مطلوب واحد. املأه وولّد الرسائل، وستحصل بالضبط على رسالة المستخدم أعلاه. + +## أكثر من رسالة واحدة {#more-than-one-message} + +مراجعة الشيفرة رسالة واحدة. أما جلسة تصحيح الأخطاء فمحادثة، ويمكن لقالب توجيه تهيئتها كاملة. + +أعِد قائمة رسائل بدلًا من `str`: + +```python title="server.py" hl_lines="2 13-20" +--8<-- "docs_src/prompts/tutorial002.py" +``` + +* تأتي `UserMessage` و`AssistantMessage` من `mcp.server.mcpserver.prompts.base`. مرّر إليهما `str` فتغلّفانها في `TextContent` نيابة عنك. يحدد اسم الفئة الدور. +* `Message` فئتهما الأساسية المشتركة. استخدمها كتعليق نوع الإرجاع. + +ينتج توليد رسائل `debug_error` الآن ثلاث رسائل بالترتيب: + +```json +{ + "description": "Start a debugging conversation.", + "messages": [ + {"role": "user", "content": {"type": "text", "text": "I'm seeing this error:"}}, + {"role": "user", "content": {"type": "text", "text": "TypeError: 'int' object is not iterable"}}, + { + "role": "assistant", + "content": {"type": "text", "text": "I'll help debug that. What have you tried so far?"} + } + ], + "resultType": "complete" +} +``` + +لاحظ الأخيرة. تعبئة دور `assistant` مسبقًا وسيلة لتوجيه رد النموذج *التالي* دون أن يكتب المستخدم التوجيه بنفسه. + +## العناوين وأوصاف الوسائط {#titles-and-argument-descriptions} + +`review_code` اسم دالة، وليس عنوانًا للعرض. امنح العميل اسمًا أفضل لوضعه على الزر، وصف كل وسيطة كي يكون النموذج واضحًا: + +```python title="server.py" hl_lines="10-13" +--8<-- "docs_src/prompts/tutorial003.py" +``` + +* `title="Code review"` هو الاسم المقروء للبشر، تمامًا مثل `title` للأداة. +* `Annotated[str, Field(description=...)]` هو النمط نفسه الذي تستخدمه **[الأدوات](tools.md)** لوصف مَعلمات الأداة. هنا يظهر الوصف على الوسيطة بدلًا من مخطط. +* تملك `language` قيمة افتراضية، فتتوقف عن كونها مطلوبة. + +يحمل إدخال `prompts/list` الآن كل ما يحتاج إليه العميل لإنشاء نموذج جيد: + +```json +{ + "name": "review_code", + "title": "Code review", + "description": "Review a piece of code.", + "arguments": [ + {"name": "code", "description": "The code to review.", "required": true}, + {"name": "language", "description": "The language the code is written in.", "required": false} + ] +} +``` + +!!! info + إذا قرأت **[الأدوات](tools.md)**، فأنت تعرف كل ما سبق بالفعل. المزخرف نفسه، + وسلسلة التوثيق كوصف، و`Annotated`/`Field` نفسيهما. لا يتغير إلا من + يبدأ الاستدعاء (المستخدم) وأين تذهب النتيجة (إلى المحادثة). + +## أكثر من نص {#more-than-text} + +تقبل `UserMessage` و`AssistantMessage` أيضًا كتلة محتوى، أو كائنًا مساعدًا `Image` / `Audio`، حيثما تقبلان `str`. تظهر حالتان في قوالب التوجيه: إرفاق مستند وإرفاق صورة. + +### تضمين ملف {#embedding-a-file} + +```python title="server.py" hl_lines="5 12 21 23" +--8<-- "docs_src/prompts/tutorial004.py" +``` + +* دليل الأسلوب مورد على `style://python` (تغطي **[الموارد](resources.md)** ذلك)، يُقرأ من `style-guide.md` بجانب `server.py`. ضع أي ملف Markdown هناك. +* تحمل `EmbeddedResource(resource=TextResourceContents(...))`، وكلاهما من `mcp.types`، الملف مع URI ونوع MIME كرسالة أولى؛ ويتبعها الطلب الذي يشير إليه كنص عادي. +* يسمح التضمين، بدلًا من لصق الدليل داخل سلسلة f-string، للعميل بعرضه كمرفق وإعادة فتح `style://python` لاحقًا، ويتلقى النموذج الملف حرفيًا. لملف ثنائي، استخدم `BlobResourceContents` مع `blob` بترميز base64. + +بعد توليد الرسائل، يكون `content` للرسالة الأولى كتلة `resource`: + +```json +{"type": "resource", "resource": {"uri": "style://python", "mimeType": "text/markdown", "text": "* Prefer early returns.\n..."}} +``` + +### إرفاق صورة {#attaching-an-image} + +```python title="server.py" hl_lines="4 15" +--8<-- "docs_src/prompts/tutorial005.py" +``` + +* `Image` هو الكائن المساعد في **[الصور والصوت والأيقونات](media.md)**. تحوّله `UserMessage` إلى كتلة `ImageContent` (الملف بترميز base64، ونوع MIME مستنتج من `.png`) عند توليد رسائل القالب؛ ويتحول `Audio` إلى `AudioContent` بالطريقة نفسها. +* ضع أي صورة PNG باسم `architecture.png` بجانب `server.py`. وسائط قوالب التوجيه نصوص، لذلك تأتي الصورة دائمًا من الخادم؛ ولا يوفّر `component` إلا الكلمات. + +```json +{"type": "image", "data": "iVBORw0KGgoAAAANSUhEUg...", "mimeType": "image/png"} +``` + +## تغيير القائمة أثناء التشغيل {#changing-the-list-at-runtime} + +يمكن إضافة قوالب التوجيه أثناء اتصال العملاء، مثلًا للسماح للمستخدم بحفظ تعليمات كعنصر قائمة خاص به. سجّل القالب ثم أرسل الإشعار: + +```python title="server.py" hl_lines="5 23-27" +--8<-- "docs_src/prompts/tutorial006.py" +``` + +* تسجّل `mcp.add_prompt(Prompt.from_function(fn, name=..., description=...))` دالة تمامًا كما تفعل `@mcp.prompt()`، و`mcp.remove_prompt(name)` العملية العكسية. تحتفظ `add_prompt` بإدخال موجود بالاسم نفسه بدلًا من استبداله، لذلك تزيل الأداة الإدخال القديم أولًا كي يستبدله الحفظ. يعكس `prompts/list` التغيير فورًا. +* ترسل `await ctx.notify_prompts_changed()` إشعار `notifications/prompts/list_changed` إلى كل عميل `2026-07-28` يستمع على تدفّق `subscriptions/listen` (**[الاشتراكات](../handlers/subscriptions.md)**). وترسله `await ctx.session.send_prompt_list_changed()` إلى العميل المستدعي إذا كان يستخدم بروتوكولًا أقدم من 2026 (**[خدمة العملاء القدامى](../run/legacy-clients.md)**). استدعِ كليهما؛ لا تفعل أيٌّ منهما شيئًا إذا لم يوجد من تُبلغه. +* يعيد العميل الذي يتلقى الإشعار استدعاء `prompts/list`. في `Client` الخاص بـPython، تكون الصيغة `async with client.listen(prompts_list_changed=True) as sub:`، التي تنتج حدث `PromptsListChanged`. + +## مراجعة {#recap} + +* وضع `@mcp.prompt()` على دالة يجعلها قالب توجيه. يأتي الاسم من الدالة والوصف من سلسلة التوثيق. +* **يتحكم المستخدم** في قوالب التوجيه: يعرضها العميل، ويختار المستخدم واحدًا ويملأ الوسائط. +* الوسائط قائمة مسطحة من نصوص مسمّاة (دون مخطط). والمَعلمة ذات القيمة الافتراضية اختيارية. +* أعِد `str` فتصبح رسالة مستخدم واحدة. وأعِد قائمة `UserMessage` / `AssistantMessage` لتهيئة محادثة متعددة الأدوار. +* يضع العميل `title=` و`Field(description=...)` في واجهته. +* يؤدي غياب وسيطة مطلوبة إلى إخفاق الطلب كله. لا توجد نتيجة خطأ منفصلة لكل قالب. +* غلّف `EmbeddedResource` أو `Image` داخل `UserMessage` لإرفاق مستند أو صورة. +* أضف قوالب التوجيه أو احذفها أثناء التشغيل باستخدام `mcp.add_prompt(...)` / `mcp.remove_prompt(...)`، ثم `await ctx.notify_prompts_changed()` و`await ctx.session.send_prompt_list_changed()`. + +الإكمال التلقائي من جانب الخادم لوسائط قالب توجيه (أو قالب مورد) موضوع **[الإكمالات](completions.md)**. diff --git a/i18n/ar/pages/servers/resources.md b/i18n/ar/pages/servers/resources.md new file mode 100644 index 0000000000..a51164f965 --- /dev/null +++ b/i18n/ar/pages/servers/resources.md @@ -0,0 +1,146 @@ +--- +translation: + sections: [09df998c2a799f78, 0cf131146d16d4f9, 4e6b91e3f8025346, 8fe4eef576db17ed, 0d0d1ed43e3d0a53] + tool: 1 +--- +# الموارد {#resources} + +**المورد** بيانات تتيحها ليقرأها التطبيق. + +هذا هو الفرق. الأداة شيء يقرر **النموذج** استدعاءه. أما المورد فهو شيء يقرر **التطبيق** تحميله (ملف إعدادات أو سجل أو مستند) ووضعه أمام النموذج كسياق. + +تعلن عنه بوضع `@mcp.resource(uri)` على دالة Python عادية. + +## موردك الأول {#your-first-resource} + +```python title="server.py" hl_lines="6-8" +--8<-- "docs_src/resources/tutorial001.py" +``` + +له شكل الأداة نفسه، مع إضافة واحدة: **URI**. تُطلب الموارد بعناوينها لا بأسمائها. يطلب العميل `config://app`، وليس `get_config` أبدًا. + +ما زالت SDK تقرأ الباقي من الدالة: + +* **الاسم** هو اسم الدالة: `get_config`. +* **الوصف** الذي يراه العميل هو سلسلة التوثيق. +* **المحتوى** هو ما تعيده. + +أثناء `resources/list` يتلقى العميل هذا: + +```json +{ + "name": "get_config", + "uri": "config://app", + "description": "The active shop configuration.", + "mimeType": "text/plain" +} +``` + +وعندما يقرأ `config://app`، تعمل دالتك وتعود القيمة المعادة كنص: + +```python +result.contents # [TextResourceContents(uri="config://app", mime_type="text/plain", text="theme=dark\nlanguage=en")] +``` + +!!! tip + عرض القائمة قليل التكلفة. **لا** تُستدعى دالتك أثناء `resources/list`، بل أثناء + `resources/read` فقط، ولعنوان URI المطلوب فقط. أتح ألف مورد + ولن تدفع تكلفة إلا للموارد التي يفتحها أحدهم. + +### جرّبه {#try-it} + +شغّل الخادم باستخدام MCP Inspector: + +```console +uv run mcp dev server.py +``` + +افتح عنوان URL الذي يطبعه وانتقل إلى تبويب **Resources**. يظهر `config://app` في القائمة مع وصفه. انقر عليه فيقرؤه Inspector: ها هما سطرا إعداداتك. + +## قوالب الموارد {#resource-templates} + +استخدام URI لكل سجل لا يتوسع جيدًا. ضع **عنصرًا نائبًا** في URI ومَعلمة مطابقة في الدالة: + +```python title="server.py" hl_lines="12-13" +--8<-- "docs_src/resources/tutorial002.py" +``` + +`{user_id}` في URI، و`user_id: str` في الدالة. هذا هو العقد بالكامل. + +أصبح هذا **قالب مورد**، ويتغير مكان عرضه: يغادر `resources/list` ويظهر بدلًا منه في `resources/templates/list`، كنمط بدلًا من عنوان: + +```json +{ + "name": "get_user_profile", + "uriTemplate": "users://{user_id}/profile", + "description": "A customer's profile.", + "mimeType": "text/plain" +} +``` + +يملأ العميل العنصر النائب ويقرأ URI محددًا: `users://42/profile` أو `users://ada/profile`. تجيب دالة واحدة عن الجميع، مع تمرير القيمة المطابقة في `user_id`: + +```python +result.contents # [TextResourceContents(uri="users://42/profile", text="User 42: 12 orders since 2021.")] +``` + +لاحظ `uri` في النتيجة. إنه URI **المحدد** الذي طلبه العميل، وليس القالب. + +!!! check + يجب أن تتطابق العناصر النائبة والمَعلمات. غيّر اسم مَعلمة الدالة إلى + `user` بينما لا يزال URI يحتوي على `{user_id}`، وسيرفض المزخرف ذلك **أثناء الاستيراد**، + قبل أن يتصل أي عميل: + + ```text + ValueError: Mismatch between URI parameters {'user_id'} and function parameters {'user'} + ``` + + عدم التطابق لا يمكن أن يكون إلا خطأ، لذلك تمنع SDK تشغيل الخادم في هذه الحالة. + +صياغة العناصر النائبة هي [RFC 6570](https://datatracker.ietf.org/doc/html/rfc6570): تستخدم `{+path}` للقيم متعددة المقاطع، و`{?q,lang}` لمَعلمات الاستعلام الاختيارية، وغير ذلك. تطبّق SDK أيضًا فحوص أمان المسارات على القيم المستخرجة افتراضيًا. راجع **[قوالب URI وأمان المسارات](uri-templates.md)** للمرجع الكامل. + +يمكن أن تأخذ `get_user_profile` أيضًا مَعلمة ذات تعليق نوع `Context`. تحقنها SDK دون اعتبارها مَعلمة URI، وتشرح صفحة **[السياق](../handlers/context.md)** ما توفره لك. + +## ما تعيده {#what-you-return} + +لست مقيدًا بـ`str`. امنح كل مورد `mime_type` وأعِد ما يناسبه: + +```python title="server.py" hl_lines="8-9 14-15 20-21" +--8<-- "docs_src/resources/tutorial003.py" +``` + +* تعيد `readme` قيمة `str`، فتُرسل كما هي. هذه الحالة الشائعة. +* تعيد `catalog_stats` قاموس `dict`، فتسلسله SDK إلى **نص JSON** نيابة عنك: + + ```json + { + "books": 1204, + "authors": 391 + } + ``` + +* تعيد `placeholder_cover` قيمة `bytes`، فيتلقى العميل `BlobResourceContents` بدلًا من `TextResourceContents`، مع ترميز بايتاتك بصيغة base64 في حقل `blob`. + +تنطبق القاعدة نفسها على أي شيء آخر قابل للتسلسل إلى JSON: قائمة أو نموذج Pydantic أو فئة بيانات. إذا لم يكن `str` أو `bytes`، يصبح JSON. + +أنت من يعلن `mime_type`، وقيمته الافتراضية `text/plain`. لا تفحص SDK القيمة المعادة لتخمينه، لذلك يُعلن مورد `dict` الذي لم تحدد نوعه كنص عادي أيضًا. + +!!! tip + يقبل `@mcp.resource()` أيضًا `name=` و`title=` و`description=` عندما لا + تريد اشتقاقها من الدالة. وعندما لا توجد دالة تحتاج إلى كتابتها أصلًا، + توفر `mcp.server.mcpserver.resources` فئات `Resource` جاهزة (`TextResource` و + `BinaryResource` و`FileResource` و`HttpResource` و`DirectoryResource`) تسجّلها + باستخدام `mcp.add_resource(...)`. + +يستطيع العميل أيضًا **الاشتراك** في مورد وتلقي إشعار عند تغيّره؛ وهذا جانب العميل، وتشرحه **[العميل](../client/index.md)**. + +## مراجعة {#recap} + +* وضع `@mcp.resource(uri)` على دالة يجعلها موردًا. URI هو العنوان، والقيمة المعادة هي المحتوى، وسلسلة التوثيق هي الوصف. +* وجود `{placeholder}` في URI يجعله **قالبًا**: يُعرض ضمن `resources/templates/list` وتخدم دالة واحدة كل URI مطابق. +* يجب أن تساوي أسماء العناصر النائبة أسماء مَعلمات الدالة. إذا أخطأت، ستعرف أثناء الاستيراد، لا في الإنتاج. +* تعمل دالتك عند **قراءة** المورد، لا عند عرضه في القائمة. +* تتحول `str` إلى نص، و`bytes` إلى كتلة ثنائية base64، وكل ما عداهما إلى نص JSON. وتحدد النوع باستخدام `mime_type=`. +* الأدوات ليتصرف النموذج. والموارد ليقرأها التطبيق. + +العنصر الأساسي الثالث، الذي يختاره شخص من قائمة، هو **[قوالب التوجيه](prompts.md)**. diff --git a/i18n/ar/pages/servers/structured-output.md b/i18n/ar/pages/servers/structured-output.md new file mode 100644 index 0000000000..64d1a44446 --- /dev/null +++ b/i18n/ar/pages/servers/structured-output.md @@ -0,0 +1,257 @@ +--- +translation: + sections: [a838d57f003aed44, 857d03886a0137ed, 42d9efcb9f542867, 2290ff08435b5573, 91be9b73602abcf1, 6cdbad079f7b47f0, d4b607372fb28b51, 7608fc5ebc31d6ea, c7eff2a5698225fa, c851964bb3301907, 8f296f1f09e4c400, d715db6f8dccc9cc, a0c344a48450dbe4] + tool: 1 +--- +# المخرجات المنظّمة {#structured-output} + +تنتج الأداة التي تعيد `str` عادية النتيجة مرتين: كنص في `content`، وكـ`{"result": "..."}` في `structured_content`. + +تتناول هذه الصفحة القناة الثانية: مصدرها، وجميع الأشكال التي يمكن أن تتخذها، وكيف تضمن SDK صحتها. + +الفكرة المختصرة: **تعليق نوع الإرجاع هو مخطط المخرجات**. وقد كتبته بالفعل. + +## مخطط المخرجات {#the-output-schema} + +```python title="server.py" hl_lines="9" +--8<-- "docs_src/structured_output/tutorial001.py" +``` + +السطر المهم هو التوقيع: `-> int`. + +بسببه، تحمل الأداة التي ترسلها SDK أثناء `tools/list` حقل `output_schema` بجانب مخطط المدخلات الذي تبنيه من مَعلماتك (تغطي **[الأدوات](tools.md)** ذلك المخطط): + +```json +{ + "properties": { + "result": {"title": "Result", "type": "integer"} + }, + "required": ["result"], + "title": "get_temperatureOutput", + "type": "object" +} +``` + +لا يشكّل `int` منفرد كائن JSON، لذا **تغلّفه** SDK في `{"result": ...}`. استدعِ الأداة فتُملأ القناتان: + +```python +result.content # [TextContent(text="17")] +result.structured_content # {"result": 17} +``` + +تحصل كل قيمة مفردة على الغلاف نفسه: `str` و`int` و`float` و`bool` و`bytes` و`None`. + +## قناتان {#two-channels} + +لماذا تُرسل القيمة نفسها مرتين؟ + +* `content` مخصص لـ**النموذج**. يقرأ النموذج اللغوي النص؛ وهذا الجزء الوحيد من النتيجة الذي يراه. +* `structured_content` مخصص لـ**التطبيق** الذي يعمل النموذج داخله: شيفرة تريد `17`، لا جملة تحتوي على "17". +* `output_schema` هو العقد بينهما، ويُنشَر قبل أي استدعاء للأداة. + +تعيد قيمة Python واحدة. وتملأ SDK الثلاثة جميعًا. + +## أعِد نموذجًا {#return-a-model} + +أعلن الشكل باستخدام `BaseModel` من Pydantic وأعِد نسخة منه: + +```python title="server.py" hl_lines="8-11 15" +--8<-- "docs_src/structured_output/tutorial002.py" +``` + +أصبحت `WeatherData` **هي** المخطط. لا غلاف ولا مفتاح `result`: + +```json +{ + "properties": { + "temperature": {"description": "Degrees Celsius.", "title": "Temperature", "type": "number"}, + "humidity": {"description": "Relative humidity, 0 to 1.", "title": "Humidity", "type": "number"}, + "conditions": {"title": "Conditions", "type": "string"} + }, + "required": ["temperature", "humidity", "conditions"], + "title": "WeatherData", + "type": "object" +} +``` + +`structured_content` هو الكائن بكل حقوله: + +```python +result.structured_content # {"temperature": 16.2, "humidity": 0.83, "conditions": "Overcast"} +``` + +ولا يُترك النموذج اللغوي دون محتوى. تسلسِل SDK الكائن نفسه إلى نص JSON في `content`: + +```json +{ + "temperature": 16.2, + "humidity": 0.83, + "conditions": "Overcast" +} +``` + +لاحظ أن `Field(description=...)` على `temperature` و`humidity` ظهرت في المخطط. تصف `Field` نفسها التي وصفت **مدخلاتك** مخرجاتك أيضًا. + +!!! info + إذا استخدمت `response_model` في FastAPI، فأنت تعرف ذلك: نموذج Pydantic للاستجابة + المعلنة، يجري تسلسله وتوثيقه نيابة عنك. الفرق الوحيد هنا أن تعليق نوع الإرجاع + هو الإعلان بأكمله. + +## استخدام `TypedDict` {#a-typeddict} + +لا يحتاج كل شكل إلى فئة. ينتج `TypedDict` المخطط نفسه: + +```python title="server.py" hl_lines="8" +--8<-- "docs_src/structured_output/tutorial003.py" +``` + +يكون `TypedDict` قاموس `dict` عاديًا أثناء التشغيل، لذلك فهذا ما تبنيه وتعيده. يتبع المخطط والتحقق و`structured_content` القواعد نفسها في إصدار `BaseModel`: أضف سلسلة توثيق للفئة أو `Annotated[..., Field(description=...)]` فتصبح الأوصاف، وإذا تركت مفتاح `NotRequired` خارج القاموس، يبقى خارج `structured_content`. + +## فئة بيانات {#a-dataclass} + +تعمل فئات البيانات أيضًا، وكذلك أي فئة عادية تملك خصائصها تلميحات أنواع. تبني SDK داخليًا نموذج Pydantic من التعليقات. + +```python title="server.py" hl_lines="8-9" +--8<-- "docs_src/structured_output/tutorial004.py" +``` + +ثلاث صيغ ومخطط واحد. استخدم ما هو موجود أصلًا في قاعدة شيفرتك. + +## القوائم {#lists} + +لا تكون `list[...]` كائن JSON أيضًا، لذا تحصل على غلاف `{"result": ...}`، مع نوع عناصرها كمرجع `$defs` داخله: + +```python title="server.py" hl_lines="15" +--8<-- "docs_src/structured_output/tutorial005.py" +``` + +```json +{ + "$defs": { + "WeatherData": { + "properties": { + "temperature": {"title": "Temperature", "type": "number"}, + "humidity": {"title": "Humidity", "type": "number"}, + "conditions": {"title": "Conditions", "type": "string"} + }, + "required": ["temperature", "humidity", "conditions"], + "title": "WeatherData", + "type": "object" + } + }, + "properties": { + "result": {"items": {"$ref": "#/$defs/WeatherData"}, "title": "Result", "type": "array"} + }, + "required": ["result"], + "title": "get_forecastOutput", + "type": "object" +} +``` + +اطلب توقعات يومين، فيكون `structured_content` هو `{"result": [{...}, {...}]}`. ويتحول `content` إلى كتلتَي `TextContent`، **اثنتين**، واحدة لكل عنصر: تُفك القائمة إلى عناصر للنموذج بدلًا من تحويلها إلى سلسلة واحدة. + +تُغلَّف `tuple[...]` واتحادات الأنواع و`Optional[...]` بالطريقة نفسها. + +## القواميس {#dictionaries} + +`dict[str, ...]` هو النوع العام الوحيد الذي *يشكّل* كائن JSON أصلًا، لذلك لا يُغلَّف: + +```python title="server.py" hl_lines="9" +--8<-- "docs_src/structured_output/tutorial006.py" +``` + +```json +{ + "additionalProperties": {"type": "number"}, + "title": "get_temperaturesDictOutput", + "type": "object" +} +``` + +```python +result.structured_content # {"London": 16.2, "Reykjavik": 4.4} +``` + +يجب أن تكون المفاتيح `str`. لا يمكن أن يكون `dict[int, float]` كائن JSON، لذلك يعود إلى غلاف `{"result": ...}`. + +تستخدم نتائج القواميس `TypeAdapter` من Pydantic للتحقق والتسلسل. إذا فحصت `FuncMetadata.output_model` لأداة، فستجده يحمل تعليق نوع القاموس مع عنوان مخططه. + +## التحقق {#validation} + +ليس `output_schema` مجرد توثيق. **يُتحقَّق من توافق** كل ما تعيده دالتك معه قبل خروجه من الخادم. + +لن تلاحظ ذلك عندما تبني القيمة يدويًا: فقد تأكدت Pydantic بالفعل أن `WeatherData` لديك هي `WeatherData`. ستلاحظه حين تأتي البيانات من مصدر لا تتحكم فيه: + +```python title="server.py" hl_lines="9 21" +--8<-- "docs_src/structured_output/tutorial007.py" +``` + +يَعِد التعليق بإعادة `WeatherData`. لكن الاستجابة من الخدمة الأخرى توقفت عن إرسال `humidity`. + +!!! check + استدعِ `get_weather`، ولن يمرّر كائنًا نصف فارغ إلى العميل بصمت. يفشل الاستدعاء: + يتلقى العميل `is_error=True` مع `Error executing tool get_weather`، فيعرف النموذج أن + الاستدعاء فشل بدلًا من قراءة معلومات طقس غير موجودة بثقة. أما اسم الحقل فهو لك، + في سجل الخادم بمستوى `ERROR`: + + ```text + Tool 'get_weather' raised an unexpected exception + ... + pydantic_core._pydantic_core.ValidationError: 1 validation error for WeatherData + humidity + Field required [type=missing, input_value={'temperature': 16.2, 'conditions': 'Overcast'}, input_type=dict] + ``` + +وبالمناسبة، لا بأس بإعادة `dict` عادية من أداة تعلن `-> WeatherData`. وهذا بالضبط ما أنتجته `json.loads`. يجري التحقق من القيمة، لا من نوع Python. + +## تعطيل المخرجات المنظّمة {#opting-out} + +يكون تعليق الإرجاع أحيانًا مخصصًا لفاحص الأنواع لا للبروتوكول. مرّر `structured_output=False` فتصبح الأداة نصية فقط: + +```python title="server.py" hl_lines="6" +--8<-- "docs_src/structured_output/tutorial008.py" +``` + +لا `output_schema` ولا تغليف ولا تحقق. تكون `structured_content` هي `None`، و`content` السلسلة التي أعدتها. + +أما العكس، `structured_output=True`، فيحوّل الاكتشاف التلقائي إلى شرط: تثير الأداة التي لا يستطيع نوع إرجاعها إنتاج مخطط استثناءً أثناء الاستيراد بدلًا من الاكتفاء بالنص. + +## كتل المحتوى والوسائط {#content-blocks-and-media} + +تُستثنى كتل المحتوى والوسائط (`TextContent` و`EmbeddedResource` و`Image` و`Audio` وما شابه، منفردة أو كعناصر `list` أو `tuple` أو `Sequence`، أو كبدائل ضمن اتحاد أنواع) تلقائيًا: فهي مخصصة لقراءة النموذج، لذا لا يشتق الاكتشاف التلقائي مخططًا منها (تغطي **[الصور والصوت والأيقونات](media.md)** كلًّا من `Image` و`Audio`). ما زال `structured_output=True` يفرض مخططًا لفئات كتل المحتوى. + +## فئة دون تلميحات أنواع {#a-class-without-type-hints} + +هناك طريقة واحدة للحصول على مخرجات غير منظّمة دون طلب ذلك: إعادة فئة **لا توجد تعليقات أنواع في جسمها**. + +```python title="server.py" hl_lines="6-9" +--8<-- "docs_src/structured_output/tutorial009.py" +``` + +تعيّن `Station` كلًّا من `name` و`online` داخل `__init__`، لكن *الفئة* لا تعلن شيئًا. تقرأ SDK تعليقات الفئة فلا تجد أيًّا منها، وتتوقف عن المحاولة. + +!!! warning + يحدث ذلك **بصمت**. تكون `output_schema` هي `None`، و`structured_content` هي `None`، والنص + الذي يقرؤه النموذج هو `repr` للكائن: + + ```text + "" + ``` + + لا خطأ ولا تحذير، وأداة غير مفيدة. انقل التعليقات إلى جسم الفئة، أو مرّر + `structured_output=True` لتحويل هذا إلى خطأ صريح لحظة استيراد الوحدة: + `Function get_station: return type is not serializable for structured output`. + +!!! tip + هل تحتاج إلى تحكم كامل (بناء `CallToolResult` بنفسك، أو إرفاق `_meta` يستطيع + التطبيق رؤيتها ولا يستطيع النموذج)؟ راجع **[الخادم منخفض المستوى](../advanced/low-level-server.md)**. + +## مراجعة {#recap} + +* **تعليق نوع الإرجاع** هو مخطط المخرجات. يُنشر في `tools/list` باسم `output_schema`. +* تُغلَّف القيم المفردة والقوائم والصفوف واتحادات الأنواع في `{"result": ...}`. أما النماذج و`TypedDict` وفئات البيانات والفئات ذات تعليقات الأنواع و`dict[str, ...]` فهي كائنات أصلًا وتبقى كما هي. +* تحمل كل نتيجة `content` (نصًا للنموذج) **و**`structured_content` (بيانات للتطبيق). +* يُتحقَّق من القيمة المعادة مقابل المخطط. عدم التطابق خطأ أداة، لا نتيجة تالفة. +* يستثني `structured_output=False` الأداة. تُستثنى كتل المحتوى و`Image` و`Audio` افتراضيًا؛ وتُستثنى الفئة دون تلميحات أنواع بصمت، لذا انتبه إليها. + +أصبحت تتحكم الآن في كل ما يمكن أن تعيده الأداة. التالي هو العنصر الأساسي الثاني: **[الموارد](resources.md)**. diff --git a/i18n/ar/pages/servers/tools.md b/i18n/ar/pages/servers/tools.md new file mode 100644 index 0000000000..8a99b4a67b --- /dev/null +++ b/i18n/ar/pages/servers/tools.md @@ -0,0 +1,179 @@ +--- +translation: + sections: [e4cc390d56573409, f30cf8103a6e918c, 2c97b9f888398951, 048e5471dfa71aea, 3076b1e16ad95950, c1115cd005b81e8f, 3d8ef8da89fa87c1, f6c0e02e6ea5a363] + tool: 1 +--- +# الأدوات {#tools} + +**الأداة** دالة يستطيع النموذج استدعاءها. + +تعلن عنها بوضع `@mcp.tool()` على دالة Python عادية. هذه هي API كاملة. + +## أداتك الأولى {#your-first-tool} + +```python title="server.py" hl_lines="6-8" +--8<-- "docs_src/tools/tutorial001.py" +``` + +انظر إلى ما كتبته. لا مخططات ولا JSON ولا معالجة للبروتوكول، بل دالة فقط. تقرأ SDK منها ثلاثة أشياء: + +* **اسم** الأداة هو اسم الدالة: `search_books`. +* **الوصف** الذي يراه النموذج هو سلسلة التوثيق: `Search the catalog by title or author.` +* **الوسائط** التي يُسمح للنموذج بتمريرها تأتي من تلميحات الأنواع: `query: str` و`limit: int`. + +### مخطط المدخلات {#the-input-schema} + +تولّد SDK من تلميحات الأنواع هذه JSON Schema وترسله إلى العميل أثناء `tools/list`: + +```json +{ + "type": "object", + "properties": { + "query": {"title": "Query", "type": "string"}, + "limit": {"title": "Limit", "type": "integer"} + }, + "required": ["query", "limit"], + "title": "search_booksArguments" +} +``` + +كلتا الوسيطتين ضمن `required` لأن أيًّا منهما لا يملك قيمة افتراضية. ستصلح ذلك بعد قليل. (مفاتيح `title` ناتجة عن Pydantic؛ أما الخصائص وأنواعها و`required` فهي العقد.) + +لا يوجد مفتاح `$schema` أيضًا: يتعامل MCP مع مخطط لا يتضمنه على أنه **JSON Schema 2020-12**، وهو ما تولّده Pydantic، لذلك لا يلزم اختيار شيء حتى تكتب المخططات يدويًا على **[الخادم منخفض المستوى](../advanced/low-level-server.md#the-dialect-is-json-schema-2020-12)**. + +!!! tip + ليست تلميحات الأنواع هنا مجرد توثيق. إنها **العقد**. إذا أرسل العميل `"limit": "ten"`، + ترفضه SDK قبل أن تعمل دالتك أصلًا. + +### ما الذي يتلقاه النموذج؟ {#what-the-model-gets-back} + +استدعِ الأداة مع `{"query": "dune", "limit": 5}`، وستتكون النتيجة من جزأين: + +```python +result.content # [TextContent(text="Found 3 books matching 'dune' (showing up to 5).")] +result.structured_content # {'result': "Found 3 books matching 'dune' (showing up to 5)."} +``` + +`content` هو النص الذي يقرؤه **النموذج**. و`structured_content` بيانات ذات أنواع محددة لـ**تطبيق العميل**. وهو موجود لأنك أعلنت نوع الإرجاع `-> str`. + +لا تقلق بشأن `structured_content` بعد. أعِد كائنات Python فعلية من أدواتك وسيُنفَّذ المطلوب؛ وتشرح صفحة **[المخرجات المنظّمة](structured-output.md)** ذلك بالكامل. + +### جرّبها {#try-it} + +شغّل الخادم باستخدام MCP Inspector: + +```console +uv run mcp dev server.py +``` + +افتح عنوان URL الذي يطبعه، وانتقل إلى تبويب **Tools**، واستدعِ `search_books`. + +يعرض Inspector نموذجًا بحقل نصي مطلوب `query` وحقل عددي مطلوب `limit`. أنشأ ذلك النموذج من تلميحات الأنواع لديك. وكذلك سيفعل كل عميل MCP آخر. + +## الوسائط الاختيارية {#optional-arguments} + +امنح المَعلمة قيمة افتراضية فتتوقف عن كونها مطلوبة. هذا كل شيء. إنها Python فحسب. + +```python title="server.py" hl_lines="7" +--8<-- "docs_src/tools/tutorial002.py" +``` + +يتبعها المخطط: + +```json +{ + "type": "object", + "properties": { + "query": {"title": "Query", "type": "string"}, + "limit": {"default": 10, "title": "Limit", "type": "integer"} + }, + "required": ["query"], + "title": "search_booksArguments" +} +``` + +خرجت `limit` من `required` وحصلت على `"default": 10`. يحصل العميل الذي يحذفها على `10`، تمامًا كما يحدث في Python. + +## مخططات أغنى باستخدام `Field` {#richer-schemas-with-field} + +توصلك تلميحات الأنواع بعيدًا، لكنك قد تريد أحيانًا *وصف* وسيطة أو تقييدها. + +غلّف النوع باستخدام `Annotated` وأضف `Field` من Pydantic: + +```python title="server.py" hl_lines="12-14" +--8<-- "docs_src/tools/tutorial003.py" +``` + +ثلاث إضافات، جميعها على المَعلمات: + +* `Field(description=...)`: وصف لكل وسيطة يقرؤه النموذج إلى جانب سلسلة التوثيق. +* `Field(ge=1, le=50)`: حدود عددية. تظهر في المخطط بصيغة `"minimum": 1, "maximum": 50`. +* `Literal["fiction", "non-fiction", "poetry"]`: تعداد قيم. لا يستطيع النموذج اختيار إلا واحدة منها. + +!!! check + القيود ليست للزينة. استدعِ الأداة مع `limit=999`، وستجيب SDK + بخطأ أداة **قبل تشغيل دالتك**: + + ```text + Input should be less than or equal to 50 + ``` + + يعود ذلك الخطأ إلى النموذج كنتيجة للأداة، فيقرؤه النموذج ويعيد المحاولة + بقيمة صالحة. كتبت `le=50` مرة واحدة وحصلت دون جهد إضافي على وكلاء يصححون أخطاءهم. + +!!! info + إذا استخدمت FastAPI أو Pydantic، فأنت تعرف كل ذلك بالفعل. إنها `Field` نفسها، + و`Annotated` نفسها، والتحقق نفسه. لا شيء خاص بـMCP لتتعلمه هنا. + +## نموذج بوصفه مَعلمة {#a-model-as-a-parameter} + +عندما تأخذ الأداة أكثر من وسيطتين تقريبًا، اجمعها في نموذج Pydantic: + +```python title="server.py" hl_lines="8-11 15" +--8<-- "docs_src/tools/tutorial004.py" +``` + +يتداخل مخطط `Book` داخل مخطط مدخلات الأداة (كمرجع `$defs`)، ويملؤه النموذج اللغوي ككائن JSON، وتتلقى دالتك **نسخة `Book` فعلية** جرى التحقق منها مسبقًا، مع الخصائص `.title` و`.author` و`.year`. + +يمكنك الجمع كما تريد: مَعلمات عادية بجانب مَعلمات نماذج، ونماذج متداخلة، وقوائم نماذج. تتولى Pydantic جميع المستويات. + +## `async def` {#async-def} + +إذا نفّذت الأداة عمليات إدخال وإخراج (استدعاء API أو قراءة ملف أو استعلام قاعدة بيانات)، فأعلنها باستخدام `async def` واستخدم `await` داخلها. ستنتظرها SDK. + +تعمل أداة `def` العادية أيضًا: تشغّلها SDK في خيط تنفيذ كي لا تحجب الخادم. ويمكن للأداة طويلة التنفيذ التحقق مما إذا كان العميل ما زال ينتظر؛ راجع **[الإلغاء](../handlers/cancellation.md)**. + +لا يوجد شيء آخر لإعداده. + +## الأسماء والعناوين والتعليقات {#names-titles-and-annotations} + +يمكنك استبدال كل ما تستنتجه SDK في المزخرف: + +```python title="server.py" hl_lines="7-10" +--8<-- "docs_src/tools/tutorial005.py" +``` + +* `title` اسم مقروء للبشر في الواجهات. تعرض العملاء *"البحث في الفهرس"* بدلًا من `search_books`. +* `annotations` **تلميحات** سلوكية للعميل: + * `read_only_hint=True`: هذه الأداة لا تغيّر شيئًا. + * `open_world_hint=False`: تعمل على مجموعة مغلقة (هذا الفهرس)، لا على الويب المفتوح. + * يصف الخياران الآخران، `destructive_hint` و`idempotent_hint`، أداة *تكتب*: هل يمكن أن + تحذف شيئًا، وهل استدعاؤها مرتين مماثل لاستدعائها مرة واحدة؟ تعرّف المواصفة كليهما + للأدوات التي ليست للقراءة فقط، لذلك لا يعبّران عن شيء في `search_books`. + +يستخدمها العميل الملتزم ليقرر مثلًا: *"هل أحتاج إلى سؤال المستخدم قبل تشغيل هذا؟"*. إنها تلميحات وليست ضمانات أمنية. لا تعتمد أبدًا على التزام العميل بها. + +!!! tip + يقبل `@mcp.tool()` أيضًا `name=` و`description=` إذا لم ترد اشتقاقهما + من اسم الدالة وسلسلة توثيقها. وغالبًا ما تريد ذلك الاشتقاق. + +## مراجعة {#recap} + +* وضع `@mcp.tool()` على دالة يجعلها أداة. يأتي الاسم من الدالة والوصف من سلسلة التوثيق. +* تلميحات الأنواع **هي** مخطط المدخلات. وتجعل القيم الافتراضية الوسائط اختيارية. +* تضيف `Annotated[..., Field(...)]` أوصافًا وقيودًا؛ وتضيف `Literal` تعدادات القيم. +* مَعلمة نموذج Pydantic هي طريقة تلقّي "جسم" منظّم. +* تُرفض الوسائط غير الصالحة نيابة عنك، بخطأ يستطيع النموذج قراءته والتعافي منه. +* استخدم `async def` للإدخال والإخراج، و`def` العادية لكل ما عدا ذلك. + +تشرح **[المخرجات المنظّمة](structured-output.md)** ما يحدث للقيمة التي تعيدها باستخدام `return`. diff --git a/i18n/ar/pages/servers/uri-templates.md b/i18n/ar/pages/servers/uri-templates.md new file mode 100644 index 0000000000..08113113a5 --- /dev/null +++ b/i18n/ar/pages/servers/uri-templates.md @@ -0,0 +1,274 @@ +--- +translation: + sections: [4a7033e1ed8ad602, 55dcbfff0c6271bf, 317f4256a650cab6, 4b6c4a845438abc7, f98b46bafbee4acd] + tool: 1 +--- +# قوالب URI وأمان المسارات {#uri-templates-and-path-safety} + +هذا مرجع لصياغة قوالب URI التي +يقبلها [`@mcp.resource`](resources.md)، وكذلك +لسياسة أمان المسارات التي تطبقها SDK على القيم المستخرجة. للحصول على +مقدمة عن الموارد ومتى تستخدمها، ابدأ بـ +**[الموارد](resources.md)**؛ تفترض هذه الصفحة أنك تعرف كيفية إعلان +مورد وتريد مجموعة المعاملات الكاملة، أو إعدادات الأمان، أو +الربط منخفض المستوى. + +صياغة القوالب هي [RFC 6570](https://datatracker.ietf.org/doc/html/rfc6570). +تدعم SDK مجموعة فرعية اختيرت لمطابقة عناوين URI الواردة في `resources/read`، +إضافة إلى طبقة أمنية ترفض القيم التي تشير بعد حل مساراتها إلى مواقع +خارج المجلد الذي تنوي إتاحته. لتفاصيل البروتوكول +(تنسيقات الرسائل ودورة الحياة وتقسيم النتائج إلى صفحات)، راجع +[مواصفة موارد MCP](https://modelcontextprotocol.io/specification/latest/server/resources). + +## مجموعة المعاملات الكاملة {#the-full-operator-set} + +العنصر النائب العادي `{user_id}` هو ما تقدّمه **[الموارد](resources.md)**. توجد أربع +صيغ معاملات أخرى؛ إليك جميعها في خادم واحد لتراها +معًا: + +```python title="server.py" hl_lines="16-17 22-23 28-29 34-35 40-41" +--8<-- "docs_src/uri_templates/tutorial001.py" +``` + +كل مزخرف مميز يجزّئ URI بطريقة مختلفة. +تشرحها الأقسام أدناه من الأعلى إلى الأسفل. + +### التوسيع البسيط: `{name}` {#simple-expansion-name} + +`books://{isbn}` هو الشكل العادي الشائع. يرتبط العنصر النائب +بالمَعلمة `isbn`، لذلك يستدعي العميل الذي يقرأ `books://978-0441172719` +الدالة `get_book("978-0441172719")`. + +يتوقف `{name}` العادي عند أول `/`. لا يطابق `books://978/extra` +لأن الشرطة المائلة بعد `978` تنهي التقاط القيمة ويبقى `/extra` +دون مطابقة. + +### تحويل الأنواع {#type-conversion} + +تصل القيم المستخرجة كسلاسل نصية، لكن يمكنك إعلان نوع +أكثر تحديدًا لتحوّله SDK. يصل `orders://{order_id}` إلى دالة +مَعلمتها `order_id: int`، لذلك تؤدي قراءة `orders://12345` إلى استدعاء +`get_order(12345)`، وليس `get_order("12345")`. وتجري دالة المعالجة +حسابات عليه (`order_id + 1`) دون تحويل صريح. + +### المسارات متعددة المقاطع: `{+name}` {#multi-segment-paths-name} + +لالتقاط قيمة تحتوي على شرطات مائلة، استخدم `{+name}`. مع +`manuals://{+path}`: + +* يعطي `manuals://returns.md` القيمة `path = "returns.md"` +* يعطي `manuals://printing/setup.md` القيمة `path = "printing/setup.md"` + +استخدم `{+name}` متى كانت القيمة هرمية: مسارات نظام +الملفات أو مفاتيح كائنات متداخلة أو مسارات URL تمرّرها عبر وكيل. + +### مَعلمات الاستعلام: `{?a,b,c}` {#query-parameters-abc} + +يضع `reviews://{isbn}{?limit,sort}` كلًّا من `limit` و`sort` بعد `?`. +يحدد المسار *أي* كتاب، ويضبط الاستعلام *كيفية* قراءته. + +تُطابَق مَعلمات الاستعلام بمرونة: لا يهم ترتيبها، وتُتجاهل المَعلمات الإضافية، +وتستخدم المَعلمات المحذوفة القيم الافتراضية لدالتك. لذلك +يستخدم `reviews://978-0441172719` القيم `limit=10, sort="newest"`، +ولا يستبدل `reviews://978-0441172719?sort=top` إلا `sort`. + +### مقاطع المسار كقائمة: `{/name*}` {#path-segments-as-a-list-name} + +إذا أردت كل مقطع مسار عنصرًا مستقلًا في قائمة، بدلًا من +سلسلة واحدة تحتوي على شرطات مائلة، فاستخدم `{/name*}`. مع `shelves://browse{/path*}`، +يستدعي العميل الذي يقرأ `shelves://browse/fiction/sci-fi` +الدالة `browse_shelf(["fiction", "sci-fi"])`. + +### مرجع القوالب {#template-reference} + +الأنماط الأكثر شيوعًا: + +| النمط | مثال مدخل | ما تحصل عليه | +|--------------|-----------------------|-------------------------| +| `{name}` | `alice` | `"alice"` | +| `{name}` | `docs/intro.md` | *لا تطابق* (يتوقف عند `/`) | +| `{+path}` | `docs/intro.md` | `"docs/intro.md"` | +| `{.ext}` | `.json` | `"json"` | +| `{/segment}` | `/v2` | `"v2"` | +| `{?key}` | `?key=value` | `"value"` | +| `{?a,b}` | `?a=1&b=2` | `"1"`، `"2"` | +| `{/path*}` | `/a/b/c` | `["a", "b", "c"]` | + +### ما يرفضه المحلّل {#what-the-parser-rejects} + +تُكتشَف بعض أشكال القوالب مسبقًا بدلًا من الإخفاق عند +الطلب الأول. يحلّل `@mcp.resource` القالب عند تنفيذ المزخرف، +لذلك لا يصل أي منها إلى خادم يعمل. + +تثير `UriTemplate.parse()` الاستثناء `InvalidUriTemplate` في الحالات التالية: + +* **متغيران لا يفصل بينهما شيء.** يُرفض `manuals://{+path}{ext}` + لأن المطابقة لا تستطيع معرفة أين تنتهي `path` وتبدأ `ext`. + ضع نصًا حرفيًا بينهما (`manuals://{+path}/{ext}`)، أو استخدم + معاملًا يوفر فاصلًا خاصًا به. يُقبل `manuals://{+path}{.ext}` + لأن `{.ext}` يضيف `.` بنفسه. +* **أكثر من متغير واحد متعدد المقاطع.** يُسمح بواحد كحد أقصى من `{+var}` أو + `{#var}` أو متغير موسّع (`{/var*}` أو `{.var*}` أو `{;var*}`) + لكل قالب. وجود اثنين ملتبس بطبيعته: لا توجد طريقة واضحة + لتحديد أيهما يستوعب مقطعًا إضافيًا. +* **أخطاء الصياغة المعتادة**: قوس غير مغلق، أو اسم متغير يُستخدم + مرتين، أو ميزة RFC 6570 لا تدعمها SDK، مثل + معدّل البادئة `{var:3}` أو توسيع الاستعلام `{?vars*}`. + +إضافة إلى ذلك، يثير `@mcp.resource` الاستثناء `ValueError` عندما ترتبط مَعلمة +دالة معالجة بمتغير استعلام في سلسلة +`{?...}`/`{&...}` النهائية للقالب دون قيمة Python افتراضية. تُطابَق هذه المتغيرات +بمرونة (يمكن للعميل حذف أي منها)، لذا فإن مَعلمة +دون قيمة افتراضية لن تظهر إلا كخطأ داخلي غير واضح عند +أول طلب يحذفها. يشكّل `reviews://{isbn}{?limit,sort}` في +الخادم أعلاه النسخة الصحيحة: تملك كل من `limit` و`sort` +قيمة افتراضية. + +## الأمان {#security} + +تأتي مَعلمات القالب من العميل. إذا استُخدمت في عمليات نظام الملفات +أو قاعدة البيانات دون تحقق، فقد تؤدي قيم مثل `../../etc/passwd` إلى +مسارات خارج المجلد الذي تنوي إتاحته. + +### ما تتحقق منه SDK افتراضيًا {#what-the-sdk-checks-by-default} + +قبل تشغيل دالة المعالجة، ترفض SDK أي مَعلمة: + +* تخرج من مجلد البداية عبر مكونات `..` +* تبدو مسارًا مطلقًا (`/etc/passwd` أو `C:\Windows`) أو + مسارًا نسبيًا إلى محرك Windows (`C:foo`). لا يمكن التمييز كنص بين قيمة نسبية إلى محرك + ومعرّف ذي نطاق أسماء مثل `x:y`، + لذلك تُرفض افتراضيًا أي قيمة تبدأ بحرف واحد تتبعه نقطتان؛ + استثنِ المَعلمة إذا كانت تتلقى هذه القيم على نحو مشروع +* تحتوي على بايت صفري (`\x00`) + +يعتمد فحص `..` على المكونات، لا على البحث عن سلسلة فرعية. تمر قيم مثل +`v1.0..v2.0` أو `HEAD~3..HEAD` لأن `..` ليس مقطع مسار مستقلًا +فيها. + +تنطبق هذه الفحوص على القيمة بعد فك الترميز، لذا تكتشف تجاوز المسارات +بغض النظر عن ترميزه في URI (تُكتشَف `../etc` و`..%2Fetc` و +`%2E%2E/etc` و`..%5Cetc` و`%00` جميعًا). + +!!! check + اقرأ `manuals://../etc/passwd` من الخادم أعلاه وسيُرفض الطلب + مباشرة: تتوقف مطابقة القوالب عند أول إخفاق، + لذلك لا يُجرَّب قالب لاحق (قد يكون أقل تقييدًا) + كبديل. يرى العميل خطأ `-32602` نفسه، "مورد غير معروف"، + الذي يراه لعنوان URI لا يطابق أي قالب، + ولا تعمل `read_manual` أبدًا. + +### دوال معالجة نظام الملفات: استخدم safe_join {#filesystem-handlers-use-safe_join} + +تمنع الفحوص المدمجة الحالات الشائعة، لكنها لا تعرف حدود بيئتك +المعزولة. للوصول إلى نظام الملفات، استخدم `safe_join` لحل المسار +والتحقق من بقائه داخل مجلدك الأساسي: + +```python title="server.py" hl_lines="5 15" +--8<-- "docs_src/uri_templates/tutorial002.py" +``` + +تكتشف `safe_join` الخروج عبر الروابط الرمزية وتسلسلات `..` وحيل المسارات +المطلقة التي قد يفوتها فحص نصي بسيط. إذا خرج المسار المحلول +من `DOCS_ROOT`، تثير `PathEscapeError` الذي يظهر +للعميل كـ`ResourceError`. + +### عندما تعيقك الإعدادات الافتراضية {#when-the-defaults-get-in-the-way} + +قد تمنع الفحوص أحيانًا قيمًا مشروعة. فقد تتلقى أداة استيراد فهرس +مسارًا مطلقًا عمدًا، أو تكون المَعلمة مرجعًا +نسبيًا مثل `../sibling` تفسّره دالة المعالجة +بأمان دون الوصول إلى نظام الملفات. استثنِ هذه المَعلمة، أو خفّف +السياسة للخادم كله: + +```python title="server.py" hl_lines="9 16-19" +--8<-- "docs_src/uri_templates/tutorial003.py" +``` + +* يتجاوز `security=ResourceSecurity(exempt_params={"source"})` على المزخرف + الفحوص لتلك المَعلمة وحدها في ذلك المورد وحده. ويحتفظ + باقي الخادم بالسياسة الافتراضية. +* يحدد `resource_security=` في مُنشئ `MCPServer` الإعداد الافتراضي + لجميع الموارد. وهنا يعطّل `relaxed` فحص `..` بالكامل. + +الفحوص القابلة للضبط: + +| الإعداد | الافتراضي | ما يفعله | +|-------------------------|---------|-------------------------------------| +| `reject_path_traversal` | `True` | يرفض تسلسلات `..` التي تخرج من مجلد البداية | +| `reject_absolute_paths` | `True` | يرفض `/foo` و`C:\foo` ومسارات UNC و`C:foo` النسبي إلى محرك (ويكتشف `x:y` أيضًا) | +| `reject_null_bytes` | `True` | يرفض القيم التي تحتوي على `\x00` | +| `exempt_params` | فارغ | أسماء المَعلمات التي تُتجاوز فحوصها | + +هذه الفحوص مرشّح أولي استدلالي؛ وعند الوصول إلى نظام الملفات، +تبقى `safe_join` الحد الذي يضمن الاحتواء. + +!!! tip + إذا لم تستطع دالة المعالجة تلبية الطلب (الملف غير موجود أو المعرّف غير معروف)، فأثِر + `ResourceNotFoundError` كما تفعل `read_manual` أعلاه. يتلقى العميل `-32602` مع رسالتك + وURI. ويتحول الاستثناء غير المتوقع بدلًا من ذلك إلى `-32603` عام. راجع + **[معالجة الأخطاء](handling-errors.md#a-resource-that-doesnt-exist)**. + +## الموارد على الخادم منخفض المستوى {#resources-on-the-low-level-server} + +إذا كنت تبني على `Server` منخفض المستوى (راجع **[الخادم منخفض +المستوى](../advanced/low-level-server.md)**)، فتسجّل دوال معالجة طريقتي البروتوكول `resources/list` و +`resources/read` مباشرة. لا يوجد مزخرف؛ بل +تعيد أنواع البروتوكول بنفسك. + +### الموارد الثابتة {#static-resources} + +لعناوين URI الثابتة، احتفظ بسجل ووجّه بناءً على المطابقة الدقيقة: + +```python title="server.py" hl_lines="17 21 27" +--8<-- "docs_src/uri_templates/tutorial004.py" +``` + +تخبر دالة معالجة القائمة العملاء بما يتوفر؛ وتتيح دالة معالجة القراءة +المحتوى. افحص سجلك أولًا، ثم انتقل إلى +القوالب (أدناه) إن كانت لديك، ثم أثِر استثناءً لأي حالة أخرى. + +### القوالب {#templates} + +يوجد محرك القوالب الذي تستخدمه `MCPServer` في `mcp.shared.uri_template` +ويعمل مستقلًا. تحصل على التحليل والمطابقة نفسيهما؛ وتربط +التوجيه وسياسة الأمان بنفسك. + +```python title="server.py" hl_lines="13-16 22-25 29 33 45" +--8<-- "docs_src/uri_templates/tutorial005.py" +``` + +تحدث ثلاثة أشياء في الأسطر المميزة: + +* **حلّل مرة وطابق لكل طلب.** تبني `UriTemplate.parse()` + القالب؛ وتعيد `template.match(uri)` المتغيرات المستخرجة في + `dict`، أو `None` إذا لم يطابق URI. يحدث فك ترميز URL داخل + `match()`؛ وتُعاد القيم المفكوكة كما هي دون التحقق من أمان + المسارات. تخرج القيم كسلاسل نصية: حوّلها بنفسك + (`int(matched["id"])` و`Path(matched["path"])`). +* **طبّق فحوص الأمان بنفسك.** توجد فحوص `..` والمسارات المطلقة + التي تشغّلها `MCPServer` افتراضيًا في `mcp.shared.path_security`. + تستدعيها `read_manual_safely` قبل الوصول إلى `MANUALS`. إذا لم تكن + المَعلمة مسار نظام ملفات (بل ISBN أو استعلام بحث)، فتجاوز + الفحوص لتلك القيمة: أنت تتحكم في السياسة لكل دالة معالجة بدلًا من + كائن إعدادات. +* **اعرض القوالب من المصدر نفسه.** تكتشف العملاء + القوالب عبر `resources/templates/list`. تعيد `str(template)` + سلسلة القالب الأصلية، لذلك تشترك القائمة والمطابِق + في مصدر حقيقة واحد. + +## مراجعة {#recap} + +* يطابق `{name}` مقطعًا واحدًا؛ ويحتفظ `{+name}` بالشرطات المائلة؛ ويستخرج `{?a,b}` + القيم من سلسلة الاستعلام؛ ويقسّم `{/name*}` المقاطع إلى قائمة. +* يُرفض متغيران لا يفصل بينهما شيء، أو متغير متعدد المقاطع + ثانٍ، أثناء التحليل. يجب أن تعلن المَعلمة المرتبطة بمتغير استعلام نهائي + من نوع `{?...}`/`{&...}` قيمة Python افتراضية. +* أضف تعليق نوع للمَعلمة (`order_id: int`) فتتولى SDK التحويل. +* ترفض سياسة الأمان الافتراضية `..` والمسارات المطلقة والبايتات + الصفرية قبل تشغيل دالة المعالجة؛ استبدلها لكل مورد باستخدام + `security=ResourceSecurity(...)` أو للخادم كله باستخدام + `resource_security=`. +* للوصول إلى نظام الملفات، تشكّل `safe_join` حد الاحتواء. +* على `Server` منخفض المستوى، حلّل باستخدام `UriTemplate.parse()` وطابق + باستخدام `.match()`، وطبّق `mcp.shared.path_security` بنفسك. diff --git a/i18n/ar/pages/translations.md b/i18n/ar/pages/translations.md new file mode 100644 index 0000000000..1bdd00d63f --- /dev/null +++ b/i18n/ar/pages/translations.md @@ -0,0 +1,30 @@ +--- +translation: + sections: [f671b445b16e4f99, f4976d2b682e2b23, b5c8bd4f2b3903e5, c6e2debf1da06eb7, 81d412ed5f399f94] + tool: 1 +--- +# الترجمات {#translations} + +كُتب هذا التوثيق بالإنجليزية. ولإفادة عدد أكبر من القراء، ننشر أيضًا نسخًا مترجمة آليًا، وتشرح هذه الصفحة ما يعنيه ذلك لك وكيف تساعد في تحسينها. + +## ما المتاح {#whats-available} + +التوثيق المترجم حاليًا **نسخة معاينة** بثلاث عشرة لغة: العربية وDeutsch وespañol وfrançais وहिन्दी و日本語 و한국어 وportuguês (Brasil) وрусский язык وTürkçe وукраїнська мова و简体中文 و繁體中文. اختر لغة من مبدّل اللغات أعلى أي صفحة. قد تتبعها لغات أخرى بعد التحقق من جودة هذه النسخ. + +لا يُترجم مرجع API: يربط الموقع المترجم بالمرجع الإنجليزي الوحيد. + +## الإنجليزية هي المرجع المعتمد {#english-is-the-source-of-truth} + +إذا اختلفت صفحة مترجمة عن أصلها الإنجليزي، فالصفحة الإنجليزية هي الصحيحة. تبدأ كل صفحة في الموقع المترجم بإحدى ملاحظات ثلاث توضّح حالتها: + +- **ترجمة آلية** — تُرجمت الصفحة تلقائيًا، وتربط بأصلها الإنجليزي. +- **الترجمة متأخرة عن الصفحة الإنجليزية** — تغير الأصل الإنجليزي بعد ترجمة الصفحة. ما زلت تقرأ تلك الترجمة، ولذلك قد تكون أجزاء منها قديمة حتى تُحدّث؛ وتربط الملاحظة بالصفحة الإنجليزية الحالية. +- **معروضة بالإنجليزية** — لم تُترجم الصفحة بعد، ولذلك تقرأ النص الإنجليزي. + +## كيفية إعداد الترجمات {#how-the-translations-are-made} + +تُولّد الصفحات المترجمة آليًا بأداة في هذا المستودع من الصفحات الإنجليزية تحت `docs/`، وتسترشد بمدخلين يكتبهما البشر لكل لغة: دليل أسلوب (مستوى اللغة والنبرة والطباعة والتعامل مع الدعابات والتعابير الاصطلاحية)، ومسرد (المصطلحات التي تبقى بالإنجليزية، والترجمات المطلوبة والممنوعة لغيرها). لا يُعدّل النص المولّد يدويًا مطلقًا. تدخل كل التحسينات في هذين المدخلين بدلًا من ذلك، حتى تبقى عند إعادة توليد الصفحات. + +## الإبلاغ عن مشكلة في الترجمة {#reporting-a-translation-problem} + +هل وجدت مصطلحًا خاطئًا أو جملة غير طبيعية أو ترجمة تقول ما لا يقوله الأصل الإنجليزي؟ [افتح بلاغًا](https://github.com/modelcontextprotocol/python-sdk/issues) مع تحديد اللغة والصفحة والمقطع؛ وتفيد بلاغات المتحدثين الأصليين خصوصًا. إذا عرفت التصحيح، فاقترحه مباشرة في طلب سحب على دليل أسلوب اللغة (`instructions.md`) أو مسردها (`glossary.json`) تحت [`i18n/`](https://github.com/modelcontextprotocol/python-sdk/tree/main/i18n) — يصل التصحيح عندئذ إلى كل صفحة متأثرة في التوليد التالي. أما مشكلات النص الإنجليزي نفسه، فتُصلح في الصفحات تحت `docs/`، مثل أي تغيير آخر في التوثيق. diff --git a/i18n/ar/pages/troubleshooting.md b/i18n/ar/pages/troubleshooting.md new file mode 100644 index 0000000000..cd05c180d5 --- /dev/null +++ b/i18n/ar/pages/troubleshooting.md @@ -0,0 +1,443 @@ +--- +translation: + sections: [3d58228e81b99543, 170514ce901c4139, 17d61fad0a50d62b, 8a6e351ec756904d, 137454d469c867f5, fcf984fa0615ed11, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 525cdf1755e29d4c, 30fd31be74169d9a, d2e88333d4f7841f, c2dc3b1007d2e987, d6eabf60cc366341, f798e815252852c2, 0cba47bae78d04eb, cdc6d86a4dae8a34] + tool: 1 +--- +# استكشاف الأخطاء وإصلاحها {#troubleshooting} + +كل عنوان في هذه الصفحة هو النص المطابق تمامًا لخطأ تنتجه SDK، يتبعه معناه والحل المباشر. ابحث هنا عن السطر الأخير من تتبّع الاستثناء (أو سجل الخادم) باستخدام البحث داخل الصفحة في المتصفح، واقرأ ذلك المدخل فقط. + +تستخدم عدة مداخل هذا الخادم الواحد. أداة واحدة ومورد قالب واحد، يثير كل منهما استثناءً لمدينة لا يعرفها: + +```python title="server.py" +--8<-- "docs_src/troubleshooting/tutorial001.py" +``` + +تتصل به هذه المداخل على `http://localhost:8000/mcp`، فأبقِه يعمل عبر HTTP: + +```console +uv run mcp run server.py --transport streamable-http +``` + +الأخطاء المقتبسة في هذه الصفحة حقيقية: تعيد مجموعة اختبارات SDK نفسها إنتاج كل واحد منها. + +## `ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)` {#exceptiongroup-unhandled-errors-in-a-taskgroup-1-sub-exception} + +ليس هذا خطأ MCP. إنه تغليف من anyio، والخطأ الفعلي هو **السطر الأخير** في النص المنسوخ. + +تبدأ `Client.__aenter__` مجموعة مهام. تغلّف anyio كل ما يخرج من مجموعة مهام في `ExceptionGroup`، ولذلك يصل *كل* استثناء يفلت من كتلة `async with Client(...)`، أيًا كان، داخل مجموعة: + +```python +async def main() -> None: + async with Client("http://localhost:8000/mcp") as client: + await client.read_resource("weather://Atlantis") +``` + +```text + + Exception Group Traceback (most recent call last): + | ... + | ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception) + +-+---------------- 1 ---------------- + | Exception Group Traceback (most recent call last): + | ... + | ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception) + +-+---------------- 1 ---------------- + | Traceback (most recent call last): + | ... + | mcp.shared.exceptions.MCPError: No forecast for 'Atlantis'. + +------------------------------------ +``` + +يمكنك فعل أمرين: + +1. **اقرأ النهاية.** `MCPError: No forecast for 'Atlantis'.` هو الإخفاق؛ ابحث عن *نصه* في هذه الصفحة. +2. **التقط داخل الكتلة.** لا تظهر `ExceptionGroup` إلا عندما *يغادر* الاستثناء `async with`. إذا التقطته داخلها، يكون الإخفاق نفسه `MCPError` عاديًا دون مجموعة: + +```python +async def main() -> None: + async with Client("http://localhost:8000/mcp") as client: + try: + await client.read_resource("weather://Atlantis") + except MCPError as e: + print(e) # No forecast for 'Atlantis'. +``` + +!!! tip + يفلت الإخفاق أثناء *الاتصال* (عنوان URL خاطئ، أو خادم لا يعمل، أو `421` المذكور + أدناه) من `async with` نفسها، فلا توجد كتلة داخلية لالتقاطه فيها. + في هذه الحالات، اقرأ نهاية المجموعة. + +## `RuntimeError: Client must be used within an async context manager` {#runtimeerror-client-must-be-used-within-an-async-context-manager} + +تبني `Client(...)` الكائن فقط. لا يحدث اتصال حتى `async with`، ولذلك ترفض كل طريقة العمل: + +```python +async def main() -> None: + client = Client("http://localhost:8000/mcp") + tools = await client.list_tools() # RuntimeError +``` + +ادخل السياق. تمثل `__aenter__` الاتصال: + +```python +async def main() -> None: + async with Client("http://localhost:8000/mcp") as client: + tools = await client.list_tools() +``` + +وتمثل `__aexit__` قطع الاتصال، ولذلك لا توجد `client.close()` قد تنساها. تُبنى **[الاختبار](get-started/testing.md)** على هذا النمط نفسه. + +## `Error executing tool : ` و`Error executing tool ` و`Unknown tool: ` {#error-executing-tool-name-message-error-executing-tool-name-and-unknown-tool-name} + +تقرأ **نتيجة**، وليست استثناءً. لم ترفع `call_tool` استثناءً، ولن تفعل ذلك لمجرد إخفاق أداة. + +استدعِ `forecast` لمدينة لا يعرفها الخادم، وستعود `ToolError` التي ترفعها مع تعليم الطلب بأنه *نجح*: + +```python +result.is_error # True +result.content # [TextContent(text="Error executing tool forecast: No forecast for 'Atlantis'.")] +result.structured_content # None +``` + +تمثل `Unknown tool: get_forecast` الشكل نفسه لاسم لم يسجّله الخادم، وتُرفض الوسيطة غير الصالحة بالطريقة نفسها مقابل مخطط إدخال الأداة، قبل أن تعمل دالتك. + +الحل في عميلك: **افحص `result.is_error`**. لا تلتقط `try/except` حول `call_tool` أيًا من هذه الحالات، فلا يوجد استثناء لالتقاطه. هذا مقصود، وهو أهم فكرة في هذه الصفحة: *النموذج* اختار الاستدعاء، ولذلك يحصل على الرسالة وفرصة إعادة المحاولة. تشرح **[معالجة الأخطاء](servers/handling-errors.md)** التفاصيل كاملةً، بما فيها مسار `MCPError` الذي يرفع استثناءً بالفعل. + +تعني الصيغة المجردة، `Error executing tool ` دون رسالة، أن الأداة **تعطلت**: أفلت منها استثناء لم تتوقعه (أو لم تطابق قيمة إرجاعها مخطط المخرجات)، ويُحجب نص الاستثناء عن الشبكة. يوجد التتبّع في **سجل الخادم** بمستوى `ERROR`، تحت `Tool '' raised an unexpected exception`. + +## `TypeError: The @tool decorator was used incorrectly. Did you forget to call it? Use @tool() instead of @tool` {#typeerror-the-tool-decorator-was-used-incorrectly-did-you-forget-to-call-it-use-tool-instead-of-tool} + +كتبت `@mcp.tool` بدلًا من `@mcp.tool()`. تمثل `tool()` *مصنعًا* للمزخرفات: دون الأقواس، تمرّر Python دالتك إلى مَعلمة `name=` الخاصة بها. + +```python +@mcp.tool # <- missing () +def forecast(city: str) -> str: + """Today's forecast for one city.""" + return f"{city}: Rain." +``` + +```text +TypeError: The @tool decorator was used incorrectly. Did you forget to call it? Use @tool() instead of @tool +``` + +أضف الأقواس. تقول `@mcp.resource(...)` و`@mcp.prompt()` الشيء نفسه عند الخطأ نفسه. + +!!! note + يُرفع هذا عند **استيراد** الوحدة، قبل اتصال أي عميل. إذا عرض التطبيق المضيف + خادمك بأنه *فشل في البدء* (أو *غير متصل*)، بدلًا من متصل بلا + أدوات، فقد تكون هذه الحالة: شغّل `python server.py` بنفسك واقرأ التتبّع. يكتشف فاحص الأنواع + ذلك أيضًا: الدالة ليست قيمة `name=` صالحة. + +## `InvalidSignature: Tool '' has an invalid x-mcp-header annotation: ` {#invalidsignature-tool-name-has-an-invalid-x-mcp-header-annotation-reason} + +عُلّمت وسيطة أداة بـ`x-mcp-header` بطريقة لا تسمح بها المواصفة، وتحدد `` القاعدة المخالفة. تستبعد العملاء على `2026-07-28` أداة كهذه من قائمتها، ولذلك ترفض SDK تسجيلها. + +لا يمكن تعليم سوى وسيطات `str` و`int` و`bool`، وليست `str | None` أيًا منها. تعرض **[مَعلمات الترويسات](advanced/header-parameters.md)** الصيغة المناسبة لوسيطة اختيارية. + +مثل المدخل أعلاه، يُرفع هذا عند **استيراد** الوحدة، قبل اتصال أي عميل. + +## `Tool already exists: ` {#tool-already-exists-name} + +استخدم تسجيلان اسم الأداة نفسه. يتقدم **الأول**، ويُسقط الثاني بصمت، ولا تظهر سوى هذه الرسالة في *سجل الخادم*: + +```python title="server.py" hl_lines="6 12" +--8<-- "docs_src/troubleshooting/tutorial002.py" +``` + +```text +WARNING mcp.server.mcpserver.tools.tool_manager: Tool already exists: forecast +``` + +تعرض `tools/list` أداة `forecast` واحدة، وهي `forecast_today`. أعِد تسمية إحداهما. تسكت `MCPServer(..., warn_on_duplicate_tools=False)` التحذير دون تغيير النتيجة، فأبقِه مفعّلًا. تتبع الموارد وقوالب التوجيه القاعدة نفسها ورسالة السجل نفسها (`Resource already exists:` و`Prompt already exists:`). + +## يعرض التطبيق المضيف صفر أدوات {#my-host-lists-zero-tools} + +لا يوجد نص خطأ لهذا، وهو ما يصعّب البحث عنه. لا تسقط SDK أداة مسجّلة من `tools/list` مطلقًا، فافحص من الداخل إلى الخارج: + +* **هل بدأ الخادم أصلًا؟** تثير `@mcp.tool` دون أقواس استثناءً عند الاستيراد، ويبدو الخادم المتعطل مثل خادم فارغ في بعض التطبيقات المضيفة. شغّل `python server.py` بنفسك. +* **هل الأداة موجودة في `mcp` الذي يشغّله التطبيق المضيف؟** تمثل `MCPServer(...)` ثانية في وحدة أخرى خادمًا مختلفًا وفارغًا. تحقّق من الكائن الذي يستورده أمر التطبيق المضيف فعلًا. +* **هل اشتركت أداتان في اسم؟** عندئذ اختفت إحداهما. ابحث عن `Tool already exists:` في سجل الخادم. +* **هل قائمة التطبيق المضيف قديمة؟** لا تصل إضافة أداة بعد بدء التشغيل إلا إلى العملاء التي تعالج `notifications/tools/list_changed`. إعادة تشغيل التطبيق المضيف حل مباشر. +* **هل كتب شيء إلى `stdout` خارج فترة التحويل؟** أثناء التشغيل، تحوّل SDK المخرجات العارضة *المفرغة* من stdout إلى stderr بقدر الإمكان (تُترك البيئة التي تستبدل التدفقات القياسية كما هي)، لكن المخرجات المفرغة إلى stdout قبل ذلك (مثل سكربت تغليف يطبع، أو `print()` عند الاستيراد في عملية بلا تخزين مؤقت)، أو `print()` مخزّنة تُفرغ عند خروج المفسّر، تصل إلى تدفّق البروتوكول. وقد يدفع سطر غير صالح واحد التطبيق المضيف إلى قطع الاتصال، وهو ما تعرضه بعض التطبيقات كخادم فارغ. استخدم وحدة `logging` للتسجيل بدلًا من ذلك. توجد بقية قائمة فحوص جانب التطبيق المضيف في **[الاتصال بتطبيق مضيف فعلي](get-started/real-host.md)**. + +اسم الأداة «غير الصالح» *ليس* ضمن القائمة: يسجّل الاسم غير المطابق تحذيرًا، لكن تُسجّل الأداة وتُدرج رغم ذلك. + +## `MCPError: Server returned an error response` {#mcperror-server-returned-an-error-response} + +رفض الخادم طلب HTTP بالكامل، مع جسم ليس JSON-RPC، فلا يملك `Client` في Python رسالة أفضل من هذه البديلة. + +السبب الأكثر شيوعًا هو خادم Streamable HTTP منشور حديثًا. تستخدم `streamable_http_app()` (و`mcp.run("streamable-http")`) دون `transport_security=` **الحماية من إعادة ربط DNS** افتراضيًا: لا تقبل إلا الطلبات التي تكون ترويسة `Host` فيها localhost. هذا افتراضي صحيح على حاسوبك، لكنه غير مناسب خلف اسم مضيف فعلي: + +```python title="server.py" hl_lines="12" +--8<-- "docs_src/troubleshooting/tutorial003.py" +``` + +انشر ذلك، ووجّه عميلًا إليه، وسيفشل الاتصال أثناء المصافحة: + +```python +async with Client("https://mcp.example.com/mcp") as client: + ... +``` + +```text +mcp.shared.exceptions.MCPError: Server returned an error response +``` + +لا تصلك القيم التي أرسلها الخادم فعلًا، `421` و`Invalid Host header`: لا يتضمن جسم 421 ترويسة `Content-Type: application/json`، فلا يستطيع العميل تحليله. توجد في **سجل الخادم**، وهو المكان التالي الذي تبحث فيه: + +```text +WARNING mcp.server.transport_security: Invalid Host header: mcp.example.com +``` + +الحل هو `transport_security=`. أضف اسم المضيف الذي تخدمه فعلًا إلى قائمة السماح: + +```python title="server.py" hl_lines="14-17" +--8<-- "docs_src/troubleshooting/tutorial004.py" +``` + +!!! check + هذا هو التغيير كاملًا. يتصل العميل نفسه الآن، ويتفاوض على `2026-07-28`، + ويستدعي `forecast`. + +تشرح **[النشر والتوسّع](run/deploy.md)** معنى كل حقل، وحالة الوكيل العكسي، وكل ما يتغير عند النشر. وتمثل `421 Misdirected Request` / `Invalid Host header` أدناه الإخفاق نفسه من الجانب الآخر. + +## `421 Misdirected Request` / `Invalid Host header` {#421-misdirected-request-invalid-host-header} + +هذا هو `Server returned an error response` كما يراه أي شيء *غير* `Client` في Python: curl أو تبويب الشبكة في المتصفح أو سجل وصول وكيل عكسي أو SDK أخرى. + +```bash +curl -i https://mcp.example.com/mcp \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json, text/event-stream' \ + -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}' +``` + +```text +HTTP/1.1 421 Misdirected Request + +Invalid Host header +``` + +تمثل `421 Misdirected Request` عبارة السبب لحالة HTTP؛ وتمثل `Invalid Host header` جسم رد SDK؛ ويعرض `Client` في Python الحدث نفسه كـ`Server returned an error response`. الثلاثة رفض واحد. يجري الفحص على **ترويسة `Host` التي يحملها الطلب**، وليس العنوان الذي ارتبط به الخادم، ولذلك يفعّله الوكيل العكسي الذي يمرّر اسم المضيف العام مثل العميل المباشر تمامًا. + +الحل هو `transport_security=TransportSecuritySettings(allowed_hosts=[...], allowed_origins=[...])` نفسه المعروض تحت `Server returned an error response`. يستحق جانبان منه التوضيح: + +* إدخال `allowed_hosts` نص مطابق تمامًا. تطابق `"mcp.example.com"` ترويسة `Host` بلا منفذ، وتطابق `"mcp.example.com:*"` أي منفذ صريح. أدرج الاثنين. +* رد `403` بجسم `Invalid Origin header` هو الفحص النظير لترويسة `Origin`. يعمل للمتصفحات فقط (لا يرسل غيرها `Origin`)، وتكون `allowed_origins=` قائمة السماح له. + +تتضمن **[النشر والتوسّع](run/deploy.md)** الشرح الكامل، بما فيه الحالات التي يكون فيها تعطيل الفحص الإعداد المناسب. + +## `RuntimeError: Task group is not initialized. Make sure to use run().` {#runtimeerror-task-group-is-not-initialized-make-sure-to-use-run} + +رُكّب تطبيق MCP داخل تطبيق ASGI آخر، ولم يبدأ شيء **مدير جلساته**. + +تعيد `mcp.streamable_http_app()` تطبيق Starlette تبدأ دورة حياته مدير الجلسات، وتشغّل `uvicorn server:app` تلك الدورة نيابة عنك. لكن Starlette **لا تشغّل دورة حياة تطبيق فرعي مركّب مطلقًا**، ولذلك عندما يدخل التطبيق في `Mount`، لا يبدأ المدير ويفشل الطلب الأول: + +```python title="server.py" hl_lines="16" +--8<-- "docs_src/troubleshooting/tutorial005.py" +``` + +يبدأ الخادم ويُحل المسار، ثم تطبع `uvicorn` هذا لكل طلب: + +```text +ERROR: Exception in ASGI application +Traceback (most recent call last): + ... +RuntimeError: Task group is not initialized. Make sure to use run(). +``` + +يرى العميل 500. الحل هو دورة حياة في التطبيق **المضيف** تدخل `mcp.session_manager.run()`: + +```python +@asynccontextmanager +async def lifespan(app: Starlette) -> AsyncIterator[None]: + async with mcp.session_manager.run(): + yield + + +app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lifespan) +``` + +تشرح **[الإضافة إلى تطبيق موجود](run/asgi.md)** ذلك، بما فيه خوادم متعددة في تطبيق واحد وFastAPI. رسالتان قريبتان من الصنف نفسه: + +* `StreamableHTTPSessionManager .run() can only be called once per instance. Create a new instance if you need to run again.` المدير أحادي الاستخدام؛ ويؤدي الدخول في دورة حياة التطبيق نفسه مرتين إلى هذه الرسالة. +* لا توجد `mcp.session_manager` إلا **بعد** استدعاء `streamable_http_app()`، فابنِ المسارات أولًا، ولا تستخدم المدير إلا داخل دورة الحياة. + +## `MCPError: Session not found` {#mcperror-session-not-found} + +لا يتعرف الخادم على `Mcp-Session-Id` الذي أرسله عميلك. إما أن الخادم **أُعيد تشغيله** (أو وُجهت إلى نسخة أخرى)، أو **انتهت صلاحية** الجلسة لعدم وجود عمل جارٍ طوال `session_idle_timeout`، وافتراضيها 30 دقيقة. راجع [عمر الجلسة وحدودها](run/legacy-clients.md#session-lifetime-and-limits). تعيش الجلسات في ذاكرة تلك العملية وحدها. + +ليس هناك خطأ برمجي في الخادم تبحث عنه. رد HTTP هو `404` وجسمه JSON-RPC فعلًا، ولذلك يعرضه `Client` في Python حرفيًا، بخلاف `421` أعلاه: + +```json +{"jsonrpc": "2.0", "id": null, "error": {"code": -32600, "message": "Session not found"}} +``` + +الحل هو إعادة الاتصال: اخرج من كتلة `async with Client(...)` وادخل أخرى جديدة، فتتفاوض على جلسة جديدة. للعميل طويل العمر، يعني ذلك التقاط `MCPError` حول الاستدعاءات وإعادة الاتصال عند هذه الرسالة، بدلًا من إعادة المحاولة داخل جلسة ميتة. + +إذا حدث ذلك *دون* إعادة تشغيل ودون أن يصمت العميل لهذه المدة، فأنت تشغّل أكثر من عامل دون جلسات ثابتة التوجيه: يحتفظ كل عامل بجدول جلساته الخاص، فيؤدي توجيه الطلب إلى العامل الخطأ إلى هذه الحالة. تشرح **[النشر والتوسّع](run/deploy.md)** و**[خدمة العملاء القدامى](run/legacy-clients.md)** ذلك وحلّيه (تثبيت التوجيه أو `stateless_http=True`). + +بالنسبة إلى مشغّل الخادم، رسالة السجل المطابقة هي `Rejected request with unknown or expired session ID: `. تُسجّل بمستوى `INFO`، فلا تظهر عند حد `WARNING` المعتاد. ظهورها في دفعات بعد النشر مباشرة طبيعي؛ فكل العملاء المتصلة تعيد الاتصال. أما عند انتهاء صلاحية الجلسة، فيسبقها `Session idle timeout`، بمستوى `INFO` أيضًا. + +## `MCPError: Method not found` {#mcperror-method-not-found} + +أرسل طرف طلب JSON-RPC لا يملك الطرف الآخر دالة لمعالجته، وتسمّي `e.error.data` الطريقة. السبب المعتاد **اختلاف الجيلين**: طريقة موجودة في إصدار بروتوكول لا في الآخر، أُرسلت إلى طرف على الإصدار غير المناسب، مثل وصول `resources/subscribe` من جيل `2025` إلى اتصال `2026-07-28`، أو إرسال `subscriptions/listen` الخاصة بـ`2026` من عميل مثبت على `mode="legacy"`. توضح **[إصدارات البروتوكول](protocol-versions.md)** دعم الطرفين، وتشرح **[الإكمالات](servers/completions.md)** السبب الآخر (قدرة اختيارية لم تسجّل لها دالة معالجة). + +حالة واحدة **لا** تنتج هذا الخطأ، رغم أنها طلب أزاله البروتوكول الحديث: أداة تستدعي `ctx.elicit()` في اتصال `2026-07-28`. يرفض الخادم *إرسال* الطلب أصلًا، فتحصل بدلًا منه على `Cannot send 'elicitation/create': ...`، الموضّح لاحقًا في هذه الصفحة. + +## `MCPError: Client did not declare the form elicitation capability required by resolver ''` {#mcperror-client-did-not-declare-the-form-elicitation-capability-required-by-resolver-name} + +يريد خادمك سؤال المستخدم، ولم يعلن هذا العميل أنه يستطيع تلقي السؤال. + +يسأل Bistro هذا قبل الحجز باستخدام دالة حل: + +```python title="server.py" hl_lines="15-17 21" +--8<-- "docs_src/troubleshooting/tutorial007.py" +``` + +شغّله بدلًا من خادم Weather واستدعِ `book_table` من عميل لم يمرّر `elicitation_callback`. ترفض دالة الحل مسبقًا، لأن العميل المتصل لم يعلن استقاء المعلومات بنموذج، وتسمّي `e.error.data` ما ينقص بالضبط: + +```json +{ + "code": -32021, + "message": "Client did not declare the form elicitation capability required by resolver 'server:ask_to_confirm'", + "data": {"requiredCapabilities": {"elicitation": {"form": {}}}} +} +``` + +مرّر `elicitation_callback=` إلى `Client(...)`. تسجيل دالة رد النداء *هو* إعلان القدرة؛ لا يوجد خيار ثانٍ: + +```python +async def main() -> None: + async with Client("http://localhost:8000/mcp", elicitation_callback=handle_elicitation) as client: + result = await client.call_tool("book_table", {"date": "Friday"}) +``` + +تسرد **[دوال رد نداء العميل](client/callbacks.md)** غيرها (`sampling_callback` و`list_roots_callback`)، وكل واحدة إعلان بالطريقة نفسها. + +!!! info + يمثل `-32021` الثابت `MISSING_REQUIRED_CLIENT_CAPABILITY`، أحد ثلاثة رموز أخطاء تضيفها مواصفة + 2026-07-28. لا يمثل أي منها صنف استثناء: تصل كلها كـ`MCPError`، + وتفحص `e.error.code` لمعرفتها. تصدّر `mcp.types` الثوابت. الرمزان الآخران هما + `-32020` و`HEADER_MISMATCH` (تختلف ترويسة HTTP عن جسم الطلب الذي ترافقه)، + و`-32022` و`UNSUPPORTED_PROTOCOL_VERSION` (سمّى الطلب إصدارًا لا يدعمه + الخادم). لا يستطيع عميل SDK متوافق إنتاج أي منهما، فإذا رأيتهما، فافحص ما + يعيد كتابة الطلبات بين العميل والخادم. + +## `MCPError: Elicitation not supported` {#mcperror-elicitation-not-supported} + +النقص نفسه الذي تشير إليه `Client did not declare the form elicitation capability ...`، بصياغة المسارات التي لا تفحص مسبقًا: احتاج الخادم إلى إجابة عن استقاء معلومات، ولم يسجّل العميل المتصل `elicitation_callback`. + +تراه من `ctx.elicit()` في اتصال قديم، وفي أي اتصال عند وصول سؤال مُعاد متعدد الجولات (**[الطلبات متعددة جولات الطلب والرد](handlers/multi-round-trip.md)**) إلى عميل بلا دالة رد نداء للإجابة عنه. الحل نفسه: مرّر `elicitation_callback=` إلى `Client(...)`. لا توجد حالة «لم يُسأل المستخدم» تتلقاها أداتك كـ`decline`؛ فالعميل الذي لا يمكن سؤاله يؤدي إلى إخفاق الاستدعاء، فصمّم أدواتك مع مراعاة ذلك. + +## `MCPError: Cannot send 'elicitation/create': this transport context has no back-channel for server-initiated requests.` {#mcperror-cannot-send-elicitationcreate-this-transport-context-has-no-back-channel-for-server-initiated-requests} + +حاولت دالة المعالجة الوصول إلى العميل أثناء الطلب، في اتصال لا يملك الاستدعاء فيه قناة تحمل طلبًا من الخادم. توجد ثلاثة إعدادات للخادم تؤدي إلى ذلك. + +**اتصال `2026-07-28`: أي وسيلة نقل، دائمًا.** لا يتضمن البروتوكول الحديث طلبات يبدأها الخادم أصلًا، فيرفض الخادم قبل إرسال أي شيء. استخدام `ctx.elicit()` داخل أداة هو المثال المعتاد، غالبًا في أول **[اختبار](get-started/testing.md)** داخل الذاكرة لتلك الأداة، لأن `Client(mcp)` تتفاوض على `2026-07-28` تلقائيًا. لا يغيّر تمرير `elicitation_callback=` شيئًا، فلا يصل إلى العميل أي طلب ليجيب عنه: + +```python title="server.py" hl_lines="16" +--8<-- "docs_src/troubleshooting/tutorial006.py" +``` + +```python +async def test_book_table() -> None: + async with Client(mcp) as client: + await client.call_tool("book_table", {"date": "Friday"}) +``` + +```text +mcp.shared.exceptions.MCPError: Cannot send 'elicitation/create': this transport context has no back-channel for server-initiated requests. +``` + +**اتصال قديم بخادم `stateless_http=True`.** يعني انعدام الحالة أن كل طلب مستقل: لا جلسة ولا تدفّق من الخادم إلى العميل، فلا يوجد موضع لإرسال `elicitation/create` (أو `sampling/createMessage` أو `roots/list`) حتى للجيل الذي يدعمها: + +```python title="server.py" hl_lines="16 23" +--8<-- "docs_src/troubleshooting/tutorial008.py" +``` + +**اتصال قديم بخادم `json_response=True`.** يُجاب عن `POST` بجسم JSON واحد، والجسم الواحد يحمل الرد فقط، فلا يوجد هنا أيضًا التدفّق الخاص بالطلب الذي تحتاجه `ctx.elicit()` أثناء الطلب. تبقى الجلسة و`Mcp-Session-Id` وتدفّقها المستقل موجودة؛ اختفت القناة الخاصة بالطلب فقط. + +تسمّي الرسالة الطريقة التي تعذر إرسالها. يرفع الخادم الصنف `NoBackChannelError`، لكن الشبكة تحمل `MCPError` الأساسية فقط، ولذلك تظهر الجملة أعلاه في آخر سطر من التتبّع، لا اسم الصنف. + +بالنسبة إلى عميل `2026-07-28`، الحل نفسه في الحالات الثلاث: لا تتواصل عكسيًا أثناء الاستدعاء. انقل السؤال إلى **دالة حل** (أو أعِد `InputRequiredResult` بنفسك)، فيصبح جزءًا من *الرد* الذي يستطيع كل اتصال حمله: + +```python title="server.py" hl_lines="15-17 21" +--8<-- "docs_src/troubleshooting/tutorial007.py" +``` + +السؤال نفسه، و`elicitation_callback` نفسها في العميل. الاختلاف في التنفيذ: تتيح دالة الحل للخادم *إعادة* السؤال من الاستدعاء بدلًا من دفعه، فلا يُرسل طلب من الخادم إلى العميل. يحل ذلك المشكلة لكل عميل `2026-07-28`، أيًا كان إعداد الخادم من الثلاثة. أما العميل *القديم* فلا تكفيه إعادة الكتابة وحدها: لا توجد في `2025-11-25` طريقة لإعادة سؤال في النتيجة، ولذلك تظل دالة الحل في اتصال قديم ترسل `elicitation/create` عبر القناة الخاصة بالطلب، وتحتاج إلى خادم يبقيها — دون `stateless_http=True` أو `json_response=True`. تغطي **[استقاء المعلومات](handlers/elicitation.md)** دوال الحل؛ وتشرح **[الطلبات متعددة جولات الطلب والرد](handlers/multi-round-trip.md)** ما يحدث على الشبكة. + +!!! check + الأداة التي تستخدم `ctx.elicit()` ليست خاطئة؛ إنها من جيل *ما قبل 2026*. اتصل بـ`mode="legacy"` + (مصافحة `initialize` التقليدية، ومواصفة `2025-11-25` أو أقدم) بخادم لا يستخدم + `stateless_http=True` ولا `json_response=True`، وستعمل، لأن قناة الخادم إلى العميل + موجودة هناك. + تشرح **[إصدارات البروتوكول](protocol-versions.md)** ما يتوفر في كل إصدار. + +## `MCPError: Invalid or expired requestState` {#mcperror-invalid-or-expired-requeststate} + +تعذر على الخادم التحقق من رمز `requestState` الذي أعاده عميلك، فرفض الجولة. + +يمثل `requestState` رمز الاستئناف المعتم الذي يحمله استدعاء **[متعدد الجولات](handlers/multi-round-trip.md)** بين مراحله. تحميه `MCPServer` عند الإرسال وتتحقق من كل إعادة له، وتفحص *كل* `request_state` واردة في `tools/call` و`prompts/get` و`resources/read`، حتى لدالة لا تصدر رمزًا أصلًا. ولذلك يُرفض الرمز الذي لم تحمه هذه العملية أينما وصل: + +```python +async def main() -> None: + async with Client("http://localhost:8000/mcp") as client: + await client.call_tool("forecast", {"city": "London"}, request_state="round-1-from-worker-a") +``` + +```text +mcp.shared.exceptions.MCPError: Invalid or expired requestState +``` + +نص الرسالة ثابت عن قصد: لا تكشف الشبكة أي فحص أخفق. يصل السبب إلى **سجل الخادم**، وقراءته هي التشخيص كاملًا: + +```text +WARNING mcp.server.request_state: requestState rejected on tools/call: malformed +``` + +الأسباب التي ستراها فعلًا: + +* **`unknown key`** هو الأهم. يُولّد مفتاح الحماية الافتراضي عند بدء العملية، ولذلك تكون إعادة المحاولة التي تصل إلى **عامل مختلف** أو نسخة أخرى خلف موازن أحمال أو الخادم نفسه **بعد إعادة التشغيل** محمية بمفتاح لم تملكه هذه العملية. لا يعني ذلك مهاجمًا؛ بل إعدادًا افتراضيًا يُستخدم مع أكثر من عملية. +* **`audience`**: حمت الرمز نسخة لها *اسم خادم مختلف*. الاسم هو ادعاء الجمهور الافتراضي للحماية، ولذلك يجب أن تشارك النسخ الاسم (أو تعيّن `RequestStateSecurity(audience=...)` صراحةً)، إلى جانب المفاتيح. +* **`expired`**: استغرقت الجولة أكثر من `ttl` للحماية، وهي 600 ثانية لكل جولة، لا لكل استدعاء. +* **`malformed`** / **`codec error`**: تغير الرمز أثناء النقل، أو لم يكن رمزًا محميًا أصلًا. +* **`request binding`**: عاد الرمز بأداة مختلفة أو وسيطات مختلفة أو طريقة مختلفة. + +حل تعدد العمليات هو وسيطة واحدة (`keys` *نفسها* في كل نسخة)، مع شيء ليس وسيطة أصلًا: *اسم* الخادم نفسه (أو `audience=` مشتركة وصريحة). + +```python +mcp = MCPServer("Weather", request_state_security=RequestStateSecurity(keys=[key])) +``` + +تحمي `keys[0]` الرموز، ويتحقق كل مفتاح في القائمة منها، مما يتيح تدوير المفاتيح دون توقف. تشرح **[الطلبات متعددة جولات الطلب والرد](handlers/multi-round-trip.md#protecting-requeststate)** ما تحميه الآلية وتسلسل التدوير، وتعرض **[النشر والتوسّع](run/deploy.md)** إخفاق العاملين كاملًا وحلّه ذا الجزأين. + +!!! tip + ترفض `keys=[...]` المفتاح الضعيف فورًا برسالة مفيدة على غير المعتاد: + + ```text + ValueError: request-state keys must be at least 32 bytes of secret randomness; keys[0] is 7 bytes. Generate one with: python -c "import secrets; print(secrets.token_hex(32))" + ``` + + نفّذ ما تقوله. + +## هل ما زلت عالقًا؟ {#still-stuck} + +* إذا لم تجد رسالة أنتجتها SDK في هذه الصفحة، فهذه مشكلة توثيق تستحق الإبلاغ عنها بذاتها. +* ابحث في [متعقّب البلاغات](https://github.com/modelcontextprotocol/python-sdk/issues)؛ فقد شرح شخص بالفعل معظم رسائل الأخطاء الموجودة فيه. +* لم تجد شيئًا؟ [افتح بلاغًا](https://github.com/modelcontextprotocol/python-sdk/issues/new?template=v2-feedback.yaml) مع التتبّع الكامل، أو اسأل في [#python-sdk-dev على Discord الخاص بمساهمي MCP](https://discord.gg/6CSzBmMkjX). + +## مراجعة {#recap} + +* لا تمثل `ExceptionGroup: unhandled errors in a TaskGroup` الخطأ الفعلي مطلقًا. اقرأ **السطر الأخير**؛ والتقاط `MCPError` *داخل* كتلة `async with Client(...)` يتجنب التغليف بالكامل. +* لا ترفع `call_tool` استثناءً لأداة تفشل. تمثل `Error executing tool ...` و`Unknown tool: ...` نتائج: افحص `result.is_error`. غياب رسالة بعد اسم الأداة يعني أنها تعطلت، ويوجد التتبّع في سجل الخادم. +* `Client must be used within an async context manager` -> استخدم `async with`. و`Use @tool() instead of @tool` -> أضف الأقواس. +* `has an invalid x-mcp-header annotation` -> لا يمكن تعليم سوى وسيطات `str` و`int` و`bool`. +* `Tool already exists:` في سجل الخادم هي الإشارة الوحيدة إلى دمج أداتين تحملان الاسم نفسه في واحدة. +* رد 421 واحد بثلاث صيغ: `Server returned an error response` (في `Client` الخاص بـPython)، و`421 Misdirected Request` / `Invalid Host header` (في غيره)، و`Invalid Host header: ` (في سجل الخادم). الحل: `transport_security=TransportSecuritySettings(allowed_hosts=[...])`. +* `Task group is not initialized` -> تطبيق مركّب لم تدخل دورة حياة تطبيقه المضيف `mcp.session_manager.run()`. +* `Session not found` -> أُعيد تشغيل الخادم أو انتهت صلاحية الجلسة (`session_idle_timeout`)؛ أعِد الاتصال. +* `Cannot send 'elicitation/create': ... no back-channel ...` -> تحتاج `ctx.elicit()` إلى قناة من الخادم إلى العميل: لا يملكها اتصال `2026-07-28` مطلقًا، وتزيل `stateless_http=True` قناة الجيل القديم، وتزيل `json_response=True` القناة الخاصة بالطلب. استخدم دالة حل (ويحتاج العميل القديم أيضًا إلى خادم يبقي القناة). أما `Method not found` المجاورة فهي طلب لطريقة لا توجد في إصدار بروتوكول الطرف الآخر. +* `Client did not declare the form elicitation capability ...` و`Elicitation not supported` -> يفتقد العميل `elicitation_callback=`. +* لا توضّح `Invalid or expired requestState` السبب على الشبكة مطلقًا. يوضّحه سجل الخادم؛ وتعني `unknown key` ضرورة مشاركة `RequestStateSecurity(keys=[...])` بين العمال. diff --git a/i18n/ar/pages/whats-new.md b/i18n/ar/pages/whats-new.md new file mode 100644 index 0000000000..50c2a95743 --- /dev/null +++ b/i18n/ar/pages/whats-new.md @@ -0,0 +1,218 @@ +--- +translation: + sections: [cfe01c0c5863dfa2, 0dbb68d8b210177b, bd42ee3a268f6ea4, 875eb2889263424e] + tool: 1 +--- +# الجديد في v2 {#whats-new-in-v2} + +حدث أمران معًا في v2. **أُعيد بناء SDK**: محرك جديد تحت العميل والخادم، و`Client` بواجهة مباشرة، ومجموعة تغييرات أسماء يواجهها مشروع v1 عند أول استيراد. و**تغير البروتوكول**: يدعم v2 إصدار MCP بتاريخ 2026-07-28، الذي يزيل مصافحة الاتصال والجلسة وكل طلب يبدأه الخادم، دون قطع الدعم عن عملائك الحاليين. + +تجول هذه الصفحة في الجانبين، بقسم لكل تغيير رئيسي ينتهي بصفحة الموضوع. أما دليل النقل فهو **[دليل الترحيل](migration.md)**: كل تغيير غير متوافق مع السابق، مع شيفرة قبل التغيير وبعده. + +!!! note "v2 هو الإصدار المستقر" + يثبّت `pip install mcp` سلسلة 2.x، وتحتوي صفحة **[التثبيت](get-started/installation.md)** على + أمر التثبيت الجاهز للنسخ. إذا تعطل شيء في v2 أو فاجأك أو عطّل عملك، + [أخبرنا](https://github.com/modelcontextprotocol/python-sdk/issues/new?template=v2-feedback.yaml). + +## SDK: من v1 إلى v2 {#the-sdk-v1-to-v2} + +### أصبحت `FastMCP` هي `MCPServer` {#fastmcp-is-now-mcpserver} + +أُعيدت تسمية صنف الخادم عالي المستوى ووحدته معه. هذا أول ما يواجهه كل خادم v1، لأن مسار الاستيراد القديم أُزيل ولم يُهمل فقط: + +```python +from mcp.server import MCPServer # v1: from mcp.server.fastmcp import FastMCP + +mcp = MCPServer("Demo") # v1: FastMCP("Demo") +``` + +وهذا أيضًا معظم عملية النقل لخادم مبني بالمزخرفات. تقبل `@mcp.tool()` و`@mcp.resource()` و`@mcp.prompt()` ما كانت تقبله في v1 (تضيف `@mcp.resource()` وسيطة مسماة اختيارية واحدة هي `security=`)، ويظل مخطط الإدخال مشتقًا من تلميحات الأنواع. أما التفاصيل الأخرى: ينتقل كل ما تحت `mcp.server.fastmcp.*` إلى `mcp.server.mcpserver.*`، وتصبح `ctx.fastmcp` هي `ctx.mcp_server`، وتختفي `get_context()` (أعلن مَعلمة `ctx: Context` بدلًا منها)، ويصبح أساس الاستثناءات `FastMCPError` هو `MCPServerError`. يحتوي **[دليل الترحيل](migration.md#fastmcp-renamed-to-mcpserver)** على جدول الاستيرادات. + +### `Resolve`: الطريقة الجديدة لطلب إدخال من المستخدم {#resolve-the-new-way-to-ask-the-user-for-input} + +لا ينبغي أن يأتي كل ما تحتاجه الأداة من النموذج. الجديد في v2 أن مَعلمة الأداة المعلّقة بـ`Resolve(fn)` تملؤها دالة تكتبها أنت، دون أن يراها النموذج، ويمكن أن تعيد الدالة `Elicit(...)` لعرض سؤال للمستخدم. هذه الطريقة المفضّلة للحصول على أي شيء من العميل أثناء الاستدعاء: تحمل SDK السؤال بالآلية التي يدعمها الاتصال (طلب استقاء معلومات مباشر لعميل قديم، أو عدة جولات في 2026-07-28)، فيخدم جسم أداة واحد الجيلين. تشرح ذلك صفحة **[الاعتماديات](handlers/dependencies.md)**. + +!!! note + يبقى الشكلان الآخران عند الحاجة: تظل `ctx.elicit()` تعمل للعملاء على + الاتصالات القديمة (**[استقاء المعلومات](handlers/elicitation.md)**)، ويمكن للدالة إعادة + `InputRequiredResult` بنفسها وإدارة الجولات يدويًا، وهي أيضًا طريقة نقل طلبات أخذ العينات + والمجلدات الجذرية في 2026-07-28 (**[الطلبات متعددة جولات الطلب والرد](handlers/multi-round-trip.md)**). + +### `Client` بواجهة مباشرة {#a-first-class-client} + +أعطاك v1 ثلاث طبقات متداخلة: مدير سياق لوسيلة النقل ينتج تدفقات خامًا، و`ClientSession` تغلّفها، واستدعاء يدوي لـ`await session.initialize()`. يقدم v2 كائنًا واحدًا: + +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/client/tutorial001_client.py" +``` + +يقبل `Client` عنوان URL (Streamable HTTP)، أو `StdioServerParameters` (عملية فرعية عبر stdio)، أو أي مدير سياق نقل آخر مثل `sse_client(...)`، أو كائن الخادم نفسه في الاختبارات (داخل الذاكرة، دون نقل). يتصل الدخول في `async with` ويتفاوض على إصدار البروتوكول، أيًا كان جيل الخادم؛ وتتوفر بعدها `client.server_capabilities` و`client.protocol_version` مباشرة، وكذلك `client.server_info` عندما يعرّف الخادم نفسه (أصبح نوعها `Implementation | None`، لأن الهوية اختيارية في جيل 2026). تظل دوال رد نداء أخذ العينات واستقاء المعلومات التي سجّلتها في v1 تعمل (تخضع أجسامها لإعادة تسمية الخصائص إلى snake_case مثل بقية هذه الصفحة)، وتجيب الآن أيضًا عن طلبات نمط 2026 المضمّنة في النتائج (أدناه)، وتعمل بالتزامن بدلًا من واحدة في كل مرة. تظل `ClientSession` تحتها لمن يريد الواجهة منخفضة المستوى، وتتيحها `client.session`؛ وقد تغيرت أيضًا (تعمل على محرك التوزيع الجديد، وتغيرت بعض توقيعاتها)، فاقرأ **[دليل الترحيل](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)** قبل الانتقال إليها. + +تقدّمها صفحة **[العميل](client/index.md)**، وتغطي **[وسائل نقل العميل](client/transports.md)** أشكال الاتصال الأربعة، وتشرح **[دوال رد نداء العميل](client/callbacks.md)** تلك الدوال، وتعرض **[الاختبار](get-started/testing.md)** نمط الاتصال داخل الذاكرة الذي يحل محل دالة v1 المساعدة `create_connected_server_and_client_session()`. + +### أُعيد بناء `Server` منخفض المستوى، ولم تُعد تسميته {#the-low-level-server-was-rebuilt-not-renamed} + +إذا كنت تعمل في طبقة JSON-RPC، فهذا الجزء من v2 تتغير فيه الآلية بالكامل. إليك خادم الأداة الواحدة نفسه بالطريقتين؛ انقر العلامات لمعرفة ما تغير. + + + +```python title="v1" +from typing import Any + +import mcp.types as types +from mcp.server.lowlevel import Server + +server = Server("Bookshop") + + +@server.list_tools() # (1)! +async def list_tools() -> list[types.Tool]: + return [ # (2)! + types.Tool( + name="search_books", + description="Search the catalog by title or author.", + inputSchema={ # (3)! + "type": "object", + "properties": {"query": {"type": "string"}}, + "required": ["query"], + }, + ) + ] + + +@server.call_tool() +async def call_tool(name: str, arguments: dict[str, Any]) -> list[types.ContentBlock]: # (4)! + if name != "search_books": + raise ValueError(f"Unknown tool: {name}") # (5)! + ctx = server.request_context # (6)! + return [types.TextContent(type="text", text=f"Found 3 books matching {arguments['query']!r}.")] # (7)! +``` + +1. تُسجّل دوال المعالجة بالمزخرفات (المستدعاة مع أقواس)، في أي وقت بعد إنشاء الخادم. +2. تعيد قائمة `list[Tool]` مجردة، وتغلّفها SDK في `ListToolsResult`. +3. تستخدم الحقول camelCase في Python، ويُفرض المخطط **فعليًا**: تتحقق SDK عبر jsonschema من وسيطات `call_tool` قبل تشغيل دالتك، ولذلك تكون `arguments["query"]` أدناه آمنة. +4. تخدم دالة `call_tool` واحدة كل الأدوات، وتتلقى اسم الأداة والوسيطات المتحقق منها بالفعل، مفككة ولا تكون `None` مطلقًا. +5. رفع الاستثناء هو طريقة أداة v1 للإشارة إلى الإخفاق: يُلتقط أي استثناء ويُعاد كـ`CallToolResult(isError=True)` مع `str(e)` كنصه، فيقرأ النموذج المستدعي الرسالة ويستطيع إعادة المحاولة. +6. يأتي السياق من ContextVar ضمنية، تصل إليها عبر كائن الخادم أثناء الطلب. +7. تُغلّف كتل المحتوى المجردة في `CallToolResult` نيابة عنك. + +```python title="v2" +--8<-- "docs_src/whats_new/tutorial001.py" +``` + +1. أصبحت الحقول snake_case، والمخطط **معلنًا لكنه لا يُطبّق مطلقًا**: لا يفحص شيء الوسيطات قبل تشغيل دالتك. +2. لكل دالة معالجة الشكل نفسه: `async (ctx, params) -> result`. السياق هو الوسيطة الأولى (توجد عليه `ctx.session` و`ctx.request_id` و`ctx.protocol_version`)؛ وهذا هو البديل لـ`server.request_context`. +3. تبني `ListToolsResult` كاملةً بنفسك. أصبحت إعادة قائمة مجردة تؤدي إلى `TypeError` على جانب الخادم، بدلًا من أن تغلّفها SDK. +4. مَعلمات ذات أنواع داخلة (`params.name` و`params.arguments`)، ونتيجة كاملة خارجة. لا فك ولا تغليف ولا تحويل نيابة عنك. +5. الفحص نفسه بصيغة أخرى. تصل `ValueError` هنا إلى النموذج كخطأ معتم `-32603` (أدناه)، ولذلك يُرفع الخطأ المقصود على الشبكة كـ`MCPError`: يمر برمزه ورسالته كما هما، و`-32602` مع هذا النص هو رد المواصفة نفسها على أداة غير معروفة. +6. يمكن أن تكون `params.arguments` مساوية لـ`None`؛ وكان v1 يحوّلها افتراضيًا إلى `{}` قبل أن تراها شيفرتك. مع غياب التحقق قبل الدالة، يعتمد التنفيذ على هذا السطر. +7. يصبح الاستثناء غير المتوقع المرفوع هنا خطأ بروتوكول **منقّحًا**، هو `-32603` مع `"Internal server error"`: لا يرى النموذج الرسالة مطلقًا. لإخفاق يجب أن يقرأه النموذج ويتفاعل معه، أعِد `CallToolResult(is_error=True, ...)`. +8. دوال المعالجة وسيطات للمُنشئ، فتكتمل واجهة الخادم لحظة إنشائه؛ وتمثل `add_request_handler()` منفذ التحكم المباشر بعد الإنشاء، ومدخل الطرائق المخصصة. + +المثال هو النمط. عمومًا، لكل دالة الشكل نفسه: مَعلمات ذات أنواع داخلة ونتيجة كاملة خارجة؛ اختفى فحص jsonschema القديم لوسيطات الأدوات؛ والاستثناء خطأ بروتوكول، لا نتيجة أداة `is_error=True`؛ واختفت ContextVar الضمنية `server.request_context`. تُدعم طرائق المورّد المخصصة ضمن نطاق أسمائه مباشرة عبر `add_request_handler(method, params_type, handler)`، التي تتحقق من المَعلمات الواردة مقابل نموذجك قبل تشغيل الدالة. وتغلّف قائمة `middleware` (الموسومة بالمؤقتة عمدًا) كل رسالة واردة، بدلًا من طرائق `_handle_*` الخاصة التي اعتاد البعض تجاوزها. + +تحت ذلك، استُبدلت حلقة استقبال `BaseSession` في v1 بمحرك توزيع يشترك فيه العميل والخادم الآن، وهو ما يتيح عدة أمور في هذه الصفحة معًا: يخدم كائن `Server` واحد جيلي البروتوكول، وتوزّع `Client(server)` داخل العملية دون تغليف JSON-RPC، ويلغي انتهاء مهلة طلب العميل الآن دالة المعالجة على الخادم فعلًا. + +تشرح ذلك صفحة **[الخادم منخفض المستوى](advanced/low-level-server.md)**؛ ويتناول **[دليل الترحيل](migration.md#lowlevel-server-decorator-based-handlers-replaced-with-constructor-on_-params)** كل منفذ أُزيل. إذا لم تنتقل إلى مستوى أدنى من `MCPServer`، فلن تؤثر هذه التغييرات فيك. + +### انتقلت أنواع النقل إلى `mcp-types`، وأصبح كل حقل snake_case {#the-wire-types-moved-to-mcp-types-and-every-field-is-snake_case} + +توجد أنواع البروتوكول الآن في توزيع مستقل، `mcp-types`. لا يعتمد إلا على pydantic وtyping-extensions، فتستطيع بوابة أو وكيل أو مولّد شيفرة استهلاك أشكال MCP المنقولة دون تثبيت حزمة HTTP: يثبّت هذا المشروع `mcp-types` ويستورد `mcp_types`. تعتمد `mcp` نفسها على إصدار محدد تمامًا من الحزمة وتعيد إتاحتها، ولذلك تظل الشيفرة التي تعتمد على SDK تكتب `import mcp.types as types` و`from mcp.types import Tool` (اسم مستعار دائم، وكل اسم يشير إلى الكائن نفسه)، وتعلن اعتماديتها الفعلية الوحيدة، `mcp`. القاعدة العملية: استورد من الحزمة التي تعتمد عليها فعلًا. + +في هذه الأنواع، أصبحت كل خاصية Python بصيغة snake_case: `result.is_error` و`tool.input_schema` و`listing.next_cursor`. تبقى JSON المنقولة بصيغة camelCase كما كانت؛ تغيرت كتابة الخصائص فقط. ويصاحب ذلك افتراضيان أشد صرامة: تُهمل الحقول غير المعروفة بدلًا من إعادة نقلها (ضع الإضافات في `_meta`)، ويتحقق الطرفان من الحركة مقابل إصدار البروتوكول الذي تفاوضا عليه. راجع جدول الأسماء في **[دليل الترحيل](migration.md#field-names-changed-from-camelcase-to-snake_case)**. + +### انتقل إعداد النقل إلى `run()` {#transport-configuration-moved-to-run} + +تحدد `MCPServer(...)` *ماهية* الخادم: اسمه وتعليماته ودورة حياته ومصادقته. أما كيفية *تشغيله* فأصبحت من اختصاص `run()` وبناة التطبيقات، حيث انتقلت `host` و`port` و`stateless_http` و`json_response` ومسارات نقاط النهاية و`transport_security` (تؤدي `MCPServer("x", port=9000)` إلى `TypeError`). التحميلات الزائدة محددة الأنواع حسب وسيلة النقل، فيخبرك المحرر بالخيارات التي تقبلها `stdio` والتي تقبلها `streamable-http`. إزالة مهمة: اختفت `mount_path`؛ والطريقة المدعومة للعمل تحت بادئة مسار هي تركيب تطبيق ASGI. + +تغطي **[تشغيل خادمك](run/index.md)** الخيارات؛ وتغطي **[الإضافة إلى تطبيق موجود](run/asgi.md)** التركيب. + +### سلوك يتغير دون خطأ استيراد {#behavior-that-changes-without-an-import-error} + +تعلن تغييرات الأسماء عن نفسها. أما هذه التغييرات فلا: + +* **تعمل الدوال المتزامنة على خيط عامل.** لم تعد أداة `def` (أو مورد أو قالب توجيه أو دالة حل) تحجب حلقة الأحداث؛ والمقابل أن جسمها لا يعمل على خيط حلقة الأحداث نفسه، وهو مهم للشيفرة المرتبطة بخيط بعينه. لا تتغير دوال `async def`. **[دليل الترحيل](migration.md#sync-handler-functions-now-run-on-a-worker-thread)**. +* **أصبح `MCPError` (الذي كان `McpError` في v1) المرفوع داخل أداة خطأ بروتوكول.** لا يراه النموذج مطلقًا. يظل كل استثناء آخر يصبح نتيجة `is_error=True`، لكن لا تصل إلى النموذج سوى رسالة `ToolError`: تعرض الاستثناءات الأخرى الآن `Error executing tool `، ويكون التتبّع في سجل الخادم. تشرح **[معالجة الأخطاء](servers/handling-errors.md)** هذا الفصل. +* **تُفحص النتائج قبل خروجها.** تؤدي أداة `Tool` مبنية يدويًا مع `input_schema` تساوي `{}` إلى إخفاق `tools/list` الآن (تتطلب المواصفة `"type": "object"`). لا تواجه خوادم `@mcp.tool()` ذلك؛ تكتب SDK مخططاتها. +* **يتحقق عميلك مما يتلقاه.** تفحص `list_tools()` و`call_tool()` رد الخادم مقابل إصدار البروتوكول المتفاوض عليه، ولذلك يرفع خادم غير صالح تمامًا كان تحليل v1 المتساهل يتقبله `pydantic.ValidationError` الآن. إذا اتصلت بخوادم لا تتحكم فيها، فتوقع أن تكتشف هذه المشكلات؛ ويتضمن **[دليل الترحيل](migration.md#client-validates-inbound-traffic-against-the-protocol-schema)** التفاصيل. +* **أصبحت قوالب URI تطبّق RFC 6570 فعلًا.** تعمل `{+path}` و`{?query}` وغيرها، وتصبح المطابقة دقيقة بدلًا من تساهل التعابير النمطية، ويُرفض اجتياز المسارات في القيم المستخرجة افتراضيًا. تفشل القوالب غير المقبولة عند تطبيق المزخرف، لا عند أول طلب. **[قوالب URI](servers/uri-templates.md)**. +* **تعمل دورة حياة streamable HTTP مرة واحدة** عند بدء التشغيل، وتُشارك حالتها بين كل الجلسات والطلبات. في v1، كانت تعمل مرة لكل جلسة، ومرة لكل طلب تحت `stateless_http=True`. تصبح مجموعات الاتصالات والذاكرات المؤقتة المبنية فيها أقل تكلفة بكثير؛ أما ما كان يحجز موردًا لكل اتصال فيها، فموضعه الآن جسم دالة المعالجة. **[دورة الحياة](handlers/lifespan.md)**. +* **تثبّت `mcp dev` و`mcp install` البيئة التي تنشئانها** على إصدار SDK المثبّت لديك. يشغّل الأمران خادمك في بيئة `uv run --with ...` جديدة، وكانت تحل `mcp` إلى أحدث إصدار مستقر بدلًا من الإصدار الذي تطوّر عليه. **[دليل الترحيل](migration.md#mcp-dev-and-mcp-install-pin-the-spawned-environment-to-your-sdk-version)**. +* **أصبح عميل HTTP هو `httpx2` بدلًا من `httpx`.** يغيّر استبدال الاعتمادية ما تلتقطه شيفرتك وتمرّره (`httpx2.AsyncClient` و`httpx2.ConnectError`)، وكيفية التحقق من شهادات TLS: تتحقق `httpx2` عبر `truststore` مقابل مخزن ثقة نظام التشغيل بدلًا من قائمة CA المرفقة مع certifi. لا تلاحظ معظم البيئات ذلك؛ لكن حاوية دنيا بلا مخزن CA للنظام، أو CA خاصة لم تعرفها إلا حزمة certifi، تبدأ بإخفاق مصافحة TLS. عيّن `SSL_CERT_FILE`/`SSL_CERT_DIR` أو مرّر `verify=ssl_context` إلى عميلك. **[دليل الترحيل](migration.md#httpx-and-httpx-sse-replaced-by-httpx2)**. + +### ما أُزيل بالكامل {#removed-outright} + +لكل مما يلي قسم في **[دليل الترحيل](migration.md)**: + +* **وسيلة نقل WebSocket** في الجانبين، والإضافة `mcp[ws]`. لم تكن جزءًا من مواصفة MCP مطلقًا. +* واجهة **المهام التجريبية** (`mcp.*.experimental`). ينقل 2026-07-28 المهام من البروتوكول الأساسي إلى امتداد رسمي ([SEP-2663](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2663))، لم تنفّذه SDK هذه بعد. +* مسارات الاستيراد `mcp.shared.version` و`mcp.shared.progress` و`mcp.shared.session` (مع `RequestResponder` الذي استوردته تعليقات نوع `message_handler` في v1). (لم تُزَل `mcp.types`: تبقى اسمًا مستعارًا دائمًا لحزمة `mcp_types` المستقلة.) +* الاسم المهمل `streamablehttp_client`، ودالة رد النداء `get_session_id` من `streamable_http_client` (التي تنتج الآن تدفقين فقط). +* `McpError`، التي أُعيدت تسميتها **`MCPError`** بمُنشئ مباشر `(code, message, data)`. +* `MCPServer.get_context()` و`mount_path=` وطرائق المزخرفات وContextVar وقواميس دوال المعالجة في `Server` منخفض المستوى. + +## البروتوكول: من 2025-11-25 إلى 2026-07-28 {#the-protocol-2025-11-25-to-2026-07-28} + +ينفّذ v2 إصدار 2026-07-28، ويخدم **الإصدارين** في الوقت نفسه: يجيب تطبيق `streamable_http_app()` نفسه (وخادم stdio نفسه) عن `initialize` لعميل من جيل 2025 وعن طلبات عميل من جيل 2026، دون إعداد أو خيار تفعّله أو نشر منفصل. لا يقطع دعم الإصدار الجديد الاتصال بعميل يستخدم القديم. ما يلي هو تغييرات الإصدار الجديد نفسه. + +### دون مصافحة أو جلسة {#no-handshake-no-session} + +لا يفتح عميل 2026-07-28 اتصالًا ثم يتفاوض ثم يبدأ العمل. يحمل كل طلب إصدار البروتوكول ومعلومات العميل وقدراته في `_meta`، ويكون استدعاء الاكتشاف الوحيد، `server/discover`، طلبًا عاديًا كغيره. يتصرف `Client` بصورة صحيحة افتراضيًا: يفحص `server/discover` مرة واحدة ويعود إلى مصافحة `initialize` إذا كان الخادم أقدم. + +لا توجد `Mcp-Session-Id` عبر Streamable HTTP في مسار 2026، وهذا أهم تغيير تشغيلي: **لا شيء يربط الطلب الحديث بعامل محدد**، فتستطيع أي نسخة خلف موازن أحمال بالتناوب الدوري العادي الإجابة عنه. مع توضيحين. يظل عملاء جيل 2025 (وهم معظم العملاء حاليًا) يفتحون جلسات ويحتاجون إلى تثبيت التوجيه الذي احتاجوه في v1؛ لا يتغير شيء لهم. والشيء الوحيد الذي يجب أن تحمله إعادة محاولة *متعددة الجولات* بين العمال هو `request_state` المحمية، التي يُولّد مفتاحها الافتراضي لكل عملية، ولذلك تمرّر بيئة النشر الموسّعة `RequestStateSecurity(keys=[...])`. (لا ترتبط `stateless_http=True` بذلك: تؤثر فقط في خدمة عملاء جيل 2025، ولا تقرؤها حركة 2026 مطلقًا؛ إذا عيّنتها في v1، فلا يتغير شيء.) + +تشرح **[إصدارات البروتوكول](protocol-versions.md)** جانب العميل، وتقدّم **[النشر والتوسّع](run/deploy.md)** قائمة التشغيل (قائمة Host المسموح بها، ومفتاح `request_state`، والإشعارات بين النسخ)، وتشرح **[خدمة العملاء القدامى](run/legacy-clients.md)** خدمة الجيلين معًا. + +### لا يستطيع الخادم استدعاء العميل: طلبات متعددة الجولات {#the-server-cannot-call-the-client-multi-round-trip-requests} + +اختفى كل طلب يبدأه الخادم في 2026-07-28: استقاء المعلومات بالدفع وأخذ العينات و`roots/list`. لا توجد قناة لها في اتصال 2026، ولذلك تفشل فيه `ctx.elicit()` و`ctx.session.create_message()` باستخدام `NoBackChannelError` (وتظلان تعملان للعملاء القدامى). + +يعكس البديل اتجاه الاستدعاء. *تعيد* الأداة التي تحتاج إلى شيء من المستخدم السؤال (`InputRequiredResult`)، ويجيب عنه العميل بدوال رد النداء التي كان يملكها دائمًا، ويُعاد الاستدعاء مع إرفاق الإجابات. يدير `Client` الحلقة نيابة عنك. في الخادم، نادرًا ما تبني النتيجة بنفسك، لأن **[اعتمادية](handlers/dependencies.md)** تفعل ذلك: علّق مَعلمة بـ`Resolve(ask_quantity)`، حيث `ask_quantity` دالة عادية تكتبها أنت، وتسأل SDK بالآلية التي يدعمها الاتصال: طلب استقاء معلومات مباشر في جلسة قديمة، أو عدة جولات في 2026. جسم أداة واحد للجيلين: + +```python title="server.py" hl_lines="21" +--8<-- "docs_src/legacy_clients/tutorial001.py" +``` + +```python title="client.py" hl_lines="14-15" +--8<-- "docs_src/legacy_clients/tutorial001_client.py" +``` + +هذان الملفان يوضحان الفكرة كاملةً: خادم واحد، وأداة تعتمد على `Resolve`، وعميل قديم وآخر حديث يحصلان على إجابتهما من الخادم الجاري نفسه (تشرحهما **[خدمة العملاء القدامى](run/legacy-clients.md)**). تشرح **[الطلبات متعددة جولات الطلب والرد](handlers/multi-round-trip.md)** الآلية (بما فيها `request_state` التي تحميها SDK وتتحقق منها نيابة عنك)؛ وتغطي **[استقاء المعلومات](handlers/elicitation.md)** طرح الأسئلة. + +!!! warning "هذا هو الموضع الذي يتغير فيه سلوك خادم منقول من v1" + تواجهه اختباراتك أولًا: تتفاوض `Client(mcp)` على 2026-07-28 مع خادم v2 + افتراضيًا، فتفشل أداة تستدعي `ctx.elicit()` في اختبار كان ينجح على v1. انقل + السؤال إلى مَعلمة `Resolve(...)` (تعمل عبر الجيلين)، أو ثبّت عميل الاختبار على + `mode="legacy"` إذا أردت سلوك الدفع فعلًا. + +### أُهملت المجلدات الجذرية وأخذ العينات والتسجيل عبر البروتوكول؛ وأُزيلت `ping` {#roots-sampling-and-protocol-logging-are-deprecated-ping-is-removed} + +يهمل [SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577) ثلاث *قدرات* كاملة في كل إصدارات البروتوكول: المجلدات الجذرية وأخذ العينات والتسجيل على مستوى MCP (`ctx.info()` وما يشبهها). وهذا أمر مستقل عن غياب القناة العكسية أعلاه؛ فالإهمال إرشادي، وتظل الميزات تعمل في جلسات جيل 2025، ولا يتغير شيء في البيانات المنقولة. ما تلاحظه هو `MCPDeprecationWarning`، الذي ينتمي إلى `UserWarning` فيُطبع افتراضيًا؛ توقع أن يخبرك أول `ctx.info(...)` بعد الترقية بذلك. + +أما `ping` فأشد حسمًا: أُزيلت من البروتوكول، ولم تُهمل. أُزيلت طريقتان مستقلتان للميزات المهملة في 2026-07-28 بالطريقة نفسها، هما `logging/setLevel` و`notifications/roots/list_changed` من العميل، وأصبحت إشعارات التقدم من الخادم إلى العميل فقط. + +تتضمن **[الميزات المهملة](deprecated.md)** الجدول الكامل والبديل لكل ميزة ومرشح السطر الواحد إذا احتجت سجلًا هادئًا أثناء خدمة العملاء القدامى. + +### تصبح إشعارات التغيير تدفقًا واحدًا {#change-notifications-become-one-stream} + +في 2026-07-28، يحل `subscriptions/listen` محل تدفّق HTTP GET المستقل و`resources/subscribe`: يفتح العميل تدفقًا طويل العمر واحدًا ويسمّي أنواع الإشعارات التي يريدها. يخدمه `MCPServer` مباشرة؛ وتنشر باستخدام `await ctx.notify_resource_updated(uri)` (و`notify_tools_changed()` وغيرها)، ويمكن لبرمجية وسيطة رفض طلب الاستماع حسب المستدعي، وتستخدم بيئات النشر متعددة النسخ `SubscriptionBus` مشتركة. في العميل، تفتح `async with client.listen(...)` التدفّق: تدخل المرشحات كوسيطات مسماة، وتعود أحداث تغيير ذات أنواع، وتمثل `sub.honored` المجموعة الفرعية التي وافق الخادم على تسليمها. + +تغطي **[الاشتراكات](handlers/subscriptions.md)** النشر والخدمة، وتغطي **[نظيرتها للعملاء](client/subscriptions.md)** المراقبة، وتغطي **[النشر والتوسّع](run/deploy.md)** الناقل. + +### بقية التغييرات بإيجاز {#the-rest-quickly} + +* **الهوية بيانات وصفية اختيارية لكل رسالة.** مفتاح `clientInfo` في `_meta` للطلب اختياري (الزوج المطلوب هو `protocolVersion` + `clientCapabilities`)، وانتقلت `serverInfo` من جسم نتيجة `server/discover`: تضعها الخوادم بدلًا من ذلك في `_meta` لكل نتيجة من جيل 2026 ([المواصفة #3002](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/3002)). تضع SDK الوسم دائمًا؛ وتكون `client.server_info` مساوية لـ`None` عندما لا يعرّف الخادم نفسه (مثلًا، إذا حذفت برمجية وسيطة المفتاح). تعرض **[الخادم منخفض المستوى](advanced/low-level-server.md)** الوسم في البيانات المنقولة. +* **يمكن توجيه الطلبات دون تحليل أجسامها.** تحمل طلبات HTTP الحديثة `Mcp-Method` (و`Mcp-Name` للاستدعاءات الثلاث المتعلقة بالأدوات)؛ وتُعكس خاصية مخطط إدخال أداة المعلّقة بـ`x-mcp-header` في ترويسة `Mcp-Param-*` ويتحقق الخادم من تطابقهما ([SEP-2243](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2243)). تستطيع البوابات ومحددات المعدل التوجيه بالترويسات وحدها. تعرض **[مَعلمات الترويسات](advanced/header-parameters.md)** كيفية تعليم وسيطة. +* **تحمل النتائج تلميحات تخزين مؤقت.** تعلن نتائج القوائم والقراءات `ttlMs` و`cacheScope` ([SEP-2549](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2549))؛ وتعيّنهما لكل طريقة باستخدام `cache_hints=`، ويحترمهما `Client` بذاكرة ردود مدمجة. يتلقى الخادم الذي لا يرسل تلميحات (كل خادم يسبق 2026) حركة مطابقة دون تخزين. **[تلميحات التخزين المؤقت](client/caching.md)**. +* **الامتدادات مدعومة مباشرة.** تعلن الخوادم والعملاء حزم قدرات اختيارية تحت معرّفات DNS معكوسة ([SEP-2133](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2133))؛ ويمثل امتداد `Apps` المدمج (MCP Apps) المرجع. **[الامتدادات](advanced/extensions.md)** و**[MCP Apps](advanced/apps.md)**. +* **وُحّدت رموز الأخطاء.** المورد المفقود هو `-32602` مع URI في `error.data`، وتظهر الرموز الجديدة المحجوزة للمواصفة كـ`-32020` (عدم تطابق الترويسة) و`-32021` (قدرة مطلوبة مفقودة) و`-32022` (إصدار بروتوكول غير مدعوم). ترتّب **[استكشاف الأخطاء وإصلاحها](troubleshooting.md)** المعلومات حسب الرسائل المطابقة تمامًا. +* **أصبح استخدام التفويض بأمان أسهل.** يتحقق العميل من `iss` المُعادة مع رمز التفويض ([RFC 9207](https://datatracker.ietf.org/doc/html/rfc9207)؛ تعيد `callback_handler` الآن `AuthorizationCodeResult`)، ويرسل `application_type` عند التسجيل، ولا يعيد إرسال بيانات الاعتماد إلى خادم تفويض مختلف مطلقًا. الجديد للمؤسسات: تدفّق إفادة الهوية وفق [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990). يسرد **[دليل الترحيل](migration.md)** كل تغيير OAuth؛ وتشرح **[OAuth للعملاء](client/oauth-clients.md)** و**[إفادة الهوية](client/identity-assertion.md)** الاستخدام. +* **كل خادم قابل للتتبّع.** تُفعّل OpenTelemetry افتراضيًا كبرمجية وسيطة: يحصل كل طلب على مقطع تتبّع للخادم، دون تكلفة حتى تضبط العملية مُصدّرًا. عندما يشغّل الطرفان SDK، ينقل العميل أيضًا سياق تتبّع W3C في `_meta`، فتترابط التتبّعات. **[OpenTelemetry](run/opentelemetry.md)**. + +## هل ترقي من v1؟ {#upgrading-from-v1} + +* **[دليل الترحيل](migration.md)** هو القائمة الكاملة والدقيقة لما تغيّره؛ أما هذه الصفحة فتشرح الأسباب. +* **ستبقى سلسلة v1.x متاحة.** تنتقل إلى الصيانة وتواصل تلقي الإصلاحات الحرجة والتحديثات الأمنية، ولا يعطّلها إصدار مواصفة 2026-07-28؛ يوجد توثيقها في [/v1/](https://py.sdk.modelcontextprotocol.io/v1/). إذا نشرت مكتبة تعتمد على `mcp` ولم تكن مستعدًا للترحيل، فاحتفظ بحد أعلى (مثل `mcp>=1.28,<2`) حتى يبقى حل الاعتماديات غير المثبت على 1.x. +* هل وجدت شيئًا صعبًا أو مربكًا أو معطّلًا؟ **[أرسل ملاحظات v2](https://github.com/modelcontextprotocol/python-sdk/issues/new?template=v2-feedback.yaml)**؛ تُقرأ كلها. diff --git a/i18n/languages.yml b/i18n/languages.yml index 9e01802eef..e95587c611 100644 --- a/i18n/languages.yml +++ b/i18n/languages.yml @@ -2,6 +2,10 @@ model: claude-opus-5 # public model id; used for every translation call exclude: [migration.md] # nav pages never translated (exact path or "dir/**") languages: + - code: ar + name: العربية + theme: ar + hreflang: ar - code: de # directory under i18n/ and URL prefix /de/ name: Deutsch # switcher label (shown as "de - Deutsch") theme: de # theme `language` (UI strings, search)