أنظمة MCP الإنتاجية

المشروع النهائي: ابنِ خادم MCP خاصاً بـ GitHub

50 دقيقة للقراءة

النتيجة: بنهاية هذا الدرس سيكون لديك خادم MCP يعمل في Claude Desktop يكشف مستودعات GitHub الخاصة بك كأدوات — يستطيع Claude البحث في كودك، قراءة أي ملف، سرد المشكلات المفتوحة، وصياغة طلبات pull، مؤسّساً على كودك الفعلي.

هذا المشروع النهائي يجمع كل الوحدات: مصافحة JSON-RPC (م1)، أساسيات الخادم (م2)، الأدوات والموارد (م3)، المصادقة (م4)، وتقوية الإنتاج (م5).

ما ستشحنه — هذا الموجّه سيعمل في Claude Desktop عند الانتهاء:

"في مستودع my-blog خاصتي، اعثر على كل منشور يذكر Kubernetes نُشر بعد 2026-03-01، اقرأ الأقدم، وصِغ منشور متابعة يتضمن تغييرات CSI 1.30 الجديدة."

سيستدعي Claude search_code، read_file، و create_draft_post على خادمك — مؤسساً على مستودعاتك، ليس هلوسة.


الجزء 1 — إعداد المشروع (5 دقائق)

github-mcp/
├── server.py
├── github_client.py
├── pyproject.toml
├── .env.example
└── README.md

pyproject.toml:

[project]
name = "github-mcp"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
  "mcp>=1.1.0",
  "httpx>=0.27",
  "python-dotenv>=1.0",
  "pydantic>=2",
]

.env.example:

GITHUB_TOKEN=ghp_...
GITHUB_OWNER=your-username

أنشئ رمز وصول شخصي GitHub بصلاحية repo. الصقه في .env.

pip install -e .

الجزء 2 — عميل GitHub (10 دقائق)

REST API لـ GitHub بسيط. لا حاجة لـ SDK؛ httpx يكفي.

github_client.py:

import os, httpx, base64
from typing import Any

GITHUB_API = "https://api.github.com"


class GitHubClient:
    def __init__(self, token: str, default_owner: str | None = None):
        self._token = token
        self._default_owner = default_owner

    def _headers(self):
        return {
            "Authorization": f"Bearer {self._token}",
            "Accept": "application/vnd.github+json",
            "X-GitHub-Api-Version": "2022-11-28",
        }

    async def search_code(self, repo: str, query: str, limit: int = 10):
        q = f"{query} repo:{self._resolve_repo(repo)}"
        async with httpx.AsyncClient(timeout=15) as c:
            r = await c.get(f"{GITHUB_API}/search/code",
                            params={"q": q, "per_page": limit},
                            headers=self._headers())
            r.raise_for_status()
            return [{
                "path": i["path"],
                "repo": i["repository"]["full_name"],
                "url": i["html_url"],
                "score": i["score"],
            } for i in r.json().get("items", [])]

    async def read_file(self, repo: str, path: str, ref: str = "HEAD") -> str:
        repo = self._resolve_repo(repo)
        async with httpx.AsyncClient(timeout=15) as c:
            r = await c.get(f"{GITHUB_API}/repos/{repo}/contents/{path}",
                            params={"ref": ref}, headers=self._headers())
            r.raise_for_status()
            return base64.b64decode(r.json()["content"]).decode("utf-8", errors="replace")

    async def list_issues(self, repo: str, state: str = "open", limit: int = 20):
        repo = self._resolve_repo(repo)
        async with httpx.AsyncClient(timeout=15) as c:
            r = await c.get(f"{GITHUB_API}/repos/{repo}/issues",
                            params={"state": state, "per_page": limit},
                            headers=self._headers())
            r.raise_for_status()
            return [{
                "number": i["number"], "title": i["title"],
                "body": (i.get("body") or "")[:1000],
                "labels": [l["name"] for l in i.get("labels", [])],
                "url": i["html_url"],
            } for i in r.json() if "pull_request" not in i]

    async def create_issue(self, repo: str, title: str, body: str):
        repo = self._resolve_repo(repo)
        async with httpx.AsyncClient(timeout=15) as c:
            r = await c.post(f"{GITHUB_API}/repos/{repo}/issues",
                             json={"title": title, "body": body},
                             headers=self._headers())
            r.raise_for_status()
            i = r.json()
            return {"number": i["number"], "url": i["html_url"]}

    def _resolve_repo(self, repo: str) -> str:
        if "/" in repo:
            return repo
        if not self._default_owner:
            raise ValueError(f"Repo '{repo}' بلا مالك و GITHUB_OWNER غير محدد")
        return f"{self._default_owner}/{repo}"

لماذا لا PyGithub؟ لسببين: (1) خادم MCP يعمل في عملية stdio لـ Claude Desktop، والاعتمادات الأخف = بدء أسرع؛ (2) سطح REST API الذي نحتاجه هو 4 نقاط نهاية — إضافة SDK بحجم 200KB لذلك مبالغة.


الجزء 3 — خادم MCP (15 دقيقة)

server.py (انظر النسخة الإنجليزية أعلاه للكود الكامل — المحتوى متطابق عدا التعليقات).

النمط الأساسي:

@server.list_tools()
async def list_tools() -> list[Tool]:
    return [Tool(name="search_code", description="...", inputSchema={...}), ...]


