تخطَّ إلى المحتوى

بروتوكول MCP وآليات النقل الداخلية

يصف هذا المستند كيفية تنفيذ coding-agent لرسائل MCP JSON-RPC وكيفية فصل اهتمامات البروتوكول عن اهتمامات طبقة النقل.

يغطي:

  • تدفق طلبات/استجابات JSON-RPC والإشعارات
  • ربط الطلبات ودورة حياتها لطبقات نقل stdio وHTTP/SSE
  • سلوك المهلة الزمنية والإلغاء
  • نشر الأخطاء ومعالجة الحمولات غير الصالحة
  • حدود اختيار طبقة النقل (stdio مقابل http/sse)
  • مسؤوليات إعادة الاتصال/المحاولة التي تقع على مستوى طبقة النقل مقابل مستوى المدير

لا يغطي تجربة تأليف الإضافات أو واجهة الأوامر.

طبقة البروتوكول (JSON-RPC + أساليب MCP)

Section titled “طبقة البروتوكول (JSON-RPC + أساليب MCP)”
  • أشكال الرسائل معرّفة في types.ts (JsonRpcRequest، JsonRpcNotification، JsonRpcResponse، JsonRpcMessage).
  • منطق عميل MCP (client.ts) يحدد ترتيب الأساليب ومصافحة الجلسة:
    1. طلب initialize
    2. إشعار notifications/initialized
    3. استدعاءات الأساليب مثل tools/list، tools/call

MCPTransport تجرّد التسليم ودورة الحياة:

  • request(method, params, options?) -> Promise<T>
  • notify(method, params?) -> Promise<void>
  • close()
  • connected
  • دوال رد الاتصال الاختيارية: onClose، onError، onNotification

تنفيذات طبقة النقل تملك تفاصيل التأطير والإدخال/الإخراج:

  • StdioTransport: JSON محدد بأسطر جديدة عبر stdio للعملية الفرعية
  • HttpTransport: JSON-RPC عبر HTTP POST، مع استجابات/استماع SSE اختيارية

دوال رد الاتصال لطبقة النقل (onClose، onError، onNotification) منفّذة، لكن تدفقات MCPClient/MCPManager الحالية لا تربط منطق إعادة الاتصال بهذه الدوال. الإشعارات تُستهلك فقط إذا سجّل المُستدعي معالِجات.

client.ts:createTransport() تختار طبقة النقل من التكوين:

  • type محذوف أو "stdio" -> createStdioTransport
  • "http" أو "sse" -> createHttpTransport

"sse" تُعامل كمتغير لطبقة نقل HTTP (نفس الفئة)، وليست تنفيذ طبقة نقل منفصل.

كل طبقة نقل تولّد معرّفات لكل طلب (سلسلة Math.random + طابع زمني). المعرّفات هي رموز ربط محلية لطبقة النقل.

  • الطلب الصادر يُسلسَل ككائن JSON واحد + \n.
  • #pendingRequests: Map<id, {resolve,reject}> يخزّن الطلبات قيد التنفيذ.
  • حلقة القراءة تحلل JSONL من stdout وتستدعي #handleMessage.
  • إذا كانت الرسالة الواردة تحمل id مطابقاً، يُحلّ الطلب أو يُرفض.
  • إذا كانت الرسالة الواردة تحمل method بدون id، تُعامل كإشعار وتُرسل إلى onNotification.

المعرّفات غير المعروفة تُتجاهل (لا رفض، لا دالة رد خطأ).

  • الطلب الصادر هو HTTP POST بجسم JSON ومعرّف مولّد.
  • مسار الاستجابة غير SSE: تحليل استجابة JSON-RPC واحدة وإرجاع result/طرح عند error.
  • مسار استجابة SSE (Content-Type: text/event-stream): بث الأحداث، إرجاع أول رسالة يتطابق id الخاص بها مع معرّف الطلب المتوقع وتحتوي على result أو error.
  • رسائل SSE التي تحمل method بدون id تُعامل كإشعارات.

إذا انتهى بث SSE قبل الاستجابة المطابقة، يفشل الطلب برسالة No response received for request ID ....

العميل يُرسل إشعارات JSON-RPC عبر transport.notify(...).

  • Stdio: يكتب إطار الإشعار إلى stdin (jsonrpc، method، params اختيارية) مع سطر جديد.
  • HTTP: يرسل جسم POST بدون id؛ النجاح يقبل 2xx أو 202 Accepted.

الإشعارات المُبادرة من الخادم تُعرض فقط عبر onNotification لطبقة النقل؛ لا يوجد مشترك عام افتراضي في المدير/العميل.

آليات طبقة نقل Stdio الداخلية

Section titled “آليات طبقة نقل Stdio الداخلية”

دورة الحياة وانتقالات الحالة

