كيف تبني خادم MCP من الصفر بلغة Python، خطوة بخطوة
خادم MCP بلغة Python يعمل، من مجلد فارغ إلى Claude Desktop. اكتب أدوات وموارد وأوامر نصية باستخدام SDK الرسمي 2.x، واختبرها في Inspector، وأضف أداة صور حقيقية مع استدعاءات API غير متزامنة، وانشره عبر Streamable HTTP.
تكتفي معظم دروس MCP بأداة طقس تُرجع نصًا ثابتًا مكتوبًا في الشيفرة. هذا الدرس يبني خادمًا يمكنك الاحتفاظ به فعلًا. ستكتبه من الصفر باستخدام SDK الرسمي للغة Python، وتختبره دون أي عميل ذكاء اصطناعي، ثم تربطه مع Claude Desktop و Claude Code، وتنتهي بأداة حقيقية تستدعي API لتوليد الصور وتنتظر النتيجة. كل ما يلي يحتاج إلى Python 3.10 أو أحدث، ويتوافق مع MCP Python SDK 2.x (الإصدار 2.3.0 على PyPI وقت كتابة هذا المقال، أكتوبر 2026).
إذا نسخت شيفرة من درس عام 2025 وصادفتك ModuleNotFoundError: No module named 'mcp.server.fastmcp'، فأنت في المكان الصحيح. اسم الصنف الرئيسي تغيّر، ويوضح قسم الإعداد الإصلاح بسطر واحد.
ماذا يفعل خادم MCP فعليًا؟
Model Context Protocol (MCP) طريقة قياسية تتيح لتطبيق ذكاء اصطناعي استدعاء شيفرتك. هناك ثلاثة أدوار مهمة. المضيف هو التطبيق الذي يتحدث معه الشخص، مثل Claude Desktop أو بيئة تطوير. العميل يعيش داخل المضيف ويتحدث البروتوكول. الخادم هو الجزء الذي تبنيه أنت. خادمك لا يتحدث مع النموذج مباشرة، بل يكتفي بالرد على طلبات العميل.
ثلاث قدرات أساسية، وثلاث جهات تتحكم فيها
يعرض الخادم بالضبط ثلاثة أنواع من القدرات، والفرق بينها هو من يقرر استخدامها:
القدرة الأساسية
من يُشغّلها
ما هي
مثال
أداة
النموذج
دالة تنفّذ إجراءً
توليد صورة، كتابة صف في قاعدة بيانات
مورد
التطبيق
بيانات تُحمَّل في سياق النموذج
ملف، إعداد، كتالوج
أمر نصي
المستخدم
قالب رسالة قابل لإعادة الاستخدام
أمر شرطة مائلة (slash command)
إذا سبق أن بنيت واجهة برمجية للويب، فالتشابه سريع. المورد يشبه GET، والأداة تشبه POST، والأمر النصي استعلام محفوظ يشغّله المستخدم باسمه.
💡 قاعدة عامة: إذا كان يجب أن يقرر النموذج متى يُشغَّل الشيء، فاجعله أداة. إذا كان يجب أن يرفقه التطبيق، فاجعله موردًا. إذا كان يجب أن يختاره شخص من قائمة، فاجعله أمرًا نصيًا.
اختر وسيلة النقل مبكرًا
وسيلة النقل هي الطريقة التي تنتقل بها البيانات بين العميل والخادم. تختارها بوسيط واحد يُمرَّر إلى mcp.run().
وسيلة النقل
آلية العمل
متى تستخدمها
stdio
يشغّل المضيف ملفك كعملية فرعية، ويتحدث عبر stdin و stdout الخاصة بها
الخوادم المحلية، وهي الافتراضية
streamable-http
خادم HTTP حقيقي على منفذ، ونقطة الوصول عند /mcp
أي شيء تنشره
sse
نقل HTTP الأقدم
لا تستخدمه لأي شيء جديد، فقد استُبدل في مراجعة البروتوكول 2025-03-26
ابدأ باستخدام stdio. ستنتقل إلى Streamable HTTP قرب النهاية، وشيفرة الأداة تبقى كما هي تمامًا.
إعداد Python في خمس دقائق
تثبيت uv مع SDK
تحتاج إلى Python 3.10 أو أحدث، وإلى uv. أنشئ مشروعًا وأضف SDK:
uv init mcp-image-studio
cd mcp-image-studio
uv add "mcp[cli]" httpx
تثبّت الإضافة cli الأمر mcp مع mcp dev و mcp run و mcp install. pip install "mcp[cli]" العادي يعمل أيضًا. Inspector تطبيق Node.js، لذلك يجب أن يكون npx موجودًا في PATH لديك.
تغيير الاسم الذي يكسر الشيفرة القديمة
في SDK 1.x كان الصنف عالي المستوى اسمه FastMCP. في 2.x أصبح MCPServer، ويقع في وحدة مختلفة:
# SDK 1.x, seen in older tutorials
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("Demo")
# SDK 2.x, used in this article
from mcp.server import MCPServer
mcp = MCPServer("Demo")
هناك تغيير آخر يُربك الناس: إعدادات النقل، مثل port، انتقلت من المُنشئ إلى run(). تمرير port= إلى MCPServer(...) يطلق TypeError.
اكتب خادمك الأول
أنشئ server.py. ملف واحد، وثلاثة مزخرفات، وتُسجَّل كل القدرات:
from mcp.server import MCPServer
mcp = MCPServer("Demo")
@mcp.tool()
def add(a: int, b: int) -> int:
"""Add two numbers."""
return a + b
@mcp.resource("greeting://{name}")
def greeting(name: str) -> str:
"""Greet someone by name."""
return f"Hello, {name}!"
@mcp.prompt()
def summarize(text: str) -> str:
"""Summarize a piece of text in one sentence."""
return f"Summarize the following text in one sentence:\n\n{text}"
if __name__ == "__main__":
mcp.run()
هذا خادم يعمل بالفعل. لشرط الحماية if __name__ أهمية كبيرة: mcp dev، mcp run، mcp install واختباراتك كلها تستورد هذا الملف، وكان run() غير المحمي سيبدأ خادمًا في اللحظة التي يُحمَّل فيها الملف.
أضف أداة
يقرأ SDK ثلاثة أشياء من دالتك. الاسم يصبح اسم الأداة، والسلسلة التوثيقية (docstring) تصبح الوصف الذي يراه النموذج، وتلميحات الأنواع تصبح مخطط الوسائط. لا يوجد JSON Schema تكتبه، لأن a: int, b: intهو المخطط. إذا أرسل العميل نصًا حيث صرّحت بعدد صحيح، يرفض SDK الاستدعاء قبل أن تعمل دالتك.
أعطِ المعامل قيمة افتراضية ليصبح اختياريًا. ولحدود أدق، لُفّ النوع في Annotated مع Field من Pydantic:
from typing import Annotated, Literal
from pydantic import Field
@mcp.tool()
def search_books(
query: str,
limit: Annotated[int, Field(ge=1, le=50, description="Maximum results")] = 10,
genre: Literal["fiction", "non-fiction", "poetry"] = "fiction",
) -> str:
"""Search the catalog by title or author."""
return f"Found 3 books matching {query!r} (up to {limit})."
تظهر الحدود في المخطط بصيغة minimum و maximum، وتتحول Literal إلى قائمة اختيار (enum) يجب أن يختار النموذج منها.
أضف موردًا وأمرًا نصيًا
وجود {param} في URI المورد يحوّله إلى قالب مورد، لذلك greeting://{name} لا يملك مدخلًا واحدًا يُدرج حتى يزوّد أحد اسمًا. الأمر النصي أبسط: النص الذي يعيده يصبح رسالة مستخدم. يقرأ الاثنان وصفهما من السلسلة التوثيقية، تمامًا كالأدوات.
ارفع أخطاءً يستطيع النموذج قراءتها
عندما تفشل أداة، ارفعToolError. لا تُرجع أبدًا نصًا يحمل خطأ، لأن النص المُعاد يحمل is_error=False ويبدو كإجابة ناجحة.
from mcp.server.mcpserver.exceptions import ToolError
CATALOG = {"Dune": "Frank Herbert", "Neuromancer": "William Gibson"}
@mcp.tool()
def get_author(title: str) -> str:
"""Look up the author of a book in the catalog."""
if title not in CATALOG:
raise ToolError(f"No book titled {title!r} in the catalog.")
return CATALOG[title]
يقرأ النموذج الرسالة، ويدرك أنه خمّن العنوان بشكل خاطئ، ثم يستدعي من جديد بعنوان أفضل. استثناء raise واحد يمنحك وكيلًا يصحح نفسه. أي استثناء آخر يُعد انهيارًا: لا يرى النموذج سوى أن الاستدعاء فشل، ويحصل سجلك على تتبع الأخطاء (traceback).
💡 صرّح عن الأداة باستخدام async def كلما كانت تقوم بعمليات إدخال وإخراج، مثل استدعاء API أو قراءة ملف أو استعلام قاعدة بيانات. استخدم def العادي لكل ما عدا ذلك.
اختبره واربطه
تشغيل MCP Inspector
قبل أن يلمس أي عميل ذكاء اصطناعي خادمك، شغّله تحت Inspector:
uv run mcp dev server.py
افتح الرابط الذي يطبعه. يطلق Inspector server.py كعملية فرعية عبر stdio، تمامًا كما يفعل مضيف حقيقي. تصفح التبويبات بالترتيب:
الأدوات (Tools): يظهر add مع نموذج يُبنى من تلميحات الأنواع. استدعه باستخدام a=1 و b=2 فتحصل على 3.
الموارد (Resources): القائمة فارغة، وgreeting يظهر تحت قوالب الموارد. أعطِه World فتقرأ Hello, World!.
الأوامر النصية (Prompts):summarize يحمل معاملًا مطلوبًا واحدًا هو text، ويعيد رسالة مستخدم واحدة.
اكتب اختبارًا في الذاكرة
صنف Client في SDK يتصل أيضًا في الذاكرة: مرّر له كائن الخادم، فلا توجد عملية فرعية ولا منفذ. أضف pytest مع uv add --dev pytest، ثم أنشئ test_server.py:
import pytest
from mcp import Client
from server import mcp
@pytest.fixture
def anyio_backend():
return "asyncio"
@pytest.mark.anyio
async def test_add():
async with Client(mcp, raise_exceptions=True) as client:
result = await client.call_tool("add", {"a": 1, "b": 2})
assert result.structured_content == {"result": 3}
احتفظ بالخيار raise_exceptions=True للاختبارات فقط. فهو يُظهر رسالة الخطأ الحقيقية بدلًا من Internal server error المُنقَّحة التي يراها المستدعي عن بُعد.
ربط Claude Desktop مع Claude Code
يحتاج كل مضيف إلى الشيء نفسه: الأمر الذي يشغّل خادمك. هذا الأمر يعمل من أي مجلد، دون بيئة افتراضية تحتاج إلى تفعيلها:
uv run --with "mcp[cli]" mcp run /absolute/path/to/server.py
Claude Desktop هو المضيف الوحيد الذي يستطيع SDK إعداده نيابةً عنك:
uv run mcp install server.py
يكتب ذلك مدخلًا في claude_desktop_config.json، والملف موجود في ~/Library/Application Support/Claude/ على macOS وفي %APPDATA%\Claude\ على Windows. أغلق Claude Desktop بالكامل، لا النافذة فقط، ثم أعد فتحه. يشغّل التطبيق خادمك ببيئته الخاصة، لذلك مرّر الأسرار عبر -v NAME=value أو -f .env.
Claude Code لا يحتاج إلى ملف على الإطلاق. سجّل الخادم عبر CLI، ثم شغّل /mcp داخل جلسة للتأكد من أنه متصل:
claude mcp add image-studio -- uv run --with "mcp[cli]" mcp run /absolute/path/to/server.py
يقرأ Cursor .cursor/mcp.json تحت الحقل mcpServers، ويقرأ VS Code .vscode/mcp.json تحت servers مع "type": "stdio". الأمر داخله مطابق تمامًا.
إصلاح خادم لا يظهر
أولًا، شغّل أمر التشغيل بنفسك. الخادم السليم من نوع stdio لا يطبع شيئًا، ويبقى في انتظار أن يتحدث المضيف أولًا. تتبع الأخطاء أو الخروج الفوري هو العطل الحقيقي. إذا كان ينتظر بهدوء، فتحقق من هذه الأسباب الثلاثة:
الأعراض
السبب
الحل
الخادم لا يبدأ أبدًا
مسار نسبي، لأن المضيف يشغّل من مجلد عمله الخاص
استخدم مسارات مطلقة، بما فيها المسار إلى uv (where uv على Windows، و which uv في غيره)
التعديلات بلا أثر
المضيفات تقرأ إعدادها عند التشغيل
أغلق المضيف بالكامل وأعد فتحه
الاتصال ينقطع فورًا
شيء كتب على stdout، وهو قناة البروتوكول
سجّل عبر وحدة logging التي تكتب على stderr، ولا تعتمد أبدًا على print()
يحتفظ Claude Desktop بسجل واحد لكل خادم، اسمه mcp-server-<NAME>.log، في ~/Library/Logs/Claude على macOS وفي %APPDATA%\Claude\logs على Windows. هذا الملف هو stderr الخاص بخادمك.
ابنِ أداة صور حقيقية
يستحق الخادم وجوده عندما تنجز أداة عملًا لا يستطيع النموذج إنجازه وحده. هذه الأداة تأخذ أمرًا نصيًا، وتطلب من PicassoIA API صورة، ثم تعيد الرابط.
صمّم عقد الأداة
الواجهة من نمط Replicate: تنشئ تنبؤًا، ثم تستعلم عنه، ثم تقرأ المخرجات. هذه الحقائق تعتمد عليها الأداة:
التفصيل
القيمة
الرابط الأساسي
https://api.picassoia.com/v1
المصادقة
Authorization: Bearer pia_sk_...، يُنشأ من صفحة API
إنشاء مهمة
POST /v1/models/{owner}/{name}/predictions مع {"input": {"prompt": "..."}}
تنص وثائق API على أن خطة Infinite مطلوبة، وأن الطلب بدونها يعيد 403 plan_required. التنبؤات موصوفة بأنها مجانية ولا تستهلك أي نقاط. تحقق من خطتك قبل أن تبدأ تصحيح أي شيء آخر.
أبقِ العقد صغيرًا: أداة واحدة، ومعاملان، ورابط واحد يُعاد. كل مسار فشل يرفع ToolError، فيحصل النموذج دائمًا على رسالة قابلة للقراءة.
تعامل مع المهام غير المتزامنة دون حجب
لأن المهمة تعمل على GPU بعيد، يجب على الأداة أن تنتظر دون تجميد الخادم. لذلك تستخدم async def و httpx.AsyncClient و asyncio.sleep:
import asyncio
import os
from typing import Literal
import httpx
from mcp.server import MCPServer
from mcp.server.mcpserver.exceptions import ToolError
API = "https://api.picassoia.com/v1"
MODEL = "picassoia/picassoia-image"
DONE = ("succeeded", "failed", "canceled")
mcp = MCPServer("Image Studio")
slots = asyncio.Semaphore(4)
@mcp.tool()
async def generate_image(
prompt: str,
aspect_ratio: Literal["1:1", "16:9", "9:16", "4:3"] = "16:9",
) -> str:
"""Generate an image from a text prompt and return its URL."""
token = os.environ.get("PICASSOIA_API_TOKEN")
if not token:
raise ToolError("PICASSOIA_API_TOKEN is not set for this server.")
headers = {"Authorization": f"Bearer {token}"}
body = {"input": {"prompt": prompt, "aspect_ratio": aspect_ratio}}
async with slots, httpx.AsyncClient(headers=headers, timeout=30) as http:
response = await http.post(f"{API}/models/{MODEL}/predictions", json=body)
prediction = response.json()
if not response.is_success:
raise ToolError(f"{prediction.get('code')}: {prediction.get('detail')}")
while prediction["status"] not in DONE:
eta = prediction.get("eta") or {}
await asyncio.sleep(eta.get("next_poll_in_seconds", 2))
prediction = (await http.get(prediction["urls"]["get"])).json()
if prediction["status"] != "succeeded":
raise ToolError(prediction.get("error") or prediction["status"])
output = prediction["output"]
return output[0] if isinstance(output, list) else output
if __name__ == "__main__":
mcp.run()
ثلاثة تفاصيل تجعل هذا الكود آمنًا للتشغيل دون مراقبة:
اعتمد على جدول الخادم في الاستطلاع. الاستجابة تحمل eta.next_poll_in_seconds، لذا تنتظر بالمدة المحددة تمامًا التي تطلبها الواجهة البرمجية.
حدّد التزامن الذي تتحكم فيه بنفسك.Semaphore(4) يمنع نموذجًا واحدًا كثير الطلبات من استهلاك جميع 5 فتحات للحساب.
اقرأ التوكن من متغيرات البيئة. سجّله باستخدام mcp install server.py -v PICASSOIA_API_TOKEN=pia_sk_...، ولا تلصقه في الملف أبدًا.
💡 يمكن أن يكون حقل output قائمة روابط، أو رابطًا واحدًا، أو null. السطران الأخيران يعالجان الحالتين الأوليين، وفحص succeeded فوقهما يستبعد null عمليًا.
كتابة شيفرة الأداة هي المكان الذي يوفر فيه نموذج اللغة أكبر قدر من الوقت. يتعامل Claude Sonnet 5 على PicassoIA مع البرمجة متعددة الخطوات ومهام استخدام الأدوات، لذلك يمكنك لصق أداة generate_image التي تعمل ثم طلب الأداة التالية.
اكتب الأمر النصي. ألصق خادمك واطلب: أضف أداة ثانية تسرد تنبؤاتي الأخيرة باستخدام GET /v1/predictions. أعد استخدام معالجة الأخطاء نفسها. حقل prompt هو الحقل المطلوب الوحيد.
حدّد موجّه نظام لمنع مشكلة تغيير الاسم قبل أن تبدأ: أنت تكتب Python لأجل MCP SDK 2.x. استورد MCPServer من mcp.server ولا تستخدم FastMCP أبدًا.
اختر مستوى الجهد. الافتراضي low يتخطى التفكير الموسّع، وهو الأسرع. استخدم high أو max لخطأ يلمس عدة ملفات.
أبقِ الحد الأقصى للتوكنات عند 8192 للملفات الكاملة للخادم، أو خفّضه لمقتطف سريع.
أرفق لقطة شاشة لخطأ من Inspector إن وُجدت. النموذج يقرأ الصور، و max_image_resolution (الافتراضي 0.5 ميغابكسل) يصغّرها.
ولّد، وانسخ، واختبر. ألصق الناتج في server.py وشغّله عبر mcp dev قبل أن تثق به.
المعامل
مطلوب
الافتراضي
وظيفته
prompt
نعم
لا شيء
طلبك
system_prompt
لا
فارغ
يثبّت الدور والقيود للجلسة
effort
لا
low
عمق التفكير، من الأسرع إلى الأعمق
max_tokens
لا
8192
سقف طول المخرجات
image
لا
لا شيء
لقطة شاشة أو مخطط كسياق
تفضّل نموذجًا آخر؟ Kimi K2.6 ينتمي إلى الفئة نفسها، ويوصف بأنه مخصص لبناء الوكلاء وكتابة الشيفرة.
انشره عبر HTTP
تبديل وسيلة النقل
غيّر سطرًا واحدًا في أسفل server.py:
if __name__ == "__main__":
mcp.run(transport="streamable-http", port=3001)
يتصل العملاء الآن بالعنوان http://127.0.0.1:3001/mcp. يمكنك أيضًا ترك الملف كما هو وتشغيل uv run mcp run server.py --transport streamable-http. يقبل الاستدعاء run() هذه الخيارات:
host و port، والافتراضي هو 127.0.0.1 و 8000
streamable_http_path، والافتراضي هو /mcp
json_response=True للرد على كل طلب POST بجسم JSON واحد
stateless_http=True لنقل جديد لكل طلب
سجّل الخادم البعيد في Claude Code باستخدام claude mcp add --transport http image-studio https://mcp.example.com/mcp.
أحكم تأمينه
بمجرد أن يخرج خادمك من localhost، تتغير ثلاثة أشياء:
قائمة السماح للمضيف. الافتراضي يقبل 127.0.0.1 و localhost و [::1] فقط. خلف اسم مضيف حقيقي، يفشل كل طلب مع 421 Misdirected Request وInvalid Host header. أصلحه باستخدام transport_security= وأدرج كلًا من "mcp.example.com" و"mcp.example.com:*" في allowed_hosts.
التفويض. خادمك هو مورد OAuth 2.1. نفّذ TokenVerifier بطريقة واحدة async verify_token تعيد رمز وصول أو None، ومرّر token_verifier= مع auth=.
TLS خلف وكيل. عندما ينهي موازن الحمل TLS، شغّل uvicorn باستخدام --proxy-headers ليثق بالترويسات المُعاد توجيهها.
💡 421 هو استجابة HTTP عادية، وليس خطأ بروتوكول، لذلك لا يرى العميل سوى فشل نقل عام. يظهر اسم المضيف المخالف في سجل الخادم. خادم نُشر حديثًا ويرفض كل اتصال هو مشكلة في قائمة السماح للمضيف حتى يثبت العكس.
ابنِ صورك الخاصة مع Picasso IA
صار لديك الآن خادم يسجّل أدوات وموارد وأوامر نصية، ويجتاز اختبارًا في الذاكرة، ويعمل داخل Claude، ويمكن نشره خلف اسم مضيف حقيقي. الجزء الذي يستحق ساعتك التالية هو الأداة نفسها: بدّل معرّف النموذج، أو أضف أداة edit_image، أو اربط أداة فيديو بجانبها.
جرّب النموذج الذي استدعاه خادمك للتو. PicassoIA Image يحوّل الأمر النصي إلى صورة كاملة في ثوان، ويمكنك اختبار أي أمر نصي في المتصفح قبل أن تؤتمته. وعندما لا تكفي الصورة الثابتة، يحوّل PicassoIA Video و Seedance 2.5 Lite أمرًا نصيًا أو صورة فوتوغرافية إلى مقاطع قصيرة.
افتح Picasso IA، واختر نموذجًا، وشغّل الأمر النصي نفسه الذي كنت ستعطيه لأداتك. ثم اربطه بخادمك، ودع Claude يقوم بالنقر عنك.