أنظمة MCP الإنتاجية
المشروع النهائي: ابنِ خادم MCP خاصاً بـ GitHub
النتيجة: بنهاية هذا الدرس سيكون لديك خادم 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 على خادمك — مؤسساً على مستودعاتك، ليس هلوسة.
ولا أحد يخبره بهذا الترتيب. إنه يستنتج السلسلة من أوصاف أدواتك وحدها، ولهذا يؤتي إلحاح الوحدة الثانية على كتابتها بعناية ثمرته هنا:
طلب واحد، أربعة نداءات أدوات، دون تعليمات عن الترتيب
لاحظ ما لا تفعله search_code عمداً: إرجاع محتوى الملفات. فثلاثون ملفاً مطابقاً تُرجَع كاملة ستستنفد نافذة السياق قبل أن يبلغ النموذج خطوة القراءة. البحث يُرجع عناوين، وread_file تجلب ملفاً واحداً بالضبط. وهذا الفصل هو أهم قرار تصميمي منفرد في هذا الخادم.
البنية التي ستبنيها
خادم GitHub MCP — تدفق المكونات
الجزء 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>=2,<3",
"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 (انظر النسخة الإنجليزية أعلاه للكود الكامل — المحتوى متطابق عدا التعليقات).
النمط الأساسي:
import os
from typing import Literal
from dotenv import load_dotenv
from mcp.server import MCPServer
from mcp.shared.exceptions import MCPError
from github_client import GitHubClient
load_dotenv()
gh = GitHubClient(
token=os.environ["GITHUB_TOKEN"],
default_owner=os.environ.get("GITHUB_OWNER"),
)
mcp = MCPServer(name="github-mcp")
@mcp.tool()
async def search_code(repo: str, query: str, limit: int = 10) -> str:
"""البحث عن تطابقات نصية أو برمجية داخل مستودع GitHub.
استخدمها لتعرف أين تظهر المفاهيم أو الدوال أو العبارات.
و`repo` مُعرّف مثل 'my-blog' أو 'owner/repo'.
"""
results = await gh.search_code(repo=repo, query=query, limit=limit)
if not results:
return "لا توجد تطابقات."
return "\n".join(
f"- `{r['path']}` ({r['repo']}, score {r['score']:.1f}): {r['url']}"
for r in results
)
@mcp.tool()
async def read_file(repo: str, path: str, ref: str = "HEAD") -> str:
"""قراءة محتوى ملف كاملاً من مستودع GitHub.
استخدمها بعد search_code لجلب النص الفعلي.
ويمكن أن يكون `ref` فرعاً أو وسماً أو معرّف إيداع.
"""
return await gh.read_file(repo=repo, path=path, ref=ref)
@mcp.tool()
async def list_issues(
repo: str,
state: Literal["open", "closed", "all"] = "open",
limit: int = 20,
) -> str:
"""سرد المشكلات في مستودع GitHub."""
issues = await gh.list_issues(repo=repo, state=state, limit=limit)
return "\n".join(
f"#{i['number']} {i['title']} [{', '.join(i['labels'])}] — {i['url']}"
for i in issues
) or "لا توجد مشكلات."
@mcp.tool()
async def create_issue(repo: str, title: str, body: str) -> str:
"""إنشاء مشكلة جديدة على GitHub. يتطلب صلاحية كتابة على الرمز.
استخدمها فقط حين يطلب المستخدم صراحةً فتح مشكلة أو صياغة مهمة.
"""
created = await gh.create_issue(repo=repo, title=title, body=body)
return f"أُنشئت المشكلة #{created['number']}: {created['url']}"
if __name__ == "__main__":
mcp.run()
قارن هذا بالمخططات التي كنت ستكتبها يدوياً: يتحول Literal على state إلى enum، وتتحول
القيم الافتراضية إلى قيم افتراضية، وتتحول نصوص التوثيق إلى الأوصاف التي يقرأها النموذج. لم
يبقَ شيء يمكن أن يفترق بين المخطط والدالة، لأنه لم يعد هناك إلا واحد منهما.
بخصوص معالجة الأخطاء، لاحظ ما تغيّر عن مسودة سابقة لهذا الخادم. كانت تغلّف كل أداة داخل
try/except Exception واحدة تُرجع الخطأ كنص عادي. يبدو ذلك حذراً، وثمنه أن تفقد راية
isError: فلا يستطيع المضيف التمييز بين استدعاء فاشل وآخر ناجح تصادف أن إجابته تبدأ بكلمة
"خطأ". دع حالات الفشل الحقيقية تُطلق استثناءً — يضع الـ SDK علامة الخطأ على النتيجة ويسلّم
الرسالة إلى النموذج، وهو ما كنت تريده. والتقط فقط الاستثناءات التي تستطيع قول شيء مفيد عنها:
@mcp.tool()
async def read_file(repo: str, path: str, ref: str = "HEAD") -> str:
"""قراءة محتوى ملف كاملاً من مستودع GitHub."""
try:
return await gh.read_file(repo=repo, path=path, ref=ref)
except FileNotFoundError:
# رسالة قابلة للتصرف: أخبر النموذج كيف يتعافى بدل أن تفشل فحسب
raise MCPError(
code=-32602,
message=f"لا يوجد ملف في {path} على {ref}. شغّل search_code أولاً لإيجاد المسار الحقيقي.",
)
الأنماط الرئيسية من الوحدات السابقة:
- مزخرفات
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 عرضها على المستخدم. أضف واحداً لصياغة منشورات متابعة:
@mcp.prompt()
async def draft_followup_post(repo: str, original_path: str, angle: str) -> str:
"""صياغة تدوينة متابِعة استناداً إلى تدوينة قائمة في مستودع."""
return (
f"اقرأ التدوينة الموجودة في {original_path} داخل مستودع {repo} باستخدام أداة read_file. "
f"ثم اكتب تدوينة متابِعة تغطي: {angle}. "
f"حافظ على نبرة الأصل، وأبقِ الـ frontmatter متوافقاً مع YAML."
)
وسائط الموجَّه الثلاثة تأتي مباشرةً من توقيع الدالة، وكلها مطلوبة لأن أياً منها بلا قيمة افتراضية. ولاحظ أن الموجَّه لا يجلب التدوينة بنفسه — بل يوعز إلى النموذج باستخدام أداتك. هذا هو تقسيم العمل الجدير بالاستيعاب: الموجَّهات تُركّب الأدوات ولا تحل محلها.
الآن في Claude Desktop يمكن للمستخدم اختيار هذا الموجّه من الواجهة، ملء الحجج الثلاث، و Claude يفعل الباقي — قراءة الأصلي، توليد المتابعة — كله عبر خادمك.
الجزء 6 — تقوية الإنتاج (اختياري، 10 دقائق)
كل ما في الوحدة 5 دروس 1–3 ينطبق هنا:
- تحديد المعدل — طلبات GitHub REST الموثَّقة تخضع لحد أقصى ساعي أساسي، ولنقاط البحث حد ثانوي أضيق خاص بها. وبدل تثبيت رقم قد تغيّره GitHub، اقرأه من الاستجابة نفسها: كل رد يحمل
X-RateLimit-LimitوX-RateLimit-RemainingوX-RateLimit-Reset. أضف token bucket فيgithub_client.pyيتراجع حين يبلغX-RateLimit-Remainingصفراً ويستأنف بعد وقت إعادة الضبط. والأرقام الحالية موجودة في توثيق حدود المعدل لدى GitHub. - التسجيل — وجّه stderr stdio إلى ملف.
- تقليل النطاق — لـ MCP للقراءة فقط، استخدم PAT بنطاق
public_repoفقط. - التخزين المؤقت — خزّن نتائج
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 | انتظر فهرسة جديدة |
نقطة تحقق — أنجز هذا قبل ادعاء الشهادة
- شحن الخادم. Claude Desktop يُظهر 4 أدوات + موجّهاً واحداً عند النقر على رمز المطرقة.
- شحن استدعاء حقيقي. اسأل: "ابحث في
my-blogعن 'Kubernetes'." تأكد أن Claude يستدعيsearch_codeويُرجع مسارات ملفات فعلية. - شحن استدعاء متعدد الخطوات. اسأل: "اعثر على أقدم منشور حول MCP ولخّصه." يجب أن يسلسل Claude:
search_code→read_file→ تلخيص. - شحن مسار الكتابة. اطلب من Claude إنشاء مشكلة في مستودع تجريبي. تأكد من ظهور المشكلة على github.com.
- التقط Claude Desktop مع رمز المطرقة + استدعاء أداة ناجح. هذا إثبات عملك.
شحنت للتو خادم MCP يعطي Claude وصولاً منظماً ومصادقاً عليه إلى كودك. كل نمط من الوحدات الخمس السابقة يعمل الآن في الإنتاج.
التالي (اختياري): وسّع هذا الخادم بنقل ثانٍ (Streamable HTTP) ليعمل من دردشة مستضافة، ليس فقط Claude Desktop. :::
سجّل الدخول للتقييم