إضافة MCP Tasks: خادم Python يعمل فعلياً (2026)
٢٢ سبتمبر ٢٠٢٦

ملخص: تمنح إضافة io.modelcontextprotocol/tasks المعتمدة (SEP-2663) خوادم MCP طريقة لإرجاع مقبض مهمة (task handle) مستدام بدلاً من الانتظار عند استدعاء أداة بطيئة. وبدءاً من إصدار mcp 2.2.0 (الذي صدر في 7 سبتمبر 2026)، لا تزال Python SDK الرسمية لا تدعمها — حيث قامت بحذف النسخة التجريبية القديمة ولم تترك شيئاً مكانها.
حزمة PyPI الوحيدة التي تدعم المواصفات المعتمدة، وهي fastmcp-tasks، ليست مبنية على SDK الرسمية على الإطلاق. يقوم هذا المنشور ببناء تنفيذ يعمل فوق MCPServer باستخدام إطار عمل الإضافات الخاص بـ SDK، وتشغيله من البداية إلى النهاية، واكتشاف خطأين حقيقيين أثناء العملية.
ما ستتعلمه
- لماذا لا يحتوي
mcp2.2.0 على تنفيذ للمهام (Tasks)، وما الذي تم حذفه للوصول إلى ذلك - لماذا يوجد
mcp.types.CreateTaskResultفي SDK ولكنها لن تتوافق مع عميل يتبع المواصفات المعتمدة - كيف تختلف
fastmcp-tasks، الحزمة الوحيدة التي تدعم SEP-2663، عن SDK الرسمية - كيفية بناء الإضافة بنفسك باستخدام إطار عمل
Extensionفي SDK v2 - لماذا لا يستطيع مساعد
call_tool()الخاص بالعميل تحليل استجابة المهمة من خادمك - خطأ حقيقي في الإلغاء التعاوني نتج عن هذا البناء، وكيفية إصلاحه في سطر واحد
الـ SDK الرسمية انتقلت من "تجريبية" إلى "لا شيء"
أحدث مراجعة لمواصفات MCP في 28 يوليو 2026 غيرت الكثير من الأمور دفعة واحدة: فقد ألغت مصافحة الاتصال (connection handshake)، وأزالت كل الطلبات التي يبدأها الخادم، ونقلت تصميم المهام القديم وغير الرسمي خارج المواصفات الأساسية تماماً — لتصبح إضافة مستقلة، io.modelcontextprotocol/tasks__PRESER preserve__، والتي تم اقتراحها كـ SEP-2663 في 27 أبريل 2026 واعتمدت بشكل منفصل في مسار الإضافات الخاص بها قبل أن تصدر مع مراجعة 28 يوليو.1
إصدار v2 من Python SDK (mcp 2.0.0 وما بعده) يطبق مراجعة 28 يوليو تلك. وسجل التغييرات الخاص بها صريح بشأن ما حدث للمهام خلال هذه العملية: تم حذف API المهام القديمة mcp.*.experimental تماماً، ولا تقوم SDK بتنفيذ البديل الخاص بها.2
لقد قمت بتثبيت mcp 2.2.0 حديثاً من PyPI (الإصدار الحالي وقت كتابة هذا المقال، والصادر في 7 سبتمبر 2026) للتأكد من ذلك بنفسي.3 قائمة الوحدات الفرعية لـ mcp.server لا تحتوي على وحدة tasks على الإطلاق:
['__main__', '_otel', '_streamable_http_modern', 'apps', 'auth', 'caching',
'connection', 'context', 'elicitation', 'extension', 'fastmcp', 'lowlevel',
'mcpserver', 'models', 'request_state', 'runner', 'session', 'sse', 'stdio',
'streamable_http', 'streamable_http_manager', 'subscriptions',
'transport_security', 'validation']
لا يوجد معالج من جهة الخادم، ولا نوع نتيجة من جهة العميل، لا شيء.
الفخ: mcp.types.CreateTaskResult لا يزال موجوداً
إليك الجزء الذي لم يذكر في سجل التغييرات. لا تزال mcp.types — حزمة أنواع النقل في SDK — توفر مجموعة كاملة من نماذج Pydantic الخاصة بالمهام: CreateTaskResult، و GetTaskRequest، و GetTaskResult، و CancelTaskRequest، و TaskStatusNotification، وحتى ListTasksRequest.
استورد أي منها وستعمل. لا يوجد شيء يحذرك من أنها ذات شكل خاطئ.
يوضح التعليق الموجود في مصدر الحزمة السبب، مباشرة فوق تعريفات الفئات:
"المهام: تم تقديمها في 2025-11-25، وتمت إزالتها من المواصفات الأساسية في 2026-07-28 (تستمر كإضافة). معرفة هنا كأنواع فقط؛ طرقها ليست في اتحادات الطلبات/الإشعارات أدناه، لذا لا يتم إرسالها أبداً."
هذا هو التصميم التجريبي المهجور من 2025-11-25، والذي تم الاحتفاظ به فقط كتعريفات أنواع خاملة. وهي لا تتطابق مع الإضافة المعتمدة، في خمس طرق ملموسة تحققت منها حقلاً بحقل:
| المواصفات المعتمدة (SEP-2663) | نماذج mcp.types المتبقية | |
|---|---|---|
| مميز النتيجة (Result discriminator) | resultType: "task" في كل نتيجة مهمة | لا يوجد حقل result_type على الإطلاق |
| شكل المهمة (Task shape) | مسطح — taskId و status و ttlMs توجد مباشرة في النتيجة | متداخل — CreateTaskResult.task يغلف كائن Task منفصل |
| حقل الاحتفاظ (Retention field) | ttlMs (ميلي ثانية، camelCase) | ttl |
tasks/list | غير موجود صراحةً — تشير المواصفات إلى أن هذا إصلاح أمني متعمد مقارنة بتصميم 2025-11-25، حيث أن القائمة ذات النطاق الضعيف قد تسرب معرفات مهام أحد المتصلين إلى آخر | ListTasksRequest / TasksListCapability لا يزالان معرفين بالكامل |
| تسليم النتيجة (Result delivery) | مدمجة في GetTaskResult.result | طلب tasks/result منفصل (GetTaskPayloadRequest) |
إذا قمت ببناء خادم باستخدام mcp.types.CreateTaskResult اليوم، فستحصل على شيء لا يتواصل مع أي عميل يتبع المواصفات المعتمدة في أي مكان، لأن هذا النوع من جانب العميل غير موجود في هذه الـ SDK أيضاً.
من الذي يقدم SEP-2663 فعلياً في Python
هناك حزمة PyPI واحدة تفعل ذلك: fastmcp-tasks، في الإصدار 4.0.5 اعتباراً من 17 سبتمبر 2026 — قبل خمسة أيام من هذا المنشور، وأقل من ثلاثة أسابيع بعد إصدارها المستقر 4.0.0 في 31 أغسطس.4 وصفها الخاص واضح ولا لبس فيه: "تنفيذ المهام في الخلفية لخوادم FastMCP عبر امتداد io.modelcontextprotocol/tasks (SEP-2663)."
التفصيل الجدير بالذكر: أنها ليست مبنية على حزمة mcp الرسمية. ومن بين تبعاتها الأساسية fastmcp-slim، والمثبتة على نفس الإصدار بالضبط (4.0.5).5 fastmcp-slim، وإلى جانبها fastmcp-tasks، يتواجدان في نفس المستودع الموحد (monorepo) الخاص بـ FastMCP نفسه — الذي أنشأه في الأصل Jeremiah Lowin، وتديره الآن Prefect تحت منظمة PrefectHQ GitHub.6 إنه مشروع حقيقي ومدار رسمياً، لكنه ليس الـ MCP SDK الرسمي.
هذا المشروع له علاقة حقيقية ولكن يسهل الخلط بينها وبين modelcontextprotocol/python-sdk. لقد تم دمج تصميم الخادم عالي المستوى لـ FastMCP 1.0 في الـ SDK الرسمي في عام 2024، ولا يزال موجوداً هناك اليوم تحت المسمى الذي غيره الإصدار v2 من FastMCP إلى MCPServer.
أما FastMCP 2.0 فهو قاعدة كود منفصلة، لا تزال تُطور بشكل مستقل من قبل نفس الفريق، وهو ما يعمل عليه fastmcp-slim (وبالتالي fastmcp-tasks) فعلياً — وليس mcp.server.mcpserver.MCPServer. مشروعان مختلفان يتشاركان في الاسم وسلف مشترك؛ واحد منهما فقط يقدم امتداد Tasks المعتمد، وهو المشروع الموجود خارج modelcontextprotocol/python-sdk.
بناؤها بنفسك على MCPServer
توفر لك الـ SDK v2 ما تحتاجه لبناء هذا: إطار عمل Extension من الدرجة الأولى، تمت إضافته في نفس إصدار إعادة كتابة البروتوكول (SEP-2133).2 يمكن للامتداد تسجيل طرق طلب جديدة واعتراض tools/call — وهو بالضبط الشكل الذي يحتاجه SEP-2663.
كل ما يلي تم تشغيله مقابل تثبيت حقيقي لـ mcp 2.2.0 في بيئة افتراضية نظيفة — بدون محاكاة للنقل (mocked transport)، وبدون مخرجات تقديرية.
ابدأ بأنواع سجلات المهام. تقوم types.Result و types.RequestParams بتحويل سمات Python من snake_case إلى camelCase تلقائياً عند الإرسال، لذا تصبح task_id هي taskId دون أي تعيين يدوي:
import mcp.types as types
from mcp import MCPError
class TaskGetParams(types.RequestParams):
task_id: str
class TaskResult(types.Result):
result_type: str = "complete"
task_id: str
status: str # working | input_required | completed | cancelled | failed
created_at: str
last_updated_at: str
ttl_ms: int | None = None
poll_interval_ms: int | None = None
result: dict | None = None
error: dict | None = None
class TaskAck(types.Result):
result_type: str = "complete"
قم بتسجيل الأفعال الثلاثة باستخدام MethodBinding، واربط إنشاء المهام بـ intercept_tool_call:
from mcp.server.extension import Extension, MethodBinding
from mcp.server.context import CallNext, HandlerResult, ServerRequestContext
_TASKS: dict[str, dict] = {}
class TasksExtension(Extension):
identifier = "io.modelcontextprotocol/tasks"
TASK_TOOLS = {"slow_report"}
def methods(self):
return [
MethodBinding("tasks/get", TaskGetParams, _get_task),
MethodBinding("tasks/cancel", TaskCancelParams, _cancel_task),
MethodBinding("tasks/update", TaskUpdateParams, _update_task),
]
async def intercept_tool_call(self, params, ctx, call_next) -> HandlerResult:
if params.name not in self.TASK_TOOLS:
return await call_next(ctx)
task_id = str(uuid.uuid4())
now = _now_iso()
_TASKS[task_id] = {
"task_id": task_id, "status": "working",
"created_at": now, "last_updated_at": now,
"ttl_ms": 300_000, "poll_interval_ms": 500,
"result": None, "error": None,
}
asyncio.create_task(self._run(task_id, ctx, call_next))
# Raw dict, matching the spec's flat CreateTaskResult shape exactly.
return {
"resultType": "task", "taskId": task_id, "status": "working",
"createdAt": now, "lastUpdatedAt": now,
"ttlMs": 300_000, "pollIntervalMs": 500,
}
تقوم _run بالعمل الفعلي في الخلفية، بعد أن تكون intercept_tool_call قد أعادت بالفعل مقبض المهمة (task handle) إلى العميل:
async def _run(self, task_id, ctx, call_next) -> None:
record = _TASKS[task_id]
try:
await asyncio.sleep(2) # stand-in for real slow work
result = await call_next(ctx)
if record["status"] == "cancelled":
return # see "the cancellation bug" below
record["status"] = "completed"
record["result"] = result.model_dump(by_alias=True, exclude_none=True)
except Exception as exc:
if record["status"] != "cancelled":
record["status"] = "failed"
record["error"] = {"code": -32603, "message": str(exc)}
record["last_updated_at"] = _now_iso()
تعتبر tasks/get و tasks/cancel و tasks/update معالجات عادية تقرأ وتكتب في نفس القاموس (dict) — حيث تطلق tasks/get خطأ MCPError(types.INVALID_PARAMS, ...) في حالة وجود task_id غير معروف، وهو ما يطابق الرمز -32602 المطلوب في المواصفات.1
العميل لا يمكنه استدعاء الخادم الخاص به
هذه هي النتيجة التي لم أتوقعها عند البدء. تقوم Client.call_tool() — وهي طريقة تسهيل عالية المستوى خاصة بـ SDK — بالتحقق من كل استجابة مقابل اتحاد (union) محدد مسبقاً من CallToolResult و InputRequiredResult. ولا يوجد بها أي مسار لـ resultType: "task"، لأن SDK لا تعلم بوجود هذه الإضافة.
عند استدعاء client.call_tool("slow_report", {...}) ضد الخادم الخاص بي، مع تفعيل الإضافة بالكامل من جهة الخادم، ظهر هذا الخطأ فوراً:
pydantic_core._pydantic_core.ValidationError: 2 validation errors for
union[CallToolResult,function-after[_require_one_field(), InputRequiredResult]]
CallToolResult.content
Field required [type=missing, input_value={'resultType': 'task', ...
function-after[_require_one_field(), InputRequiredResult].resultType
Input should be 'input_required' [type=literal_error, input_value='task', ...]
استجابة خادم متوافقة تماماً مع المواصفات، ولكن تم رفضها بواسطة طريقة التسهيل الخاصة بالعميل الرسمي. الحل هو التخلي عن طبقة واحدة، واستخدام client.session.send_request() مع تحديد نوع النتيجة الخاص بك:
from typing import Literal, Union
from pydantic import TypeAdapter
class CreateTaskResult(types.Result):
result_type: Literal["task"] = "task"
task_id: str
status: Literal["working"]
created_at: str
last_updated_at: str
ttl_ms: int | None = None
poll_interval_ms: int | None = None
CallToolOrTask = TypeAdapter(Union[types.CallToolResult, CreateTaskResult])
call_req = types.CallToolRequest(
params=types.CallToolRequestParams(name="slow_report", arguments={"topic": "Q3 agent adoption"})
)
created = await client.session.send_request(call_req, CallToolOrTask)
ينجح هذا الاستدعاء ويعيد CreateTaskResult مع task_id حقيقي. وتتبع عملية الاستطلاع (Polling) نفس النمط ضد tasks/get، باستخدام فئة فرعية من types.Request مع name_param = "taskId".
يخبر هذا الحقل جلسة عميل SDK بمحاكاة taskId في ترويسة Mcp-Name على Streamable HTTP، وهو ما تتطلبه المواصفات حتى يتمكن موازن التحميل (load balancer) من توجيه استدعاءات المتابعة للمهمة مرة أخرى إلى العامل (worker) الذي يحتفظ بحالتها.1 إن وسيلة النقل في الذاكرة (in-memory transport) مثل تلك التي يختبرها هذا المنشور لا ترسل ترويسات HTTP أبداً، لذا فإن name_param غير فعال هنا — ولكنه ليس اختيارياً في حالة النشر الفعلي خلف أكثر من عملية خادم واحدة.
خطأ الإلغاء (Cancellation bug)
لم يكن الإصدار الأول من _run يحتوي على حماية لحالة الإلغاء. بدا الأمر صحيحاً: أنشأت مهمة، ثم ألغيتها، وأبلغت tasks/get فوراً أن status: "cancelled". لكنه لم يكن صحيحاً.
تقوم tasks/cancel فقط بـ إرسال إشارة إلغاء — والمواصفات صريحة في أن الخادم "غير ملزم بإيقاف العمل فعلياً" وأن الانتقال النهائي إلى حالة cancelled "غير مضمون".1 استمرت عملية asyncio.create_task في الخلفية في العمل بغض النظر عن ذلك. وعندما انتهى النوم المحاكي لمدة ثانيتين، كتبت status: "completed" مباشرة فوق حالة cancelled.
اكتشفت ذلك من خلال الاستطلاع مرة أخرى بعد الوقت الذي كان يفترض أن ينتهي فيه العمل، بدلاً من التحقق فقط بعد الإلغاء مباشرة:
status right after cancel: cancelled
status 2.5s after cancel: completed | result: {'content': [{'type': 'text', ...
أن تقوم مهمة ملغاة بإلغاء إلغاء نفسها بصمت هو أمر أسوأ من "قد لا يحترم الإلغاء" المذكور في المواصفات — إنه خطأ فعلي. الحل هو إضافة شرط حماية واحد: التحقق من أن record["status"] == "cancelled" قبل كتابة النتيجة النهائية، وتجاهل العمل الذي وصل متأخراً بدلاً من ذلك.
مخرجات حقيقية، تشغيل كامل
هذه هي مخرجات الطرفية (terminal) الفعلية من تشغيل python3 client.py ضد الخادم الذي تم إصلاحه — استدعاء الأداة، إنشاء المهمة، الاستطلاع، الإلغاء، وخطأ task_id غير معروف، كل ذلك في عملية واحدة:
server_capabilities.extensions: {'io.modelcontextprotocol/tasks': {}}
quick_echo -> [TextContent(type='text', text='echo: hi', annotations=None, meta=None)]
[server] created task c3e2abd5-d9ec-4447-a555-abc61448fd4b, scheduling background run
[server] task c3e2abd5-d9ec-4447-a555-abc61448fd4b starting slow work
slow_report raw result -> meta={'io.modelcontextprotocol/serverInfo': {'name': 'task-demo', 'version': ''}} result_type='task' task_id='c3e2abd5-d9ec-4447-a555-abc61448fd4b' status='working' created_at='2026-09-22T04:09:44Z' last_updated_at='2026-09-22T04:09:44Z' ttl_ms=300000 poll_interval_ms=500
task_id -> c3e2abd5-d9ec-4447-a555-abc61448fd4b
poll 0: status=working
poll 1: status=working
poll 2: status=working
poll 3: status=working
[server] task c3e2abd5-d9ec-4447-a555-abc61448fd4b completed
poll 4: status=completed
final result: {'content': [{'type': 'text', 'text': "Report on 'Q3 agent adoption': 3 findings, 0 blockers."}], 'structuredContent': {'result': "Report on 'Q3 agent adoption': 3 findings, 0 blockers."}, 'isError': False, 'resultType': 'complete'}
[server] created task 3ec43169-4b96-4150-82aa-b8c52b4a5bb8, scheduling background run
[server] task 3ec43169-4b96-4150-82aa-b8c52b4a5bb8 starting slow work
cancel ack -> meta={'io.modelcontextprotocol/serverInfo': {'name': 'task-demo', 'version': ''}} result_type='complete'
status after cancel -> cancelled
unknown task_id error -> Failed to retrieve task: Task not found
أربعة استطلاعات بفارق 500 مللي ثانية ضد مهمة تستغرق ثانيتين، ثم اكتمال نظيف، ثم إلغاء يظل ملغى، ثم الخطأ المطلوب في المواصفات لمعرف (ID) وهمي. هذا هو نص الـ stdout الحرفي، وليس نسخة مختصرة أو معاد بناؤها.
أين يترك هذا النظام البيئي (ecosystem)
إن Python SDK ليس الوحيد المتأخر. لقد تحققت من الحزمة المنشورة حالياً لـ TypeScript SDK خصيصاً لهذا المنشور: @modelcontextprotocol/sdk في الإصدار 1.30.0، ولا يزال ملف package.json الخاص بها يصدر نقطة دخول ./experimental/tasks — وهو الاسم الذي كان مستخدماً قبل الاعتماد الرسمي، وليس اسم الإضافة.7
بناؤنا العملي الخاص بناءً على ذلك الـ SDK، والذي نُشر قبل أربعة أيام من هذا المنشور، وجد فجوة ذات صلة في نفس الحزمة: فحص قدرة تسجيل المعالج (handler-registration) في الـ Server منخفض المستوى لا يحتوي على أي حالة لـ tasks/update على الإطلاق، لذا فإن تسجيل معالج لها يتم بصمت دون التقيد بإعلان قدرة tasks التي تتطلبها طرق المهام الثلاث الأخرى.8
لغتان، وفريقان مختلفان من مطوري الـ SDK، والنتيجة واحدة: إضافة للمواصفات تم اقتراحها في أبريل، واعتُمدت قبل إصدار المواصفات الأساسية في يوليو، ولا تزال غائبة عن كلا بيئتي التشغيل الرسميتين في أواخر سبتمبر.
هل يطبق io.modelcontextprotocol/tasks المعتمد؟ | مبني على | |
|---|---|---|
mcp (Python, official) 2.2.0 | لا — تمت إزالة النسخة التجريبية القديمة، ولم يتم استبدالها بشيء | — |
@modelcontextprotocol/sdk (TS, official) 1.30.0 | لا — لا تزال tasks تُشحن تحت /experimental | — |
@modelcontextprotocol/ext-tasks (TS) 0.1.0 | من جانب الطالب (Requester) فقط — لم يتم نشر أي مساعد مستقبل/خادم (receiver/server) بالشكل المعتمد8 | Official TS SDK v2 (@modelcontextprotocol/client) |
fastmcp-tasks (Python) 4.0.5 | نعم | fastmcp-slim (PrefectHQ FastMCP 2.0) — ليس الـ SDK الرسمي |
| إضافة هذا المنشور | نعم، للأفعال الأساسية الثلاثة | إطار عمل Extension الخاص بـ mcp 2.2.0 |
التحقق
كل عينة كود في هذا المنشور تم تشغيلها، دون تعديل، مقابل mcp 2.2.0 مثبت حديثاً من PyPI في بيئة Python 3.10 افتراضية نظيفة في 22 سبتمبر 2026. لم يتم استخدام أو الحاجة إلى مفتاح API من Anthropic أو OpenAI — لأن مقبض مهمة MCP هو حالة JSON-RPC، وليس استدعاءً لنموذج، لذا فإن العميل والخادم في هذا المنشور يتحدثان مع بعضهما البعض داخل العملية باستخدام mcp.Client(server_object)، دون لمس الشبكة أبداً.
تم التحقق من ادعاء "الأنواع المتبقية" عن طريق استيراد mcp.types.CreateTaskResult وأقرانها مباشرة وقراءة model_fields، ثم قراءة تعليق المصدر الموجود فوق تعريفاتها في الحزمة المثبتة.
تم التحقق من ادعاء "من الذي يشحنها" باستخدام pip install fastmcp-tasks و pip show لكل من الحزمة وتبعيّتها fastmcp-slim. كما تم التحقق من مقارنة TypeScript حديثاً مقابل إدخال سجل npm الحالي لـ @modelcontextprotocol/sdk، ولم يتم نقلها من منشور سابق.
الخلاصة
تم اقتراح SEP-2663 في أبريل 2026، وصدر مراجعة المواصفات التي شملته منذ شهرين تقريباً. لا يوجد SDK رسمي يطبق هذا الملحق بعد، ولا تزال حزمة wire-types الخاصة بـ Python SDK تعرض نوعاً يبدو متوافقاً من حيث الشكل ولكنه غير متوافق وظيفياً، وهو متبقٍ من التصميم الذي تم استبداله.
الحزمة الوحيدة التي تطبقه تأتي من خارج الـ SDK الرسمي تماماً. إطار عمل ملحقات v2 تعبيري بما يكفي لسد هذه الفجوة — الخادم والعميل في هذا المنشور يعملان معاً في أقل من 300 سطر — ولكن لا يوجد شيء يقدم ذلك لك جاهزاً اليوم.
قراءات ذات صلة: ملحق MCP Tasks: خادم TypeScript يعمل يغطي نفس الملحق مقابل TypeScript SDK الرسمي. مواصفات MCP بتاريخ 28 يوليو 2026 تغطي بقية التغييرات في نفس المراجعة. بناء خادم MCP للإنتاج باستخدام TypeScript يغطي OAuth و Streamable HTTP لخادم يتجه إلى مرحلة الإنتاج.
Footnotes
-
Tasks — MCP Tasks Extension specification, 2026-07-28, fetched 2026-09-22 (content confirmed identical to the living Draft revision as of this date). ↩ ↩2 ↩3 ↩4
-
What's new in v2 — MCP Python SDK, fetched 2026-09-22. ↩ ↩2
-
mcp 2.2.0 — PyPI, release metadata fetched 2026-09-22 (uploaded 2026-09-07). ↩
-
fastmcp-tasks — PyPI, release metadata fetched 2026-09-22 (4.0.5 uploaded 2026-09-17). ↩
-
fastmcp-slim — PyPI, fetched 2026-09-22. ↩
-
PrefectHQ/fastmcp — GitHub, fetched 2026-09-22 (27.9k stars; the repository jlowin/fastmcp now redirects here). ↩
-
@modelcontextprotocol/sdk — npm registry, fetched 2026-09-22 (1.30.0). ↩
-
MCP Tasks Extension: A Working TypeScript Server, NerdLevelTech, 2026-09-18. ↩ ↩2
-
SEP-2663: Tasks Extension — Model Context Protocol, fetched 2026-09-22. ↩



