إذا بنيت وكيل ذكاء اصطناعي من قبل، فغالباً كتبت أدواته داخل الكود مباشرة: مخطط JSON في مكان، ودالة Python في مكان آخر، وكلها مربوطة بنموذج واحد وتطبيق واحد. هذا يعمل إلى أن تحتاج الأدوات نفسها في تطبيق ثانٍ. هنا يأتي بروتوكول MCP (Model Context Protocol): تكتب الأداة مرة واحدة على شكل خادم صغير، ثم يستخدمها أي تطبيق يدعم البروتوكول، مثل Claude Code وClaude Desktop وعدد متزايد من المحررات وأطر بناء الوكلاء.
في هذا الدليل نبني خادم MCP حقيقياً بلغة Python، ونختبره دون صرف أي استدعاء API، ثم نربطه بـ Claude. كُتب الدليل على أحدث إصدار من المواصفة، وهو 2026-07-28، ومكتبة Python SDK 2.x. وهذه النقطة مهمة أكثر من المعتاد، لأن تحديث يوليو 2026 غيّر أشياء كثيرة، وجزء كبير من الشروحات المنتشرة على الإنترنت سيعطيك كوداً لا يعمل أصلاً، أو يعتمد على ميزات صارت مُهملة رسمياً.
ماذا تغيّر في يوليو 2026؟ ولماذا لم تعد الشروحات القديمة صالحة؟
الإصدار السابق من المواصفة كان بتاريخ 2025-11-25. أما إصدار 2026-07-28 فهو أكبر تغيير منذ إطلاق البروتوكول. خمسة تغييرات تهمّك إذا كنت تبني خادماً اليوم:
- تغيّر اسم الكلاس في Python. في الإصدار 2.x صار
FastMCPاسمهMCPServer، ويُستورد هكذا:from mcp.server import MCPServer. طريقة الاستخدام بالـ decorators مثل@mcp.tool()بقيت كما هي. إذا رأيت شرحاً يبدأ بـfrom mcp.server.fastmcp import FastMCPفاعرف أنه مكتوب للإصدار 1.x. - البروتوكول صار بلا حالة (stateless). اختفت خطوة المصافحة الأولى
initialize، وصار كل طلب يحمل بنفسه رقم إصدار البروتوكول وقدرات العميل. كما أُزيلت الجلسات على مستوى البروتوكول (ترويسةMcp-Session-Id) من نقل HTTP. وإذا احتاج خادمك أن يتذكر شيئاً بين استدعاء وآخر، فالحل الذي تقترحه المواصفة هو أن يعطي العميلَ معرّفاً (ID) يعيده لاحقاً كمُعامل عادي في الأداة. - ميزات Sampling وRoots وLogging صارت مُهملة (deprecated). ما زالت تعمل خلال فترة انتقالية لا تقل عن 12 شهراً، لكن لا يُنصح ببناء أي خادم جديد عليها. البدائل التي تقترحها المواصفة: مرّر المجلدات والملفات كمُعاملات للأداة بدل Roots، واستدعِ API مزوّد النموذج مباشرة بدل Sampling، واكتب السجلات إلى stderr أو OpenTelemetry بدل Logging.
- الخادم يستطيع طلب معلومات إضافية أثناء الطلب. بدل أن يرسل الخادم طلبات خاصة به إلى العميل، صار يردّ بنتيجة من نوع
input_required، فيعيد العميل الطلب الأصلي ومعه الإجابات. مكتبة العميل في الـ SDK تتولى هذه الدورة عنك. - نقل HTTP+SSE القديم صار مُهملاً رسمياً. للخوادم البعيدة استخدم Streamable HTTP، أما الخوادم التي تعمل على جهازك فما زال stdio أبسط خيار، وهو ما سنستخدمه هنا.
والخبر الجيد أن خوادم الإصدار 2.x ما زالت تتعامل مع العملاء القدامى من جيل 2025، فلن تضطر للاختيار بين التطبيقات الجديدة والقديمة.
هل تحتاج MCP فعلاً في مشروعك؟
البروتوكول يضيف عملية منفصلة وطبقة تواصل بين النموذج وأدواتك، وهذا عبء إضافي. لذلك كن صريحاً مع نفسك:
- استخدمه إذا أردت الأدوات نفسها في أكثر من تطبيق (مساعد المحرر ووكيلك الخاص مثلاً)، أو أردت مشاركة الأدوات مع فريقك عبر ملف إعدادات، أو كنت تغلّف نظاماً ستتعامل معه عدة وكلاء، مثل قاعدة بيانات أو API داخلي أو مخزن ملفات.
- تجاوزه إذا كان عندك سكربت واحد بأداتين لن يستخدمهما إلا برنامج واحد. هنا استخدام الأدوات مباشرة عبر API مزوّد النموذج أقل كوداً وأسهل في تتبع الأخطاء، وهذا ما يشرحه دليل بناء أول وكيل.
ثلاثة أشياء يقدمها الخادم
خادم MCP يقدّم مزيجاً من ثلاثة عناصر، واختيار العنصر المناسب هو معظم عمل التصميم:
- الأدوات (Tools): دوال يقرر النموذج متى يستدعيها، مثل "أضف مهمة" أو "ابحث في الفواتير". ولأنها قد تغيّر بيانات، تشترط المواصفة على التطبيق أن يأخذ موافقة المستخدم قبل تشغيلها.
- الموارد (Resources): بيانات للقراءة فقط يجلبها التطبيق إلى سياق النموذج، ولكل منها عنوان مثل
tasks://overdue. استخدمها للمعلومة لا للفعل. - القوالب (Prompts): تعليمات جاهزة يختارها المستخدم بنفسه، تشبه أوامر الشرطة المائلة. بها تحزم "الطريقة الصحيحة لأداء المهمة" مع الأدوات.
من الأخطاء الشائعة عند المبتدئين تحويل كل شيء إلى أداة. إذا كان النموذج يحتاج أن يقرأ فقط، فالمورد يُبقي قائمة الأدوات قصيرة، وكلما قصرت القائمة زادت دقة النموذج في اختيار الأداة الصحيحة.
لنبنِ خادم قائمة مهام في حوالي 80 سطراً
سنبني خادماً صغيراً لإدارة المهام فيه ثلاث أدوات ومورد واحد وقالب واحد. يحفظ المهام في ملف JSON على جهازك، فلا حاجة لقاعدة بيانات ولا لمفتاح API. تحتاج فقط Python 3.10 أو أحدث.
جهّز المشروع باستخدام uv، وهو مدير الحزم الذي تعتمده وثائق MCP الرسمية:
# macOS / Linux
curl -LsSf https://astral.sh/uv/install.sh | sh
# Windows (PowerShell)
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
# then, in a new terminal
uv init tasks
cd tasks
uv add "mcp[cli]"
أنشئ الملف tasks_server.py:
import json
import logging
from datetime import date
from pathlib import Path
from typing import Annotated
from pydantic import Field
from mcp.server import MCPServer
logger = logging.getLogger(__name__) # logs go to stderr, never stdout
DB = Path(__file__).parent / "tasks.json"
mcp = MCPServer("tasks")
def load() -> list[dict]:
return json.loads(DB.read_text(encoding="utf-8")) if DB.exists() else []
def save(tasks: list[dict]) -> None:
DB.write_text(json.dumps(tasks, ensure_ascii=False, indent=2), encoding="utf-8")
@mcp.tool()
def add_task(
title: Annotated[str, Field(description="What needs to be done, in one short sentence.")],
due: Annotated[str | None, Field(description="Optional due date, YYYY-MM-DD.")] = None,
) -> str:
"""Add a task to the to-do list."""
tasks = load()
task_id = max((t["id"] for t in tasks), default=0) + 1
tasks.append({"id": task_id, "title": title, "due": due, "done": False})
save(tasks)
logger.info("added task %s", task_id)
return f"Added task #{task_id}: {title}"
@mcp.tool()
def list_tasks(include_done: bool = False) -> str:
"""List tasks. Open tasks only unless include_done is true."""
tasks = [t for t in load() if include_done or not t["done"]]
if not tasks:
return "No tasks."
return "\n".join(
f"#{t['id']} [{'x' if t['done'] else ' '}] {t['title']}"
+ (f" (due {t['due']})" if t["due"] else "")
for t in tasks
)
@mcp.tool()
def complete_task(
task_id: Annotated[int, Field(description="The number shown next to the task in list_tasks.")],
) -> str:
"""Mark a task as done."""
tasks = load()
for t in tasks:
if t["id"] == task_id:
t["done"] = True
save(tasks)
return f"Task #{task_id} marked done."
return f"No task with id {task_id}."
@mcp.resource("tasks://overdue")
def overdue() -> str:
"""Open tasks whose due date has passed."""
today = date.today().isoformat()
late = [t for t in load() if not t["done"] and t["due"] and t["due"] < today]
return json.dumps(late, ensure_ascii=False)
@mcp.prompt()
def weekly_review() -> str:
"""Review the week's tasks and plan the next one."""
return (
"Call list_tasks with include_done=true. Group the tasks into done, "
"still open, and overdue. Then suggest the three most important open "
"tasks for next week and explain why in one line each."
)
if __name__ == "__main__":
mcp.run(transport="stdio")
في هذا الملف ثلاث تفاصيل مقصودة، وكل واحدة منها تمنع مشكلة كنت ستواجهها لاحقاً:
- وصف المُعاملات مكتوب بـ
AnnotatedوField. عندما اختبرنا الإصدار 2.3، وجدنا أن قسمArgs:في الـ docstring يبقى داخل نص وصف الأداة، لكنه لا يظهر في مخطط المُعاملات نفسه. أماField(description=...)فيضع الوصف على المُعامل مباشرة، وهذا هو المكان الذي ينظر إليه النموذج عندما يقرر ماذا يمرّر. - السجلات عبر
loggingوليسprintأبداً. خادم stdio يتواصل مع العميل عبر المخرج القياسي، وأيprint()عابر يدسّ نصاً في هذه القناة. سترى في القسم التالي ماذا يحدث بالضبط. - الحالة محفوظة في الخادم، والنموذج يحصل على رقم. الأداة
complete_taskتأخذtask_idرآه النموذج في نتيجةlist_tasks. وهذا تماماً النمط الذي توصي به مواصفة 2026 بعد إلغاء الجلسات: لا جلسة مخفية، بل معرّف يُعاد كمُعامل.
اختبره قبل أن يلمسه أي ذكاء اصطناعي
معظم الشروحات تقفز مباشرة إلى "أعد تشغيل Claude وانتظر ظهور الأداة". وإذا لم تظهر، لن تعرف هل المشكلة في الخادم أم في ملف الإعدادات أم في التطبيق. الأفضل أن تختبر الخادم وحده أولاً بالعميل المدمج في الـ SDK. أنشئ test_client.py بجانب الخادم:
import asyncio
import sys
from mcp import Client
from mcp.client.stdio import StdioServerParameters
async def main():
server = StdioServerParameters(command=sys.executable, args=["tasks_server.py"])
async with Client(server) as client:
print("protocol:", client.protocol_version)
tools = await client.list_tools()
print("tools:", [t.name for t in tools.tools])
print(tools.tools[0].input_schema["properties"]["title"])
r = await client.call_tool("add_task", {"title": "Write the MCP article", "due": "2026-01-01"})
print(r.content[0].text)
r = await client.call_tool("add_task", {"title": "Review it"})
print(r.content[0].text)
await client.call_tool("complete_task", {"task_id": 2})
r = await client.call_tool("list_tasks", {"include_done": True})
print(r.content[0].text)
res = await client.read_resource("tasks://overdue")
print("overdue:", res.contents[0].text)
p = await client.get_prompt("weekly_review")
print("prompt:", p.messages[0].content.text[:60])
asyncio.run(main())
شغّله بالأمر uv run test_client.py. هذا هو الناتج الذي حصلنا عليه مع الإصدار 2.3.0:
protocol: 2026-07-28
tools: ['add_task', 'list_tasks', 'complete_task']
{'description': 'What needs to be done, in one short sentence.', 'title': 'Title', 'type': 'string'}
Added task #1: Write the MCP article
Added task #2: Review it
#1 [ ] Write the MCP article (due 2026-01-01)
#2 [x] Review it
overdue: [{"id": 1, "title": "Write the MCP article", "due": "2026-01-01", "done": false}]
prompt: Call list_tasks with include_done=true. Group the tasks into
السطر الأول يؤكد أن العميل والخادم اتفقا على بروتوكول 2026-07-28. وإذا فشل أي استدعاء هنا، فالمشكلة في خادمك، وقد اكتشفتها في ثوانٍ بدل التخمين داخل نافذة محادثة.
ولنرى لماذا قاعدة print مهمة، استبدلنا سطر السجل بـ print("added task", task_id) وشغّلنا الاختبار نفسه. انهار عند أول استدعاء لـ add_task برسالة Invalid JSON: expected value at line 1 column 1 ... input_value='added task 1'. العميل كان ينتظر رسالة بروتوكول، فوصلته جملة عادية. وفي تطبيق سطح المكتب يظهر الخطأ نفسه عادةً كرسالة غامضة مثل "انقطع الاتصال بالخادم".
اربطه بـ Claude
Claude Code: من مجلد مشروعك سجّل الخادم (استخدم المسار الكامل لمجلد tasks):
claude mcp add tasks -- uv --directory /ABSOLUTE/PATH/TO/tasks run tasks_server.py
كل ما بعد -- هو الأمر الذي يشغّل خادمك. النطاق الافتراضي local يفعّله للمشروع الحالي فقط، و--scope project يكتبه في ملف .mcp.json تستطيع رفعه مع الكود لفريقك، و--scope user يجعله متاحاً في كل مشاريعك. تأكد من التسجيل بـ claude mcp list، أو اكتب /mcp داخل Claude Code.
Claude Desktop: افتح ملف الإعدادات (في Windows مساره %APPDATA%\Claude\claude_desktop_config.json، وفي macOS ~/Library/Application Support/Claude/claude_desktop_config.json) وأضف:
{
"mcpServers": {
"tasks": {
"command": "uv",
"args": ["--directory", "C:\\ABSOLUTE\\PATH\\TO\\tasks", "run", "tasks_server.py"]
}
}
}
أغلق التطبيق تماماً وأعد فتحه. انتبه في Windows إلى الشرطتين المائلتين المزدوجتين في مسار JSON. وإذا لم يجد التطبيق uv، ضع مساره الكامل في command (اعرفه بالأمر where uv في Windows أو which uv في macOS).
الآن اطلب: "أضف مهمة لإرسال الفاتورة قبل الجمعة، ثم اعرض مهامي المفتوحة". ستلاحظ أن التطبيق يطلب إذنك قبل تشغيل كل أداة. لا تتعامل مع هذه الخطوة كإزعاج تريد إطفاءه؛ فالمواصفة تعتبر الأدوات تنفيذاً لكود قد يفعل أي شيء، وهذه الموافقة هي خط الدفاع الأخير.
أربعة أخطاء تجعل الوكيل يسيء استخدام أدواتك
أن يعمل الخادم شيء، وأن يستخدمه النموذج بشكل صحيح شيء آخر. هذه المشاكل تظهر بعد أن يعمل الكود:
- أوصاف غامضة. النموذج يختار الأداة من اسمها ووصفها فقط. أداة اسمها "معالجة البيانات" ستُستدعى عشوائياً، أما "تعليم المهمة كمنجزة" فستُستدعى في مكانها. اكتب الوصف لقارئ لا يرى الكود.
- أدوات كثيرة جداً. كل أداة تأخذ مساحة من سياق النموذج وتضيف خياراً آخر يحتار بينه. إذا وصلت إلى عشرين أداة، اسأل نفسك: أيّها في الحقيقة قراءة فقط (حوّلها إلى موارد)؟ وأيّها يمكن دمجه؟
- أدوات حذف بلا حماية. تعمّدنا ألّا نضيف
delete_all_tasks. إذا احتجت عمليات حذف، فاجعلها محدودة (معرّف واحد في كل مرة)، وأعِد للمستخدم ما الذي تغيّر بالضبط. - الثقة العمياء بما ترجعه الأداة. النص الذي ترجعه الأداة، كمحتوى بريد أو صفحة ويب، يدخل مباشرة إلى سياق النموذج. وإذا احتوى هذا النص على تعليمات، فقد ينفّذها النموذج. هذا ما يُسمى Prompt Injection، ولهذا يجب أن تُرجع الأدوات التي تقرأ محتوى خارجياً بيانات واضحة التصنيف، لا أن تتصرف بناءً عليها.
الخطوة التالية
استبدل ملف JSON بشيء تستخدمه فعلاً: قاعدة SQLite، أو مجلد ملاحظاتك، أو API داخلي في شركتك. حافظ على الشكل نفسه: أدوات قليلة موصوفة جيداً للأفعال، وموارد للقراءة، وقالب واحد يلخّص طريقة إنجاز العمل. اختبر بعميل الـ SDK بعد كل تعديل، ثم اربطه بـ Claude. وعندما تكون جاهزاً لتشغيله لأشخاص آخرين عبر الشبكة، انتقل إلى نقل Streamable HTTP بالأمر mcp.run(transport="streamable-http")، واقرأ قسم التفويض (Authorization) في المواصفة قبل أن تفتح أي شيء للعامة.
المصادر: مواصفة MCP إصدار 2026-07-28 وسجل تغييراتها (modelcontextprotocol.io)، ودليل "Build an MCP server" الرسمي، وملاحظات إصدارات MCP Python SDK، ووثائق MCP في Claude Code (code.claude.com). جرى اختبار الكود بتاريخ 2026-10-04 على mcp 2.3.0 وPython 3.13.