يمثّل مرشح الإصدار MCP 2026-07-28 أكبر تحوّل معماري في تاريخ البروتوكول: انتهت عصر الجلسات. لا مزيد من مصافحة initialize، ولا رأس Mcp-Session-Id، ولا توجيه لاصق على موزّع الحمل.
يبني هذا البرنامج التعليمي خادم قاعدة معرفة MCP من الصفر باستخدام SDK البيتا من TypeScript. ستُنفّذ طبقة النقل عديمة الحالة، واكتشاف القدرات، والمهام غير المتزامنة، وطلبات الرحلات المتعددة لتأكيد المستخدم — الأنماط الأربعة التي تُعرّف البروتوكول الجديد.
المتطلبات الأساسية
قبل البدء، تأكد من توفّر:
- Node.js 20 أو أحدث (
node --version) - إلمام بـ TypeScript — async/await والأنواع العامة
- فهم أساسي لمفاهيم MCP — إذا كنت جديدًا، ابدأ بدليل خادم MCP بـ TypeScript أولًا
- محطة طرفية ومحرر كود
ما ستبنيه
خادم KnowledgeBase MCP يمكن لعملاء الذكاء الاصطناعي استخدامه لـ:
- search-docs — بحث نصي كامل في فهرس التوثيق
- get-article — استرداد مقالة واحدة عبر معرّفها
- create-article — كتابة مقالة جديدة مع طلب تأكيد المستخدم عبر طلب متعدد الرحلات
- export-collection — ضغط وتصدير مجموعة مفلترة بوسم كمهمة غير متزامنة
بنهاية الدرس، سيكون لديك خادم يعمل بشكل متطابق على أي نسخة، ويتوسع أفقيًا خلف موزّع حمل دوّار بسيط، ويعالج التصديرات الطويلة دون حجب اتصال HTTP.
الخطوة 1: إعداد المشروع
أنشئ المشروع وثبّت SDK البيتا:
mkdir kb-mcp-server && cd kb-mcp-server
pnpm init
pnpm add @modelcontextprotocol/server@beta zod
pnpm add -D typescript tsx @types/nodeجهّز TypeScript:
npx tsc --init \
--target ES2023 \
--module NodeNext \
--moduleResolution NodeNext \
--strictأضف "type": "module" في package.json ثم أنشئ مجلد المصدر:
mkdir src && touch src/index.tsأضف سكريبت التشغيل في package.json:
{
"scripts": {
"start": "tsx src/index.ts",
"build": "tsc"
}
}إصدار SDK: تستهدف هذه الأمثلة @modelcontextprotocol/server@2.0.0-beta.x. شغّل pnpm list @modelcontextprotocol/server للتحقق. إصدار v2 البيتا متوافق للخلف — خوادم v2 تستجيب بشكل صحيح لعملاء v1، لذا يمكنك الترحيل بالسرعة المناسبة لك.
الخطوة 2: خادم عديم الحالة بسيط
أوضح تغيير في v2 هو ما غاب: لا معالج initialize، ولا إنشاء جلسة، ولا Mcp-Session-Id لتتبّعه. كل استدعاء أداة مكتفٍ بذاته — معرّفات المقالات تأتي في جسم الطلب، والاستجابة تعود على نفس اتصال HTTP.
أنشئ src/index.ts:
import { McpServer } from "@modelcontextprotocol/server/mcp.js";
import { StreamableHttpServerTransport } from "@modelcontextprotocol/server/streamable-http.js";
import { z } from "zod";
import http from "node:http";
// مخزن في الذاكرة للتعليم. استبدله بقاعدة بيانات حقيقية في الإنتاج.
const articles = new Map([
["001", { title: "البدء مع MCP", content: "MCP بروتوكول...", tags: ["mcp", "intro"] }],
["002", { title: "أنماط التصميم عديمة الحالة", content: "الخدمات عديمة الحالة تُزيل...", tags: ["architecture"] }],
]);
const server = new McpServer({
name: "kb-server",
version: "2.0.0",
});
server.registerTool(
"search-docs",
{
description: "بحث نصي كامل في قاعدة المعرفة.",
inputSchema: z.object({
query: z.string().describe("مصطلحات البحث"),
limit: z.number().int().min(1).max(20).default(5),
}),
},
async ({ query, limit }) => {
const q = query.toLowerCase();
const results = [...articles.entries()]
.filter(([, a]) => a.title.toLowerCase().includes(q) || a.content.toLowerCase().includes(q))
.slice(0, limit)
.map(([id, a]) => ({ id, title: a.title, tags: a.tags }));
return {
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
};
}
);
server.registerTool(
"get-article",
{
description: "استرداد مقالة واحدة عبر معرّفها.",
inputSchema: z.object({
id: z.string().describe("معرّف المقالة"),
}),
},
async ({ id }) => {
const article = articles.get(id);
if (!article) {
return {
isError: true,
content: [{ type: "text", text: `المقالة ${id} غير موجودة` }],
};
}
return {
content: [{ type: "text", text: JSON.stringify({ id, ...article }, null, 2) }],
};
}
);الخطوة 3: طبقة النقل عديمة الحالة و server/discover
في v1، تُخبر مصافحة initialize العميلَ بقدرات الخادم. في v2 يحدث هذا التفاوض عند الطلب عبر طريقة server/discover الجديدة. يتعامل SDK البيتا مع التوجيه تلقائيًا — تحتاج فقط تفعيل stateless: true على طبقة النقل.
أضف مستمع HTTP إلى src/index.ts:
const transport = new StreamableHttpServerTransport({ stateless: true });
await server.connect(transport);
const httpServer = http.createServer(async (req, res) => {
// يوجّه النقل server/discover وtools/call وtasks/get وغيرها تلقائيًا.
await transport.handleRequest(req, res);
});
httpServer.listen(3000, () => {
console.log("خادم KB MCP → http://localhost:3000/mcp");
});إعداد stateless: true يُخبر طبقة النقل بتخطي تتبّع الجلسة كليًا. الطلبات بدون Mcp-Session-Id أصبحت المسار الطبيعي، لا الاحتياطي.
اختبره مع Inspector البيتا:
npx @modelcontextprotocol/inspector@beta http://localhost:3000/mcpفي تبويب الشبكة ستجد الأدوات مدرجة بعد استدعاء server/discover واحد فقط، دون أي مصافحة initialize قبلها.
الخطوة 4: بيانات العميل عبر _meta
في v1، كانت معلومات العميل (الاسم، الإصدار، القدرات) تصل مرة واحدة خلال initialize وتُخزَّن في حالة الجلسة. في v2 تنتقل في حقل _meta مع كل طلب.
يمكن الوصول إليها في أي معالج أداة من خلال المعامل الثاني:
server.registerTool(
"get-article",
{
description: "استرداد مقالة واحدة عبر معرّفها.",
inputSchema: z.object({ id: z.string() }),
},
async ({ id }, context) => {
const clientName = context.meta?.clientInfo?.name ?? "غير معروف";
const clientVersion = context.meta?.clientInfo?.version ?? "?";
console.log(`[${clientName}@${clientVersion}] get-article id=${id}`);
const article = articles.get(id);
if (!article) {
return { isError: true, content: [{ type: "text", text: `غير موجود: ${id}` }] };
}
return { content: [{ type: "text", text: JSON.stringify({ id, ...article }) }] };
}
);كل طلب قابل للتدقيق باستقلالية. يمكنك تحديد معدل الطلبات لكل عميل، وتسجيل نشاط كل عميل، وتطبيق حصص لكل عميل — دون أي مخزن جلسات مشترك.
الخطوة 5: امتداد المهام — التصدير غير المتزامن
قد تستغرق أداة export-collection عشرات الثواني للمجموعات الكبيرة. حجب اتصال HTTP لهذه المدة غير مقبول. يتيح لك امتداد المهام إعادة معرّف مهمة فورًا وترك العميل يستفسر عن الحالة.
يُوفّر SDK v2 مساعد TasksExtension لإدارة هذه الدورة:
import { TasksExtension } from "@modelcontextprotocol/server/extensions/tasks.js";
const tasks = new TasksExtension(server);
server.registerTool(
"export-collection",
{
description: "تصدير جميع المقالات المطابقة لوسم معين كملف JSON.",
inputSchema: z.object({
tag: z.string().describe("الوسم لتصفية المقالات"),
}),
},
async ({ tag }) => {
const task = tasks.create(async (taskCtx) => {
await taskCtx.updateStatus("running", "جمع المقالات...");
const matching = [...articles.entries()].filter(([, a]) => a.tags.includes(tag));
await taskCtx.updateStatus("running", `تم إيجاد ${matching.length} مقالة. جاري التعبئة...`);
// محاكاة عمل غير متزامن (استعلام قاعدة بيانات، ضغط، رفع، إلخ)
await new Promise((r) => setTimeout(r, 2000));
const payload = JSON.stringify(Object.fromEntries(matching), null, 2);
await taskCtx.complete({ data: payload, filename: `${tag}-export.json` });
});
// إعادة معرّف المهمة فورًا — العميل يستفسر عبر tasks/get
return { type: "task", taskId: task.id };
}
);يتلقّى العميل taskId في أقل من ميلي ثانية، ثم يستدعي tasks/get بالوتيرة المناسبة حتى تصبح الحالة completed.
tasks/list غائبة عمدًا من v2. بدون جلسات لا يوجد تعريف آمن لـ "جميع المهام الجارية لهذا العميل". مرّر taskId عبر متغيرات سياق وكيلك بدلًا من الاعتماد على تعداد المهام من جانب الخادم.
الخطوة 6: طلبات الرحلات المتعددة (MRTR)
كتابة مقالة جديدة إجراء ذو معنى — يُغيّر الحالة المشتركة ويستحق تأكيدًا. تُتيح MRTR للأداة إيقاف التنفيذ مؤقتًا، وعرض نص على المستخدم، ثم الاستئناف بإجابته.
أعد InputRequiredResult لإيقاف الأداة:
import { InputRequiredResult } from "@modelcontextprotocol/server/mcp.js";
server.registerTool(
"create-article",
{
description: "إنشاء مقالة جديدة في قاعدة المعرفة.",
inputSchema: z.object({
title: z.string(),
content: z.string(),
tags: z.array(z.string()),
confirmed: z.boolean().optional().describe("مرّر true بعد تأكيد المستخدم"),
}),
},
async ({ title, content, tags, confirmed }) => {
if (!confirmed) {
return new InputRequiredResult({
message: `إنشاء مقالة "${title}" بوسوم [${tags.join("، ")}]؟ أجب بنعم أو لا.`,
schema: z.object({ confirmed: z.boolean() }),
});
}
const id = String(articles.size + 1).padStart(3, "0");
articles.set(id, { title, content, tags });
return {
content: [{ type: "text", text: `تم إنشاء المقالة ${id}: "${title}"` }],
};
}
);عند إعادة الأداة لـ InputRequiredResult، يعرض عميل MCP النص للمستخدم. يُجيب المستخدم؛ يُعيد العميل الاستدعاء مع confirmed: true. لا حاجة لآلة حالة مخصصة من جانبك.
الخطوة 7: رؤوس البوابة للتوجيه الذكي
يسمح رأسا النقل الجديدان — Mcp-Method وMcp-Name — لبوابات API بتوجيه الطلبات حسب الطريقة واسم الأداة دون تحليل جسم JSON-RPC. مثال على قاعدة nginx:
upstream standard { server kb-mcp-1:3000; server kb-mcp-2:3000; server kb-mcp-3:3000; }
upstream heavy { server kb-mcp-heavy:3000; }
server {
listen 80;
location /mcp {
set $pool standard;
if ($http_mcp_name = "export-collection") {
set $pool heavy;
}
proxy_pass http://$pool;
}
}كود الأداة لا يتغير. يُصدر العملاء v2 هذه الرؤوس تلقائيًا عند التحدث مع خوادم v2.
الخطوة 8: ترحيل خادم v1 موجود
إذا كان لديك خادم MCP بـ v1، يتولّى الـ codemod الرسمي معظم عملية الترحيل:
npx @modelcontextprotocol/codemod@beta v1-to-v2 ./srcبعد الـ codemod، تحقق يدويًا من أمرين:
أكواد الخطأ — إذا كنت تُطابق كود الخطأ -32002 للموارد المفقودة، غيّره إلى -32602 (معامل JSON-RPC غير صالح).
معالجات Initialize — يجب نقل أي دوال ردّ onInitialize. القدرات التي كنت تُرسلها خلال initialize يجب إعادتها الآن استجابةً لـ server/discover. إذا كنت تُرسل فقط القائمة القياسية للقدرات، يمكنك حذف المعالج كليًا — SDK يملؤها تلقائيًا.
شغّل مجموعة الاختبارات بعد الـ codemod. تحتاج معظم المشاريع أقل من عشر دقائق من التنظيف اليدوي.
الخطوة 9: النشر في الإنتاج
التصميم عديم الحالة يجعل التوسع الأفقي سهلًا. مثال Docker Compose بثلاث نسخ مع توزيع دوّار بسيط:
version: "3.9"
services:
kb-mcp-1:
build: .
environment:
NODE_ENV: production
kb-mcp-2:
build: .
environment:
NODE_ENV: production
kb-mcp-3:
build: .
environment:
NODE_ENV: production
nginx:
image: nginx:alpine
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
ports:
- "80:80"upstream mcp_pool {
server kb-mcp-1:3000;
server kb-mcp-2:3000;
server kb-mcp-3:3000;
}
server {
listen 80;
location /mcp {
proxy_pass http://mcp_pool;
}
}لا مخزن جلسات. لا توجيه لاصق. لا طبقة تنسيق. يمكنك إيقاف أي نسخة لنشر متدرّج، والطلبات الجارية إما تُكمَل على النسخ المتبقية أو تفشل بسرعة وتُعاد المحاولة — حسب إعداد عميلك.
اختبار التنفيذ
شغّل الخادم:
pnpm startفي محطة ثانية، شغّل Inspector البيتا:
npx @modelcontextprotocol/inspector@beta http://localhost:3000/mcpتحقق من كل أداة في Inspector:
- search-docs — ابحث بـ
"stateless"وتحقق أن الاستجابة وصلت دون مصافحة جلسة في تبويب الشبكة. - export-collection — مرّر
tag: "mcp"وتأكد أنك تلقيتtaskIdفورًا. استفسر عبرtasks/getحتى تظهر الحالةcompleted. - create-article — أرسل بدون
confirmedوتحقق أنInputRequiredResultيعرض طلب التأكيد. أعد الاستدعاء معconfirmed: trueوتحقق ظهور المقالة فيsearch-docs.
استكشاف الأخطاء
رفض Mcp-Session-Id — عميل v1 يُرسل رؤوس الجلسة سيستمر في العمل؛ خادم v2 يتجاهل الرؤوس التي لا يعرفها. إذا رأيت أخطاء رفض، تحقق من بوابتك لا من SDK.
المهام غير موجودة — تأكد من إنشاء TasksExtension قبل أي استدعاء registerTool يستخدمه.
حلقة MRTR — إذا أعاد العميل الاستدعاء دون تمرير إجابة المستخدم، ستُعيد الأداة InputRequiredResult إلى الأبد. تحقق أن confirmed موجود في المخطط وأن عميلك يمرّره في الاستدعاء التالي.
الخلاصة
حوّلت مواصفة MCP 2026-07-28 إدارة الجلسات من مشكلتك إلى لا مشكلة. إزالة المصافحة أزاحت فئة كاملة من تعقيد الأنظمة الموزعة — التوجيه اللاصق، مخازن الجلسات المشتركة، الإغلاق الرشيق المعقد — واستبدلتها بدلالات HTTP البسيطة.
أبرز ما تعلّمته:
- طبقة النقل عديمة الحالة تعني أن أي نسخة تتعامل مع أي طلب — التوزيع الدوّار كافٍ
server/discoverيحلّ محلّ مصافحةinitializeللتفاوض على القدرات_metaينقل سياق العميل مع كل طلب بدلًا من الجلسةTasksExtensionيعالج العمل الطويل دون حجب اتصالات HTTPInputRequiredResultيُنفّذ تدفقات التأكيد دون آلة حالة مخصصة- الـ codemod الرسمي يغطي معظم ترحيل v1 إلى v2 في دقائق
المواصفة النهائية تصدر في 28 يوليو. SDK البيتا جاهز للإنتاج في خوادم جديدة اليوم.
الخطوات التالية
- اقرأ دليل بروتوكول MCP عديم الحالة 2026-07-28 للاطلاع على نظرة عامة كاملة للمواصفة
- استكشف بناء عميل MCP بـ TypeScript لفهم البروتوكول من جانب العميل
- ادرس أنماط MCP للمؤسسات للنشر متعدد المستأجرين