بناء خوادم MCP
كشف الموارد
الأدوات تتيح للنموذج أن ينفّذ، والموارد تتيح له أن يقرأ: الملفات وصفوف قاعدة البيانات وملفات الإعداد — أي شيء تقبل تسليمه دون السماح بتعديله.
المورد مقابل الأداة
الفارق بينهما ليس «أهذه بيانات أم إجراء». فقد تُرجع الأداة بيانات خالصة وتبقى أداة. السؤال الحقيقي هو: من يبدأ العملية، وهل يمكن أن يتغير شيء نتيجة لها.
أيهما ينبغي أن يكون هذا؟
اجعلها أداة
- يستطيع النموذج أن يقرر حاجته إليها من تلقاء نفسه
- المعطيات تجعل أداة واحدة تغطي حالات كثيرة
- كل أداة طريق جديد ليخطئ النموذج على حسابك
- تحتاج وصفاً دقيقاً لتُستدعى في الوقت المناسب
اجعلها مورداً
- آمن للكشف على نطاق واسع لأن شيئاً لا يتضرر
- قابل للتخزين المؤقت لأن المعرّف يدل على الشيء نفسه في كل مرة
- لا يستطيع النموذج طلبه من تلقاء نفسه
- المورد الكبير قد يبتلع نافذة السياق
الاختبار العملي: إن كنت تريد موافقة إنسان قبل التنفيذ، فهي أداة. وإن كنت مرتاحاً لحدوثه بصمت مئة مرة، فهو مورد.
تعريف الموارد
تستخدم الموارد URIs لتحديد المحتوى:
@mcp.resource("config://app/settings", mime_type="application/json")
def app_settings() -> str:
"""تكوين التطبيق الحالي."""
return json.dumps(load_app_settings(), indent=2)
@mcp.resource("file:///var/log/app.log", mime_type="text/plain")
def app_logs() -> str:
"""إدخالات سجل التطبيق الأخيرة."""
return read_file("/var/log/app.log")
مُزيِّن واحد يُعلن المورد ويقرأه في آنٍ معاً. الـ URI هو المعرف الذي يطلبه العميل، ونص التوثيق يصبح الوصف، وجسم الدالة لا يُنفَّذ إلا حين يطلبه شيء فعلاً — فسرد مئة مورد لا يكلّف شيئاً حتى تُقرأ إحداها.
مخططات URI
يمكنك استخدام أي مخطط URI منطقي لبياناتك:
| المخطط | حالة الاستخدام | مثال |
|---|---|---|
file:// | الملفات المحلية | file:///home/user/doc.txt |
db:// | سجلات قاعدة البيانات | db://users/123 |
config:// | التكوين | config://app/settings |
api:// | واجهات برمجة التطبيقات الخارجية | api://weather/london |
قراءة الموارد
نفذ معالج القراءة لإرجاع المحتوى:
@mcp.resource("user://{user_id}/profile", mime_type="application/json")
async def user_profile(user_id: str) -> str:
"""ملف تعريف مستخدم واحد."""
return json.dumps(await db.get_user(user_id))
وجود {placeholder} داخل الـ URI يحوّله إلى قالب مورد: يملأ العميل القيمة فتصل إليك
كمعامل في الدالة. تُسرد القوالب بمعزل عن الموارد المحددة — يكتشفها العميل عبر
resources/templates/list — وهذا ما يتيح لك كشف مليون ملف تعريف دون تعداد أيٍّ منها.
الموارد الثنائية
للبيانات الثنائية مثل الصور، أعِد bytes وصرّح بنوع MIME:
@mcp.resource("image://logo", mime_type="image/png")
def logo() -> bytes:
"""شعار التطبيق."""
return load_image("logo.png") # بايتات خام — دون ترميز base64 يدوي
أعِد bytes وسيتولى الـ SDK ترميزها بـ base64 داخل كائن blob نيابةً عنك. أما ترميزها بنفسك
فيؤدي إلى ترميز مزدوج، فينتج مورد ينتقل عبر الشبكة على أتم وجه ثم يُعرض كنفايات — وهو من
الأخطاء التي تبدو مشكلةً في العميل ليوم كامل.
الموارد الديناميكية
يمكن توليد الموارد ديناميكياً بناءً على المعلمات:
from mcp.types import Resource
# سجّل ما هو معروف عند بدء التشغيل
for project in load_known_projects():
mcp.add_resource(
Resource(
uri=f"project://{project.id}",
name=f"مشروع: {project.name}",
description=f"نظرة عامة على {project.name}",
mimeType="application/json",
)
)
تستقبل add_resource كائن mcp.types.Resource وتسجّله إلى جانب الموارد المُزيَّنة، وهذا يغطي
الحالة التي تكون فيها المجموعة معروفة عند بدء التشغيل لا عند كتابة الكود.
أما ما يتغير أثناء تشغيل الخادم فالأفضل له القالب أعلاه. تعداد كل صف يعني استعلام قاعدة
بيانات مع كل resources/list، والعملاء يستدعون ذلك أكثر بكثير مما تتوقع — بينما ينقل القالب
العمل إلى لحظة القراءة، حيث يوجد فعلاً من ينتظر الإجابة.
التالي: معالجة الأخطاء، ولماذا يمثّل النموذج جمهوراً غير معتاد لها. :::
سجّل الدخول للتقييم