كيف تبني خادم MCP من الصفر بلغة Python، خطوة بخطوة

خادم MCP بلغة Python يعمل، من مجلد فارغ إلى Claude Desktop. اكتب أدوات وموارد وأوامر نصية باستخدام SDK الرسمي 2.x، واختبرها في Inspector، وأضف أداة صور حقيقية مع استدعاءات API غير متزامنة، وانشره عبر Streamable HTTP.

كيف تبني خادم MCP من الصفر بلغة Python، خطوة بخطوة
Cristian Da Conceicao
مؤسس Picasso IA

تكتفي معظم دروس 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": "..."}}
النموذج المستخدم هناPicassoIA Image، المعرّف slug picassoia/picassoia-image
طول الأمر النصيمن 1 إلى 4,000 حرف
قيم الحالةstarting، processing، succeeded، failed، canceled
التزامن5 تنبؤات لكل حساب، مشتركة بين كل توكن واتصال MCP

تنص وثائق API على أن خطة Infinite مطلوبة، وأن الطلب بدونها يعيد 403 plan_required. التنبؤات موصوفة بأنها مجانية ولا تستهلك أي نقاط. تحقق من خطتك قبل أن تبدأ تصحيح أي شيء آخر.

امرأة ترسم مخطط تدفق API على لوح أبيض في عليّة مشمسة

أبقِ العقد صغيرًا: أداة واحدة، ومعاملان، ورابط واحد يُعاد. كل مسار فشل يرفع 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()

قلم يشير إلى ملاحظات استجابة API بجوار حاسوب محمول

ثلاثة تفاصيل تجعل هذا الكود آمنًا للتشغيل دون مراقبة:

  1. اعتمد على جدول الخادم في الاستطلاع. الاستجابة تحمل eta.next_poll_in_seconds، لذا تنتظر بالمدة المحددة تمامًا التي تطلبها الواجهة البرمجية.
  2. حدّد التزامن الذي تتحكم فيه بنفسك. Semaphore(4) يمنع نموذجًا واحدًا كثير الطلبات من استهلاك جميع 5 فتحات للحساب.
  3. اقرأ التوكن من متغيرات البيئة. سجّله باستخدام mcp install server.py -v PICASSOIA_API_TOKEN=pia_sk_...، ولا تلصقه في الملف أبدًا.

💡 يمكن أن يكون حقل output قائمة روابط، أو رابطًا واحدًا، أو null. السطران الأخيران يعالجان الحالتين الأوليين، وفحص succeeded فوقهما يستبعد null عمليًا.

كيفية استخدام Sonnet 5 على PicassoIA

كتابة شيفرة الأداة هي المكان الذي يوفر فيه نموذج اللغة أكبر قدر من الوقت. يتعامل Claude Sonnet 5 على PicassoIA مع البرمجة متعددة الخطوات ومهام استخدام الأدوات، لذلك يمكنك لصق أداة generate_image التي تعمل ثم طلب الأداة التالية.

  1. افتح صفحة النموذج Claude Sonnet 5.
  2. اكتب الأمر النصي. ألصق خادمك واطلب: أضف أداة ثانية تسرد تنبؤاتي الأخيرة باستخدام GET /v1/predictions. أعد استخدام معالجة الأخطاء نفسها. حقل prompt هو الحقل المطلوب الوحيد.
  3. حدّد موجّه نظام لمنع مشكلة تغيير الاسم قبل أن تبدأ: أنت تكتب Python لأجل MCP SDK 2.x. استورد MCPServer من mcp.server ولا تستخدم FastMCP أبدًا.
  4. اختر مستوى الجهد. الافتراضي low يتخطى التفكير الموسّع، وهو الأسرع. استخدم high أو max لخطأ يلمس عدة ملفات.
  5. أبقِ الحد الأقصى للتوكنات عند 8192 للملفات الكاملة للخادم، أو خفّضه لمقتطف سريع.
  6. أرفق لقطة شاشة لخطأ من Inspector إن وُجدت. النموذج يقرأ الصور، و max_image_resolution (الافتراضي 0.5 ميغابكسل) يصغّرها.
  7. ولّد، وانسخ، واختبر. ألصق الناتج في 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 يقوم بالنقر عنك.

شارك هذا المقال

اختر لغتك

مقالات ذات صلة