Section titled “دورة الحياة وانتقالات الحالة”
  • الحالة الأولية: connected=false، process=null، خريطة الانتظار فارغة
  • connect():
    • إنشاء عملية فرعية بالأمر/المعاملات/المتغيرات/المسار المُكوّن
    • تعليم الاتصال
    • بدء حلقة قراءة stdout (readJsonl)
    • بدء حلقة stderr (قراءة/تجاهل؛ صامتة حالياً)
  • close():
    • تعليم قطع الاتصال
    • رفض جميع الطلبات المعلقة (Transport closed)
    • إنهاء العملية الفرعية
    • انتظار إيقاف حلقة القراءة
    • إطلاق onClose

إذا خرجت حلقة القراءة بشكل غير متوقع، يُفعّل finally دالة #handleClose() التي تؤدي نفس رفض الطلبات المعلقة ودالة رد الإغلاق.

المهلة الزمنية والإلغاء

Section titled “المهلة الزمنية والإلغاء”

لكل طلب:

  • المهلة الافتراضية config.timeout ?? 30000
  • AbortSignal اختياري من المُستدعي
  • كل من الإلغاء والمهلة يرفضان الوعد المعلق وينظفان إدخال الخريطة

الإلغاء محلي فقط: طبقة النقل لا ترسل إشعار إلغاء على مستوى البروتوكول إلى الخادم.

معالجة الحمولات غير الصالحة

Section titled “معالجة الحمولات غير الصالحة”

في حلقة القراءة:

  • كل سطر JSONL محلل يُمرر إلى #handleMessage في try/catch
  • استثناءات معالجة الرسائل غير الصالحة/غير الصحيحة تُسقط (تعليق Skip malformed lines)
  • الحلقة تستمر، لذا رسالة سيئة واحدة لا تقتل الاتصال

إذا طرح محلل التدفق الأساسي استثناءً، يُستدعى onError (عندما يكون الاتصال قائماً)، ثم يُغلق الاتصال.

عند خروج العملية أو إغلاق التدفق:

  • جميع الطلبات قيد التنفيذ تُرفض برسالة Transport closed
  • لا إعادة تشغيل أو إعادة اتصال تلقائية
  • الطبقات الأعلى يجب أن تعيد الاتصال بإنشاء طبقة نقل جديدة

ملاحظات حول الضغط العكسي/البث

Section titled “ملاحظات حول الضغط العكسي/البث”
  • الكتابة الصادرة تستخدم stdin.write() + flush() بدون انتظار دلالات التفريغ.
  • لا توجد إدارة طابور صريحة أو علامة مائية عالية في طبقة النقل.
  • المعالجة الواردة مدفوعة بالتدفق (for await عبر readJsonl)، رسالة محللة واحدة في كل مرة.

آليات طبقة نقل HTTP/SSE الداخلية

Section titled “آليات طبقة نقل HTTP/SSE الداخلية”

دورة الحياة ودلالات الاتصال

Section titled “دورة الحياة ودلالات الاتصال”

طبقة نقل HTTP لها حالة اتصال منطقية، لكن مسار الطلب بدون حالة لكل استدعاء HTTP:

  • connect() تضبط connected=true (لا مصافحة مقبس/جلسة)
  • تتبع جلسة الخادم الاختياري عبر ترويسة Mcp-Session-Id
  • close() ترسل اختيارياً DELETE مع Mcp-Session-Id، تلغي مستمع SSE، تطلق onClose

لذا connected تعني “طبقة النقل قابلة للاستخدام”، وليس “تدفق مستمر مُنشأ”.

  • عند استجابة POST، إذا كانت ترويسة Mcp-Session-Id موجودة، تخزّنها طبقة النقل.
  • الطلبات/الإشعارات اللاحقة تتضمن Mcp-Session-Id.
  • close() تحاول إنهاء جلسة الخادم بـ HTTP DELETE؛ فشل الإنهاء يُتجاهل.

المهلة الزمنية والإلغاء

Section titled “المهلة الزمنية والإلغاء”

لكل من request() و notify():

  • المهلة تستخدم AbortController (config.timeout ?? 30000)
  • الإشارة الخارجية، إن وُجدت، تُدمج عبر AbortSignal.any([...])
  • معالجة AbortError تميّز بين إلغاء المُستدعي والمهلة

الأخطاء المطروحة:

  • المهلة: Request timeout after ...ms (أو SSE response timeout ...، Notify timeout ...)
  • إلغاء المُستدعي: يُعاد طرح AbortError الأصلي عندما تكون الإشارة الخارجية ملغاة بالفعل

عند استجابة غير OK:

  • نص الاستجابة يُضمّن في الخطأ المطروح (HTTP <status>: <text>)
  • إن وُجدت، تلميحات المصادقة من WWW-Authenticate و Mcp-Auth-Server تُلحق

