امتداد Tasks في MCP: المهام غير المتزامنة والمهام الخلفية موضّحة
تنتهي مهلة استدعاءات الأدوات الطويلة، وتنقطع الاتصالات، ويضيع العمل. يعالج امتداد Tasks في MCP هذه المشكلة بمعرّف taskId دائم، واستطلاع عبر tasks/get، وحالات توقف بانتظار الإدخال input_required، وإلغاء تعاوني. تعرّف على دورة الحياة، وحمولات JSON، ومثال لخادم FastMCP، وعادات العملاء التي تصمد أمام الأعطال.
يستدعي وكيل الذكاء الاصطناعي أداة، فتحتاج الأداة إلى أربعين دقيقة، وفي حوالي الدقيقة الثانية يغلق وسيط الشبكة الاتصال. قد تظل المهمة قيد التشغيل على الخادم، لكن لم يعد بالإمكان الوصول إليها، ويبقى النموذج أمام خطأ بدل إجابة. هذه الفجوة بالضبط هي ما صُمم امتداد Tasks في MCP لسدّها. فبدل إبقاء طلب واحد مفتوحًا حتى ينتهي العمل، يعيد الخادم فورًا معرّفًا دائمًا (taskId)، ويستعلم العميل عنه متى شاء.
يشرح هذا المقال كيف تعمل المهام غير المتزامنة والمهام الخلفية في Model Context Protocol: ما هو الامتداد، وما معنى كل حالة، وكيف تبدو الحمولات، وكيف تبني خادمًا يدعم المهام، وأي عادات للعميل تحافظ على الأعمال الطويلة آمنة. أسماء الحقول أدناه مأخوذة من مواصفات الامتداد المنشورة (io.modelcontextprotocol/tasks، SEP-2663) ومن توثيق FastMCP.
لماذا تفشل الاستدعاءات المحجوبة
يتصرف استدعاء MCP tools/call القياسي كزبون واقف عند المنضدة ينتظر طبقه. يُرسل الطلب، ويبقى الاتصال مفتوحًا، وتعود الإجابة على الخط نفسه. هذا مناسب تمامًا لبحث عن الطقس. أما لخط أنابيب CI أو استيراد دفعات كبيرة أو تدريب نموذج، فهو خيار سيئ.
يحل المطعم المشكلة بطريقة مختلفة. لا أحد يقف عند نافذة المطبخ يحدّق في الطاهي. يثبت النادل تذكرة ورقية على القضيب، والتذكرة هي مقبض الطلب. تمنح المهام MCP قضيب التذاكر نفسه.
مشكلة المهلة الزمنية
يفرض كثير من العملاء ووسطاء النقل مهلًا زمنية تجعل إبقاء الطلب مفتوحًا غير عملي لأكثر من بضع ثوانٍ. موازنات الأحمال والبوابات المؤسسية والبوابات بلا خادم كلها تقطع الاتصالات الصامتة. وعندما يحدث ذلك، يرى المستدعي فشلًا رغم أن الخادم ما زال مشغولًا، ورد الفعل الطبيعي هو إعادة المحاولة وتشغيل المهمة المكلفة نفسها مرتين.
فقدان العمل بعد انقطاع الاتصال
يربط الاستدعاء المحجوب النتيجة بالاتصال. يُغلق غطاء الحاسوب المحمول، أو يغادر عميل الهاتف شبكة الواي فاي، أو تُعاد تشغيل عملية المضيف، فلا يوجد مكان تصل إليه الإجابة. مع المهمة، يصبح المعرّف مقبضًا دائمًا: يعيد العميل الاتصال، ويستدعي tasks/get بالمعرّف نفسه، ويكمل من حيث توقف.
💡 قاعدة عامة: إذا كانت عملية تستغرق بانتظام أكثر من بضع ثوانٍ، أو تتوقف بانتظار قرار بشري، فيجب أن تُنفَّذ ضمن مهمة.
ما الذي يضيفه امتداد Tasks
من المواصفة الأساسية إلى الامتداد
بدأت المهام كميزة تجريبية في المواصفة الأساسية لبروتوكول MCP. ثم نقلها البروتوكول لاحقًا من النواة إلى امتداد اختياري يُعرَّف بـ io.modelcontextprotocol/tasks، وموثّق في SEP-2663. يبقي خروجها من النواة البروتوكول الأساسي خفيفًا، بينما تفعّل الخوادم والعملاء الذين يحتاجون إلى أعمال طويلة الأمد هذا الدعم عن قصد. تصف الوثائق الرسمية النتيجة بأنها تنفيذ غير متزامن للمهام لعمليات MCP طويلة الأمد، ويوجد نص المواصفة الكامل في مستودع ext-tasks.
ثمة تغيير يستحق الانتباه. تذكر أوصاف أقدم للميزة استدعاءً منفصلًا باسم tasks/result. في الامتداد، تصل المخرجات النهائية داخل استجابة tasks/get، وهذا يُبقي حلقة العميل عند طريقة استطلاع واحدة فقط.
معرّف الامتداد والاشتراك الاختياري
يُتفاوض على الدعم ولا يُفترض أبدًا:
يُدرج العميلio.modelcontextprotocol/tasks في قدرات كل طلب، داخل _meta تحت io.modelcontextprotocol/clientCapabilities.
يعلن الخادم الامتداد نفسه في القدرات التي يعيدها للعملاء.
إذا كان الخادم يشترط دعم المهام والعميل لم يُعلن ذلك، يرد الخادم برمز الخطأ -32003 مع الرسالة Missing required client capability.
قرار متى تُنشأ المهمة يعود إلى الخادم. لا توجد علامة لكل أداة على جانب العميل. يشترك العميل مرة واحدة، ويجب أن يكون مستعدًا لشكلين من النتائج: النتيجة العادية، أو مقبض مهمة. وحتى الآن، tools/call هو نوع الطلب الوحيد القادر على إنتاج مهمة.
الطرف
ما يجب أن يفعله
لماذا يهم
العميل
يعلن الامتداد، ويتعامل مع شكلين من النتائج
لا يعيد الخادم مهمة إلى عميل لم يشترك
الخادم
يعلن الامتداد، وينشئ المهمة قبل الرد
لا يمكن لعطل يحدث مباشرة بعد الرد أن يترك المعرّف يتيمًا
الطرفان
يعاملان taskId كالمقبض الوحيد
تصبح عمليات إعادة الاتصال وإعادة التشغيل غير ضارة
دورة حياة المهمة
الحصول على المقبض والاستطلاع
يتكون التدفق من خمس خطوات:
يرسل العميل tools/call مع إرفاق قدرة المهام.
يقرر الخادم أن العمل طويل، ويعيد CreateTaskResult موسومًا بـ resultType: "task".
تُنشأ المهمة بشكل دائم قبل أن يغادر ذلك الرد الخادم.
يستدعي العميل tasks/get مع taskId، وينتظر pollIntervalMs على الأقل بين الاستدعاءات.
تحمل كل استجابة الحالة الحالية، وعندما تصل المهمة إلى حالة نهائية، فتعرض النتيجة أو الخطأ.
إليك عرض مبسّط لمهمة جديدة. الغلاف الدقيق معرّف في المواصفة، فاعتبر هذا توضيحًا للحقول:
يحتاج الخادم إلى إدخال من العميل، راجع inputRequests
لا
completed
انتهت العملية، والحقل result يحمل المخرجات
نعم
failed
حدث خطأ JSON-RPC، والحقل error يحمل التفاصيل
نعم
cancelled
توقفت بناءً على طلب، وإن لم يُحترم ذلك دائمًا
نعم
بمجرد أن تصل المهمة إلى حالة نهائية، لا تتغير حالتها أبدًا. إن tasks/get عملية idempotent، أي أن الاستطلاع عشر مرات آمن تمامًا كالاستطلاع مرة واحدة.
الإيقاف مؤقتًا لإدخال بشري
تصل بعض الأعمال إلى نقطة قرار في منتصفها: الموافقة على نشر، أو تأكيد عملية شراء، أو اختيار أحد ثلاثة خيارات. تنتقل المهمة إلى input_required، وتتضمن استجابة tasks/get التالية خريطة inputRequests تحمل طلبات الاستيضاح (elicitations) أو طلبات أخرى من الخادم.
يعرض العميل تلك الطلبات على مستخدم أو نموذج، ثم يجيب عنها عبر tasks/update، مرسلًا inputResponses تطابق الطلبات المعلقة. يقرّ الخادم بذلك بنتيجة فارغة، ويتجاهل الردود الخاصة بمدخلات مجهولة أو مُلبّاة سابقًا. وبمجرد أن يحصل كل طلب على إجابة، يواصل الخادم عمله.
💡 لماذا هذا لطيف: لا يوجد اتصال ثانٍ ولا رسالة غير مطلوبة من الخادم إلى العميل. تمرّ الخطوة البشرية عبر حلقة الاستطلاع نفسها التي تمر بها كل الخطوات الأخرى.
الإنهاء والفشل والإلغاء
عندما تنتهي المهمة بنجاح، يحتوي الحقل result على ما كان الطلب الأصلي سيعيده بشكل متزامن. وبالنسبة لاستدعاء أداة، يعني ذلك كتل المحتوى نفسها التي كان سينتجها الاستدعاء المحجوب. وعندما تكون الحالة failed، يحمل الحقل error خطأ JSON-RPC.
يُستخدم tasks/cancel للإلغاء. يقرّ الخادم بالطلب بنتيجة فارغة، لكن الإلغاء تعاوني. ربما يكون العمل قد تجاوز نقطة اللاعودة، لذلك قد تنتهي المهمة إلى حالة نهائية مختلفة.
يمكن للخوادم أيضًا دفع التحديثات عبر notifications/tasks. يشترك العملاء عبر subscriptions/listen، وكل إشعار يحمل حالة المهمة الكاملة، بالشكل نفسه الذي تعيده استجابة tasks/get.
بناء خادم مهام
أداة FastMCP بسيطة
أضاف FastMCP 4.0 دعمًا للامتداد. تثبّت fastmcp-tasks، وتسجّل TasksExtension، وتعلّم الأداة بأنها قادرة على تشغيل المهام:
import asyncio
from fastmcp import FastMCP
from fastmcp_tasks import TasksExtension
mcp = FastMCP("ReportServer")
mcp.add_extension(TasksExtension())
@mcp.tool(task=True)
async def slow_computation(duration: int) -> str:
"""A long-running operation."""
for i in range(duration):
await asyncio.sleep(1)
return f"Finished in {duration} seconds"
هناك تفصيلان مهمان هنا. تتطلب المهام الخلفية دوال غير متزامنة، واستخدام task=True على دالة متزامنة يرفع ValueError وقت التسجيل. وtask=True يشير فقط إلى أن الأداة قادرة على العمل في الخلفية. ويتوقف ما إذا كانت تعمل فعلًا على اشتراك العميل وعلى وضع التنفيذ في الخادم. تشير الوثائق إلى أن Docket يشغّل المجدول الموزّع، وهذا ما يجعل الإعداد جاهزًا للإنتاج.
التقدم وأوضاع التنفيذ
تبلغ الأدوات عن التقدم عبر اعتماد Progress مُحقن، ومنه تأتي statusMessage التي يعرضها عملاؤك:
للتحكم الأدق، استبدل القيمة المنطقية بـ TaskConfig. ثلاثة أوضاع تحدد سلوك الأداة:
الوضع
السلوك
optional
يعمل بشكل متزامن للعملاء القدامى، وفي الخلفية للعملاء القادرين على المهام
required
يرفع خطأً إذا كان العميل لا يدعم المهام، ويعمل في الخلفية غير ذلك
forbidden
متزامن دائمًا، ولا يعمل في الخلفية أبدًا
الاختصارات تتطابق بوضوح: task=True يعني optional، وtask=False يعني forbidden. ويمكنك أيضًا اقتراح وتيرة استطلاع عبر poll_interval=timedelta(seconds=2).
أنماط عملاء تصمد
استطلع بلباقة، واحفظ كل شيء
يحتاج العميل الذي يتعامل مع خوادم قادرة على المهام إلى خمس عادات:
أعلن الامتداد في قدرات كل طلب.
تعامل مع النتائج المتعددة الأشكال، لأن tools/call قد يعيد نتيجة عادية أو مهمة.
احترم pollIntervalMs، لأن الخادم قد يغيّره بين الاستجابات.
أجب عن inputRequests عبر tasks/update بدل تجاهلها.
خزّن معرّفات المهام بشكل دائم حتى يستأنف الاستطلاع بعد عطل أو إعادة تشغيل.
async def run_tool(session, name, args):
reply = await session.call_tool(name, args)
if reply.get("resultType") != "task":
return reply # ordinary synchronous result
task = reply
store.save(task["taskId"]) # survive a crash
while task["status"] in ("working", "input_required"):
if task["status"] == "input_required":
answers = await ask_user(task["inputRequests"])
await session.request("tasks/update", {
"taskId": task["taskId"],
"inputResponses": answers,
})
await asyncio.sleep(task["pollIntervalMs"] / 1000)
task = await session.request("tasks/get", {"taskId": task["taskId"]})
if task["status"] == "failed":
raise RuntimeError(task["error"])
return task.get("result")
الراكب في القطار هو النموذج الذهني هنا. ينقطع الاتصال في كل نفق، ومع ذلك تبقى التذكرة في جيبه صالحة. العميل المبني بهذه الطريقة يعيد الاتصال بعد النفق ويواصل.
الإشعارات بدل الاستطلاع
الاستطلاع هو الافتراضي، وهو يعمل في كل مكان. إذا كان الخادم يدعم notifications/tasks، يمكن للعميل أن يشترك مرة واحدة ويتخطى معظم رحلات tasks/get الذهابية والإيابية، لأن كل إشعار يحمل حالة المهمة الكاملة. أبقِ الاستطلاع احتياطيًا للخوادم التي لا تدفع التحديثات.
أخطاء يجب تجنبها
تتصرف مقابض المهام مثل الطرود في غرفة خلفية لمكتب بريد. إذا تُركت طويلًا، تُفرَّغ. هذه أكثر الفخاخ شيوعًا:
الخطأ
ما الذي يحدث
الإصلاح
تجاهل ttlMs
تنتهي صلاحية المهمة قبل أن يقرأ عميل بطيء النتيجة
اقرأ النتائج بسرعة، واضبط TTL للخادم بما يناسب سلوك العميل الفعلي
الاستطلاع أسرع من pollIntervalMs
طلبات مهدورة وحمل يمكن تجنبه
انتظر الفترة المقترحة
اعتبار الإلغاء فوريًا
تعرض الواجهة أن العمل توقف بينما يستمر في التشغيل
انتظر حالة نهائية قبل الإبلاغ عنها
إعادة مهمة إلى عميل لم يشترك أبدًا
لا يستطيع العميل قراءة الرد
تحقق من القدرات المعلنة أولًا
تغليف كل أداة في مهمة
الاستدعاءات السريعة تكتسب زمن استجابة بلا سبب
دع العمليات السريعة تعيد نتائجها بشكل عادي
مشاركة معرّفات المهام بتساهل
قد يقرأ مستدعٍ آخر مخرجات شخص غيره
تعامل مع المعرّف كمقبض واربطه بالمستدعي الموثّق (ممارسة جيدة، تتجاوز ما تذكره المواصفة)
القيمة null في ttlMs تعني غير محدود، وهذا يبدو ودودًا حتى يمتلئ التخزين بمهام منتهية لا يجمعها أحد.
ربط المهام مع PicassoIA
توليد الوسائط هو المثال الكلاسيكي للعمل الطويل، ولهذا تبدو أنماط المهام طبيعية بجانب أدوات الصور والفيديو. يناسب نموذجان من PicassoIA سير عمل المهام مباشرة.
يتعامل Claude Sonnet 5 مع البرمجة متعددة الخطوات وأعمال استخدام الأدوات، لذلك فهو شريك برمجة جيد لشيفرة المعالج في هذا المقال. من الخيارات الأخرى في الفئة نفسها GPT 5.6 Sol، إذا أردت رأيًا ثانيًا في الشيفرة نفسها.
توثيق تدفقات المهام يحتاج عادةً إلى صور نظيفة: صورة جهاز لبطاقة حالة، أو شعار لشريحة معمارية. تعيد Remove Background ملف PNG شفافًا خلال ثوانٍ، وإعداد Preserve Partial Alpha فيها يحافظ على حواف ناعمة طبيعية. أوقفه عندما تريد حوافًا حادة وغير شفافة تمامًا في صور المنتجات.
أما لمشاهد جديدة، فإن Flux 2 Pro وP Image يحوّلان الأمر النصي المكتوب إلى صورة تناسب ترويسة مدونة.
أنشئ صورك الخاصة بعد ذلك
لديك الآن تصوّر كامل للأمر: مقبض بدل اتصال محجوب، وخمس حالات، وثلاث طرق، وقائمة قصيرة من العادات تحفظ الأعمال الطويلة من الضياع. أفضل طريقة لترسيخ ذلك هي بناء شيء صغير. اكتب أداة تستغرق عشر ثوانٍ، وعلّمها بـ task=True، وراقب الحالة تنتقل من working إلى حالة نهائية.
ثم امنح مشروعك وجهًا. افتح Picasso IA، واختر نموذجًا لتحويل النص إلى صورة، وولّد صورة ترويسة لكتابتك. جرّب زوايا الكاميرا والإضاءة وتفاصيل العدسة في أوامرك النصية، وأزل خلفية لشعار نظيف، ولاحظ مدى سرعة تحوّل الفكرة إلى مرئية مكتملة. نتيجة مهمتك التالية تستحق صورة تستحق المشاركة.