MCP: أيدٍ لمساعد الذكاء الاصطناعي
Model Context Protocol (MCP) بروتوكول مفتوح يوحّد طريقة اتصال تطبيقات الذكاء الاصطناعي (Claude، محرر الشيفرة، الوكلاء) بمصادر البيانات والإجراءات الخارجية. بدلاً من كتابة تكامل خاص لكل مساعد، تكتب خادم MCP واحداً ويستطيع كل عميل متوافق استخدامه.
يعرض خادم MCP ثلاثة أنواع من القدرات:
- الأدوات (Tools): إجراءات يستطيع النموذج استدعاءها (فحص خدمة، إنشاء تذكرة، تنفيذ استعلام SQL للقراءة فقط…)
- الموارد (Resources): بيانات يستطيع العميل قراءتها (الإعدادات، التوثيق، حالة الخادم)
- القوالب (Prompts): نماذج رسائل قابلة لإعادة الاستخدام يحدد المستخدم معاملاتها
يتبادل العميل والخادم رسائل JSON-RPC 2.0، إما عبر الإدخال والإخراج القياسيين (stdio، وهو مثالي محلياً) أو عبر HTTP (Streamable HTTP، لخادم بعيد).
حزمة SDK الرسمية لـ PHP
منذ عام 2025 أصبح لـ PHP حزمة SDK رسمية: mcp/sdk، طوّرتها PHP Foundation ومشروع Symfony معاً انطلاقاً من عمل PHP-MCP وSymfony AI. الحزمة مستقلة عن أي إطار عمل وتلتزم بوعد التوافق العكسي الخاص بـ Symfony، لكنها ما زالت تجريبية قبل الإصدار 1.0: ثبّت رقم الإصدار في ملف composer.json.
المتطلبات: PHP 8.1 على الأقل. التثبيت:
composer require mcp/sdk symfony/finder
الفخ الأول: الحزمة symfony/finder مجرد اعتمادية مقترحة، لكنها ضرورية لاكتشاف الأدوات تلقائياً عبر السمات (attributes). بدونها يفشل الخادم عند الإقلاع، وبما أن الخطأ يُكتب في مخرج الأخطاء القياسي، يرى العميل ببساطة خادماً بلا أي أداة.
خادم أول: مساعد DevOps
لنبنِ خادماً مفيداً في العمل اليومي: يتحقق من أن رابطاً ما يستجيب، ويفحص المساحة المتبقية على القرص، ويعرض معلومات النظام، ويقترح قالباً لتقرير حادث. ابدأ بتعريف التحميل التلقائي لأصنافك:
{
"require": {
"mcp/sdk": "^0.8",
"symfony/finder": "^8.1"
},
"autoload": {
"psr-4": { "App\\": "src/" }
}
}
القدرات مجرد دوال PHP عادية مزوّدة بسمات. تولّد الحزمة مخطط JSON للمعاملات من أنواع PHP، والوصف من تعليق docblock:
<?php
namespace App;
use Mcp\Capability\Attribute\McpPrompt;
use Mcp\Capability\Attribute\McpResource;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Capability\Attribute\Schema;
use Mcp\Exception\ToolCallException;
final class DevOpsTools
{
/**
* Checks that a URL responds and returns its HTTP status and response time.
*/
#[McpTool(name: 'check_url')]
public function checkUrl(
#[Schema(format: 'uri', description: 'Full URL, e.g. https://benmacha.tn')]
string $url,
): array {
if (!preg_match('#^https?://#', $url)) {
throw new ToolCallException('Only http(s) URLs are accepted.');
}
$start = microtime(true);
$context = stream_context_create(['http' => ['method' => 'HEAD', 'timeout' => 5, 'ignore_errors' => true]]);
$headers = @get_headers($url, true, $context);
if ($headers === false) {
return ['url' => $url, 'up' => false, 'error' => 'Host unreachable'];
}
preg_match('#\s(\d{3})\s#', $headers[0], $m);
$status = (int) ($m[1] ?? 0);
return [
'url' => $url,
'up' => $status > 0 && $status < 400,
'status' => $status,
'time_ms' => (int) round((microtime(true) - $start) * 1000),
];
}
/**
* Returns the used and available disk space for a path.
*/
#[McpTool(name: 'disk_usage')]
public function diskUsage(
#[Schema(description: 'Path to inspect')]
string $path = '/',
): array {
$total = @disk_total_space($path);
$free = @disk_free_space($path);
if ($total === false || $free === false) {
throw new ToolCallException(sprintf('Unreadable path: %s', $path));
}
return [
'path' => $path,
'total_gb' => round($total / 1e9, 1),
'free_gb' => round($free / 1e9, 1),
'used_percent' => round(100 * ($total - $free) / $total, 1),
];
}
#[McpResource(uri: 'server://info', name: 'server_info', mimeType: 'application/json')]
public function serverInfo(): array
{
return ['hostname' => gethostname(), 'os' => PHP_OS_FAMILY, 'php' => PHP_VERSION];
}
/**
* Prepares an incident report from a service and a symptom.
*/
#[McpPrompt(name: 'incident_report')]
public function incidentReport(string $service, string $symptom): array
{
return [[
'role' => 'user',
'content' => "The \"$service\" service shows this symptom: $symptom. "
. "Use check_url and disk_usage to diagnose it, then write a short "
. "incident report: impact, probable cause, immediate actions.",
]];
}
}
نقطة الدخول لا تتجاوز بضعة أسطر: نعرّف الخادم، ونطلب منه فحص المجلد src، ثم نشغّله على ناقل stdio.
#!/usr/bin/env php
<?php
require __DIR__.'/vendor/autoload.php';
use Mcp\Server;
use Mcp\Server\Transport\StdioTransport;
exit(Server::builder()
->setServerInfo('DevOps Assistant', '1.0.0')
->setDiscovery(__DIR__, ['src'])
->build()
->run(new StdioTransport()));
ما يراه العميل
انطلاقاً من توقيع الدالة checkUrl(string $url) وتعليق docblock والسمة #[Schema]، تنشر الحزمة تعريف الأداة التالي:
{
"name": "check_url",
"description": "Checks that a URL responds and returns its HTTP status and response time.",
"inputSchema": {
"type": "object",
"properties": {
"url": { "type": "string", "format": "uri", "description": "Full URL, e.g. https://benmacha.tn" }
},
"required": ["url"]
}
}
بالنسبة إلى disk_usage، تصبح القيمة الافتراضية '/' حقلاً "default" ويصبح المعامل اختيارياً. وعندما تُرجع الأداة مصفوفة، ترسلها الحزمة نصاً بصيغة JSON وأيضاً في الحقل structuredContent الجاهز للاستخدام من طرف العميل.
معالجة الأخطاء بشكل صحيح
الفخ الثاني: ليس كل استثناء مناسباً. أي استثناء عام (InvalidArgumentException أو RuntimeException…) يتحول إلى خطأ JSON-RPC عام بعنوان "Error while executing tool"، ولا يعرف النموذج ما الذي حدث. استخدم بدلاً منه Mcp\Exception\ToolCallException: تُعاد رسالته ضمن نتيجة معلَّمة بـ isError: true، فيستطيع النموذج قراءتها وتصحيح استدعائه (مثلاً إعادة المحاولة برابط https).
الاختبار دون مساعد: عميل PHP وأداة Inspector
تحتوي الحزمة أيضاً على عميل، وهو مثالي للاختبارات الآلية:
use Mcp\Client;
use Mcp\Client\Transport\StdioTransport;
$client = Client::builder()->setClientInfo('Tests', '1.0.0')->build();
$client->connect(new StdioTransport(command: 'php', args: [__DIR__.'/server.php']));
foreach ($client->listTools()->tools as $tool) {
echo $tool->name, ': ', $tool->description, PHP_EOL;
}
$result = $client->callTool('check_url', ['url' => 'https://benmacha.tn']);
var_dump($result->structuredContent); // ['url' => ..., 'up' => true, 'status' => 200, 'time_ms' => ...]
$client->disconnect();
ولاستكشاف الخادم بصرياً، تشغّل أداة MCP Inspector الرسمية الخادمَ وتعرض أدواته وموارده وقوالبه:
npx @modelcontextprotocol/inspector php server.php
ربط الخادم بـ Claude
مع Claude Code يكفي أمر واحد:
claude mcp add devops -- php /absolute/path/to/server.php
ومع Claude Desktop أضف الخادم إلى الملف claude_desktop_config.json:
{
"mcpServers": {
"devops": {
"command": "php",
"args": ["/absolute/path/to/server.php"]
}
}
}
ثم اسأل: "هل يستجيب الموقع benmacha.tn بشكل صحيح، وهل بقيت مساحة على القرص؟". سيستدعي المساعد check_url ثم disk_usage ويلخّص النتائج.
القواعد الذهبية في بيئة الإنتاج
- لا تكتب أبداً في stdout في وضع stdio: المخرج القياسي محجوز للبروتوكول، وأي
echoأوvar_dumpمنسيّ يفسد التبادل. سجّل في stderr أو في ملف (يقبل الـ builder مسجِّلاً متوافقاً مع PSR-3). - أدوات محددة بدلاً من أداة "نفّذ أمراً": كشف سطر الأوامر أو SQL حرّ يعني تسليم مفاتيح الخادم للنموذج.
- تحقّق من كل مُدخل: مخطط JSON يوجّه النموذج لكنه لا يغني عن التحقق في الخادم (قوائم مسموح بها للمسارات والمضيفين والجداول).
- أقل الصلاحيات: شغّل الخادم بمستخدم نظام مخصص، وبحساب قاعدة بيانات للقراءة فقط كلما أمكن.
- أوصاف مكتوبة بعناية: فهي التوثيق الوحيد الذي يقرؤه النموذج لاختيار الأداة والمعامل المناسبين.
للتعمق أكثر
توفّر الحزمة أيضاً ناقل HTTP (Streamable HTTP) لاستضافة خادم بعيد يتشاركه فريق كامل، مع إدارة الجلسات والتفويض، وتدعم جيلَي البروتوكول بما فيهما المراجعة عديمة الحالة 2026-07-28. أما على مستوى أطر العمل، فتدمج symfony/mcp-bundle الحزمة في Symfony (تصبح خدماتك أدوات MCP مع حقن الاعتماديات)، وتعرض api-platform/mcp موارد API Platform مباشرة.
هذا هو النهج الذي أعتمده لربط مساعدي الذكاء الاصطناعي ببيانات العمل: بضع أدوات محددة النطاق وللقراءة فقط بأوصاف دقيقة، فيصبح الذكاء الاصطناعي قادراً على الإجابة عن أسئلة كانت تتطلب سابقاً استعلام SQL أو تصديراً يدوياً.