عند كائن خطأ JSON-RPC:

  • يُطرح MCP error <code>: <message>

فشل تحليل جسم JSON (response.json()) يُنشر كاستثناء تحليل.

يوجد مساران لـ SSE:

  1. استجابة SSE لكل طلب (#parseSSEResponse)

    • تُستخدم عندما يكون نوع محتوى استجابة POST هو text/event-stream
    • تستهلك التدفق حتى العثور على معرّف الاستجابة المطابق
    • يمكنها معالجة الإشعارات المتداخلة أثناء نفس التدفق
  2. مستمع SSE في الخلفية (startSSEListener())

    • مستمع GET اختياري للإشعارات المُبادرة من الخادم
    • لا يُبدأ تلقائياً بواسطة مدير/عميل MCP حالياً
    • إذا أرجع GET رمز 405، يُعطّل المستمع نفسه بصمت (الخادم لا يدعم هذا النمط)

معالجة الحمولات غير الصالحة وقطع الاتصال

Section titled “معالجة الحمولات غير الصالحة وقطع الاتصال”

أخطاء تحليل JSON لـ SSE تتصاعد من readSseJson وترفض الطلب/المستمع.

  • أخطاء تحليل SSE للطلب ترفض الطلب النشط.
  • أخطاء المستمع في الخلفية تُفعّل onError (باستثناء AbortError).
  • لا إعادة اتصال تلقائية للمستمع في الخلفية.

أداة json-rpc.ts مقابل تجريد طبقة النقل

Section titled “أداة json-rpc.ts مقابل تجريد طبقة النقل”

src/mcp/json-rpc.ts يوفر مساعدات callMCP() و parseSSE() لاستدعاءات MCP HTTP المباشرة (تُستخدم بواسطة تكامل Exa)، وليس تجريد MCPTransport المُستخدم بواسطة MCPClient/MCPManager.

الاختلافات الملحوظة عن HttpTransport:

  • يحلل نص الاستجابة بالكامل أولاً، ثم يستخرج أول سطر data: (parseSSE)، مع احتياطي JSON
  • لا إدارة مهلة للطلب، لا واجهة إلغاء، لا معالجة معرّف الجلسة، لا دورة حياة لطبقة النقل
  • يُرجع كائن مظروف JSON-RPC الخام

هذا المسار خفيف لكنه أقل متانة من تنفيذ طبقة النقل الكامل.

مسؤوليات إعادة المحاولة/إعادة الاتصال

Section titled “مسؤوليات إعادة المحاولة/إعادة الاتصال”

تنفيذات طبقة النقل الحالية لا تقوم بـ:

  • إعادة محاولة الطلبات الفاشلة
  • إعادة الاتصال بعد خروج عملية stdio
  • إعادة اتصال مستمعي SSE
  • إعادة إرسال الطلبات قيد التنفيذ بعد قطع الاتصال

تفشل بسرعة وتنشر الأخطاء.

MCPManager يتعامل مع اكتشاف/تنسيق الاتصال الأولي ويمكنه إعادة الاتصال فقط بتشغيل تدفقات الاتصال مرة أخرى (مسارات connectToServer/discoverAndConnect). لا يعالج تلقائياً طبقة نقل متصلة بالفعل عند دوال رد فشل وقت التشغيل.

MCPManager لديه سلوك احتياطي عند بدء التشغيل للخوادم البطيئة (أدوات مؤجلة من ذاكرة التخزين المؤقت)، لكن ذلك احتياطي لتوفر الأدوات، وليس إعادة محاولة لطبقة النقل.

  • سطر رسالة stdio غير صالح: يُسقط؛ التدفق يستمر.
  • انتهاء تدفق/عملية Stdio: طبقة النقل تُغلق؛ الطلبات المعلقة تُرفض كـ Transport closed.
  • HTTP غير 2xx: الطلب/الإشعار يطرح خطأ HTTP.
  • استجابة JSON غير صالحة: استثناء التحليل يُنشر.
  • انتهاء SSE بدون معرّف مطابق: الطلب يفشل برسالة No response received for request ID ....
  • المهلة الزمنية: خطأ مهلة خاص بطبقة النقل.
  • إلغاء المُستدعي: AbortError/السبب يُنشر من إشارة المُستدعي.

إذا كان الاهتمام يتعلق بشكل الرسالة، أو ربط المعرّفات، أو ترتيب أساليب MCP، فإنه ينتمي إلى منطق البروتوكول/العميل.

إذا كان الاهتمام يتعلق بالتأطير (JSONL مقابل HTTP/SSE)، أو تحليل التدفق، أو دورة حياة fetch/spawn، أو ساعات المهلة، أو تفكيك الاتصال، فإنه ينتمي إلى تنفيذ طبقة النقل.