@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
    try:
        if name == "search_code":
            results = await gh.search_code(...)
            return [TextContent(type="text", text="\n".join(...))]
        # ...
    except Exception as e:
        # أعد الأخطاء كمحتوى tool_result، ليس كاستثناءات Python تسقط الخادم
        return [TextContent(type="text", text=f"Error: {type(e).__name__}: {e}")]


async def main():
    async with stdio_server() as (read, write):
        await server.run(read, write, server.create_initialization_options())

الأنماط الرئيسية من الوحدات السابقة:

  • مزخرفات list_tools + call_tool (م2 درس 1)
  • شكل إرجاع TextContent (م2 درس 2)
  • الأخطاء تُعاد كـ tool_result، ليست مُثارة (م2 درس 3)
  • نقل stdio لـ Claude Desktop المحلي (م4 درس 1)

الجزء 4 — الربط بـ Claude Desktop (5 دقائق)

عدّل ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) أو ما يعادله على Windows/Linux:

{
  "mcpServers": {
    "github": {
      "command": "python",
      "args": ["/absolute/path/to/github-mcp/server.py"],
      "env": {
        "GITHUB_TOKEN": "ghp_...",
        "GITHUB_OWNER": "your-username"
      }
    }
  }
}

أعد تشغيل Claude Desktop. يجب أن ترى رمز المطرقة يشير إلى اتصال خادم MCP.

فحص سريع: اسأل Claude: "استخدم أداة list_issues على مستودع my-blog خاصتي." إذا أعاد مشكلاتك الفعلية، المصافحة + النقل يعملان.


الجزء 5 — أضف قالب موجّه (5 دقائق)

MCP يكشف أيضاً الموجّهات — قوالب قابلة لإعادة الاستخدام يمكن لـ Claude عرضها على المستخدم. أضف واحداً لصياغة منشورات متابعة:

@server.list_prompts()
async def list_prompts():
    return [
        Prompt(
            name="draft-followup-post",
            description="صِغ منشور متابعة مبنياً على منشور موجود في مستودع",
            arguments=[
                PromptArgument(name="repo", required=True),
                PromptArgument(name="original_path", required=True),
                PromptArgument(name="angle", required=True),
            ],
        ),
    ]

الآن في Claude Desktop يمكن للمستخدم اختيار هذا الموجّه من الواجهة، ملء الحجج الثلاث، و Claude يفعل الباقي — قراءة الأصلي، توليد المتابعة — كله عبر خادمك.


الجزء 6 — تقوية الإنتاج (اختياري، 10 دقائق)

كل ما في الوحدة 5 دروس 1–3 ينطبق هنا:

  1. تحديد المعدل — REST API لـ GitHub يسمح بـ 5000 طلب/ساعة مع PAT. لاستخدام أثقل، أضف token bucket بسيط.
  2. التسجيل — وجّه stderr stdio إلى ملف.
  3. تقليل النطاق — لـ MCP للقراءة فقط، استخدم PAT بنطاق public_repo فقط.
  4. التخزين المؤقت — خزّن نتائج read_file لنفس (repo, path, ref) لمدة 5 دقائق.

الجزء 7 — قائمة استكشاف الأخطاء

العرضأول شيء تتحقق منهالسبب المعتاد
Claude Desktop يُظهر 0 أدواتأعد تشغيل بعد تعديل التكوينخطأ صياغة JSON أو مسار خاطئ
401 Unauthorizedالرمز في env مقابل التكوينمنتهي أو بلا نطاق repo
404 Not Found على read_fileاسم المستودع + المالك الافتراضيGITHUB_OWNER غير معين
Claude يهلوس محتويات الملفسجلات stderr stdioالخادم سقط مبكراً
search_code لا يُرجع شيئاًفهرسة GitHubانتظر فهرسة جديدة

نقطة تحقق — أنجز هذا قبل ادعاء الشهادة

  1. شحن الخادم. Claude Desktop يُظهر 4 أدوات + موجّهاً واحداً عند النقر على رمز المطرقة.
  2. شحن استدعاء حقيقي. اسأل: "ابحث في my-blog عن 'Kubernetes'." تأكد أن Claude يستدعي search_code ويُرجع مسارات ملفات فعلية.
  3. شحن استدعاء متعدد الخطوات. اسأل: "اعثر على أقدم منشور حول MCP ولخّصه." يجب أن يسلسل Claude: search_coderead_file → تلخيص.
  4. شحن مسار الكتابة. اطلب من Claude إنشاء مشكلة في مستودع تجريبي. تأكد من ظهور المشكلة على github.com.
  5. التقط Claude Desktop مع رمز المطرقة + استدعاء أداة ناجح. هذا إثبات عملك.

شحنت للتو خادم MCP يعطي Claude وصولاً منظماً ومصادقاً عليه إلى كودك. كل نمط من الوحدات الخمس السابقة يعمل الآن في الإنتاج.

التالي (اختياري): وسّع هذا الخادم بنقل ثانٍ (Streamable HTTP) ليعمل من دردشة مستضافة، ليس فقط Claude Desktop. :::

اختبار

اختبار الوحدة 5: أنظمة MCP الإنتاجية

خذ الاختبار
نشرة أسبوعية مجانية

ابقَ على مسار النيرد

بريد واحد أسبوعياً — دورات، مقالات معمّقة، أدوات، وتجارب ذكاء اصطناعي.

بدون إزعاج. إلغاء الاشتراك في أي وقت.