دراسة حالة: كيف حوّلت MCP server داخل مشروعي إلى حزمة Laravel مفتوحة المصدر
الخلاصة: كان لدي سيرفر MCP يدير مدونتي من Claude. استخرجته إلى حزمة Composer عامة هي
abdullahdheir/laravel-blog-mcp، ثم ربطت تطبيقي بها بدل الكود الداخلي، ومعي 27 اختبارًا للحزمة وتغطية للـ endpoint الحقيقي.
المشكلة
أنشر على مدونتي مقالات بشكل يومي وأسبوعي عبر Routines تعمل بالذكاء الاصطناعي. احتجت أن يتحدث Claude مباشرة مع المدونة: يقرأ الأقسام، ينشئ المقال، يعدّله، ويستورد منشورات LinkedIn كروابط خارجية.
الحل الأول كان كودًا داخل المشروع نفسه: BlogMcpServer وMcpController وإعدادات في config/services.php. اشتغل، لكن ثلاث مشاكل ظهرت:
- الكود مربوط بجداولي. أي مدوّن Laravel آخر لن يستفيد منه دون نسخ وتعديل.
- لا حدود واضحة. منطق البروتوكول (JSON-RPC) ومنطق المدونة (slugs، حالات النشر، القسم) مخلوطان في ملف واحد.
- صعب التطوير بأمان. كل تعديل على الأداة قد يكسر الروتين الذي ينشر يوميًا على الموقع الحي.
القرار: حزمة بحدود واضحة
قسّمت المسؤوليات إلى ثلاث طبقات:
- بروتوكول MCP: سيرفر بلا حالة (stateless) عبر Streamable HTTP، يعالج
initializeوtools/listوtools/call. - الأدوات (Tools): ست أدوات:
list_categories،list_posts،get_post،create_post،create_external_post،update_post. - تخزين المقالات: واجهة
PostStore. الحزمة تأتي بتنفيذ Eloquent جاهزEloquentPostStore، ويمكن لأي مشروع تبديله بتنفيذه الخاص.
القرار الأهم أن أسماء الجداول والأعمدة تأتي من ملف إعدادات، لا من الكود. بهذا تعمل الحزمة مع جدول posts عندي وجدول مختلف عند غيري. التوثيق (Authentication) عبر bearer token، ويُقرأ من BLOG_MCP_TOKEN.
ما تعلمته عند الربط بتطبيقي
أصعب جزء لم يكن كتابة الحزمة، بل استبدال الكود القديم بها دون تغيير سلوك الموقع. كتبت اختبارات تمر على الـ endpoint الحقيقي /api/mcp قبل الاستبدال وبعده، وكشفت أربعة فروقات لم أكن لأنتبه لها:
| الفرق | الحل |
|---|---|
مطابقة slug القسم كانت حساسة لحالة الأحرف (AI لا يطابق ai) | غلاف AppPostStore يحوّل الـ slug إلى أحرف صغيرة قبل المطابقة |
| صياغة رسالة الخطأ لأقسام غير صالحة | حدّثت الاختبار لتطابق صياغة الحزمة |
نوع المقال الخارجي external مقابل syndicated | وحّدت على syndicated وحدّثت الاختبار |
منصة المقال الخارجي لازم أن تكون موجودة في جدول platforms | الغلاف ينشئها عند الحاجة بـ firstOrCreate |
الدرس: ما يخص تطبيقك لا يدخل الحزمة. بدل تلويثها بحالات خاصة بمدونتي، كتبت غلافًا صغيرًا AppPostStore (نمط Decorator) يضيف ما أحتاجه فوق التنفيذ العام: حقل emailed_to_subscribers، وأسماء الأقسام بالعربية والإنجليزية، ومطابقة الأقسام.
الجودة والاختبار
- 27 اختبارًا للحزمة باستخدام Pest وOrchestra Testbench.
- سير عمل CI على PHP 8.2 إلى 8.4 وLaravel 11 و12، لكنه لم يعمل بعد لأن حسابي على GitHub عليه قفل فوترة. أذكر هذا بوضوح: الحزمة جرّبتها فعليًا على إصدار واحد من PHP وLaravel فقط.
- أثناء الاختبار ظهرت أخطاء صغيرة: ترتيب الأقسام غير حتمي، وfixture يحتاج
$guarded = []. لو لم أكتب الاختبارات لخرجت هذه الأخطاء إلى المستخدمين.
ما لم يكتمل
- مطابقة الأقسام غير الحساسة للحالة موجودة في تطبيقي فقط، وسأنقلها إلى الحزمة في الإصدار 0.1.1.
- تشغيل CI على كامل مصفوفة الإصدارات بعد حل مشكلة الفوترة.
- أدوات الحذف غير موجودة عمدًا، لأن إتاحتها لنموذج ذكاء اصطناعي خطر لا يلزم الآن.
النتيجة
- حزمة عامة بترخيص MIT على Packagist وGitHub.
- تطبيقي صار يعتمد عليها بإصدار
^0.1، والكود الداخلي القديم حُذف. - الروتين اليومي والأسبوعي لم يتغير من جهة الاستخدام.
تثبيتها
composer require abdullahdheir/laravel-blog-mcp
php artisan vendor:publish --tag=blog-mcp-config
ثم اضبط BLOG_MCP_TOKEN وأسماء الجداول والأعمدة في config/blog-mcp.php، وسجّل الـ route الذي يشير إلى McpController. الشرح الكامل في الـ README.
إن جرّبتها وواجهتك مشكلة، افتح Issue على GitHub.