شرح خادم MCP بلغة TypeScript: مثال SDK وقالب يمكنك نسخه
خادم MCP يعمل بلغة TypeScript، من npm install حتى Claude Code. انسخ قالب SDK، وسجّل أداة ومورداً وأمراً نصياً، واختر بين stdio وStreamable HTTP، واختبر في Inspector، ثم أضف أدوات للصور والفيديو تستدعي API حقيقية دون أن تعطّل العميل.
يمكنك ربط نموذج لغوي بكودك الخاص في نحو أربعين سطراً. يبني MCP server TypeScript tutorial هذا بالضبط: خادماً يعمل على SDK الرسمي، وقالب مشروع يمكنك نسخه، والنوعين الأهم من النقل، stdio للعملاء المحليين وStreamable HTTP للعملاء البعيدين. ستنتهي بأداة ومورد وأمر نصي، تختبرها في Inspector وتسجّلها في Claude Code. ثم نضيف أدوات الصور والفيديو، لأن هنا يتوقف خادم Model Context Protocol عن كونه عرضاً تجريبياً ويبدأ بإنجاز عمل حقيقي. تعمل كل المقتطفات على Node.js 20 أو أحدث مع حزمة @modelcontextprotocol/sdk.
ما الذي يفعله خادم MCP
Model Context Protocol (MCP) هو معيار مفتوح يتيح لعميل ذكاء اصطناعي، مثل Claude Code أو Claude Desktop أو وكيل داخل IDE، استدعاء دوال وقراءة بيانات موجودة داخل عمليتك. تنتقل الرسائل بصيغة JSON-RPC 2.0. يعلن خادمك ما يقدّمه، ويعرض العميل هذه القدرات، ويقرر النموذج متى يستخدمها. لا يتحدث كودك مع النموذج مباشرةً، بل يجيب على الطلبات، ولهذا يبقى الخادم صغيراً.
ثلاث لبنات أساسية
يتكون كل خادم MCP من مزيج من ثلاثة عناصر أولية:
العنصر
من يقرر استخدامه
الاستخدام المعتاد
الأداة
النموذج
الاستعلام من قاعدة بيانات، استدعاء API، توليد صورة
المورد
التطبيق أو المستخدم
عرض مستند أو ملف أو إعداد كسياق قابل للقراءة
الأمر النصي
المستخدم
قالب قابل لإعادة الاستخدام مثل "راجع طلب السحب هذا"
تقوم الأدوات بمعظم العمل عملياً. الأداة دالة مسمّاة لها مخطط إدخال مُحدَّد الأنواع ونتيجة نصية أو صورية. الموارد والأوامر النصية اختيارية، لكن إضافتها لا تكلّف شيئاً تقريباً بمجرد وجود الخادم.
العميل والخادم والنقل
النقل ليس سوى الأنبوب الذي تمر عبره رسائل JSON-RPC. الكائن McpServer نفسه يعمل عبر stdio أو HTTP، لذا فالبنية الصحيحة هي بناء الخادم في دالة واحدة وربط وسيلة النقل في ملف مدخل منفصل. يتبع القالب أدناه هذه القاعدة، مما يُبقي الاختبارات بسيطة ويتيح لك شحن النوعين من قاعدة كود واحدة.
إعداد مشروع TypeScript
تثبيت SDK وzod
أنشئ مجلداً وثبّت الاعتماديات. يستخدم SDK مكتبة zod لمخططات الإدخال: يحوّلها إلى JSON Schema للعميل، ويتحقق من كل وسيط وارد قبل تشغيل معالجك.
mcp-notes-server/
src/
index.ts stdio transport and startup
http.ts Streamable HTTP transport
server.ts buildServer() factory
package.json
tsconfig.json
💡 تنتهي استيرادات SDK بالامتداد .js، حتى في ملفات TypeScript. مع حلّ Node16 يتطلب المترجم امتدادات صريحة، ويظهر غياب أحدها وقت التشغيل على هيئة ERR_MODULE_NOT_FOUND.
بناء قالب الخادم
مصنع الخادم
ضع كل ما يقدّمه الخادم داخل src/server.ts. تسجّل هذه النسخة أداة واحدة تحفظ ملاحظة وأخرى تبحث عن ملاحظة:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
const notes = new Map<string, string>();
export function buildServer(): McpServer {
const server = new McpServer({ name: "notes-server", version: "1.0.0" });
server.registerTool(
"add_note",
{
title: "Add note",
description: "Save a short note under a unique id. Overwrites an existing id.",
inputSchema: {
id: z.string().min(1).describe("Unique id, for example 'standup-0612'"),
text: z.string().min(1).max(2000).describe("The note body"),
},
},
async ({ id, text }) => {
notes.set(id, text);
return { content: [{ type: "text", text: `Saved note ${id}` }] };
}
);
server.registerTool(
"get_note",
{
title: "Get note",
description: "Return the text of a saved note by id.",
inputSchema: { id: z.string().min(1).describe("The note id") },
},
async ({ id }) => {
const text = notes.get(id);
if (text === undefined) {
return { isError: true, content: [{ type: "text", text: `No note with id ${id}` }] };
}
return { content: [{ type: "text", text }] };
}
);
// resources and prompts go here (next section)
return server;
}
تفصيلتان أهم مما تبدوان. أولاً، الوصف هو ما يقرؤه النموذج ليقرر إن كان سيستدعي الأداة، لذا اكتبه كما تكتب توثيقاً لزميل. ثانياً، عند حدوث خطأ، أعِد isError: true مع رسالة مقروءة بدلاً من رمي استثناء. عندها يستطيع النموذج إعادة المحاولة بوسيط مصحَّح، أو شرح المشكلة للمستخدم.
إضافة مورد وأمر نصي
استبدل التعليق النائب بهذا:
server.registerResource(
"all-notes",
"notes://all",
{
title: "All notes",
description: "Every saved note as JSON",
mimeType: "application/json",
},
async (uri) => ({
contents: [{ uri: uri.href, text: JSON.stringify([...notes.entries()]) }],
})
);
server.registerPrompt(
"summarize-notes",
{
title: "Summarize notes",
description: "Ask for a short summary of the saved notes",
argsSchema: { tone: z.string().optional() },
},
({ tone }) => ({
messages: [
{
role: "user",
content: {
type: "text",
text: `Summarize my saved notes in a ${tone ?? "neutral"} tone.`,
},
},
],
})
);
يُشار إلى الموارد بعنوان URI (notes://all)، ويعرضها العملاء عادةً في قائمة اختيار ليتمكن المستخدم من إرفاقها كسياق. تظهر الأوامر النصية كأوامر بشرطة مائلة أو كعناصر في قائمة، حسب العميل.
دع نموذج البرمجة يساعد
بعد أن يعمل هذا القالب، يستطيع نموذج برمجة إضافة أدوات في دقائق. الصق src/server.ts في محادثة واطلب أداة جديدة تتبع النمط نفسه: المخطط أولاً، ثم isError عند الفشل. Claude Sonnet 5، و Kimi K2.6 و GPT 5.6 Sol كلها مصممة لعمل البرمجة ومتاحة على PicassoIA. اقرأ المخطط المولَّد قبل قبوله: النموذج سيجعل حقلاً مطلوباً اختيارياً دون تردد.
اختيار وسيلة النقل
stdio للعملاء المحليين
stdio هو أبسط وسيلة نقل. يُشغّل العميل خادمك كعملية فرعية ويتحدث معه عبر stdin وstdout. أنشئ src/index.ts:
#!/usr/bin/env node
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { buildServer } from "./server.js";
const server = buildServer();
await server.connect(new StdioServerTransport());
console.error("notes-server ready on stdio");
💡 لا تستخدم console.log أبداً في خادم stdio. يحمل stdout البروتوكول، لذا يُفسد سطر سجل واحد التدفق كله ويفصل العميل مع خطأ تحليل. أرسل السجلات إلى stderr باستخدام console.error.
Streamable HTTP للعملاء البعيدين
للخادم الذي يعمل على مضيف بدلاً من حاسوب محمول، استخدم Streamable HTTP. وهو يحل محل النقل القديم HTTP plus SSE ويحتاج إلى نقطة نهاية واحدة. ثبّت Express بالأمر npm install express مع npm install -D @types/express، ثم احفظ هذا باسم src/http.ts:
import express from "express";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { buildServer } from "./server.js";
const app = express();
app.use(express.json());
app.post("/mcp", async (req, res) => {
const server = buildServer();
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
res.on("close", () => {
transport.close();
server.close();
});
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
});
app.listen(3000, () => console.error("MCP endpoint on http://localhost:3000/mcp"));
يؤدي ضبط sessionIdGenerator: undefined إلى تشغيل النقل في وضع عديم الحالة: خادم جديد لكل طلب، ولا توجد جلسة تحتاج إلى تتبّع، وقابلية توسّع أفقية كأي API آخر. إذا كنت تحتاج إلى إشعارات يبدأها الخادم أو تدفقات قابلة للاستئناف، فانتقل إلى الوضع ذي الحالة باستخدام معرّفات الجلسات.
stdio
Streamable HTTP
مكان التشغيل
عملية فرعية للعميل
أي مضيف يمكن الوصول إليه عبر HTTP
المصادقة
يرث بيئة المستخدم
تضيفها أنت (OAuth أو توكنات Bearer)
الأنسب في
أدوات المطورين الشخصية والمحلية
الخوادم المشتركة والمستضافة
التوسّع
عملية واحدة لكل عميل
عديم الحالة، يتوسّع كأي API
اختبر قبل الربط
تشغيل MCP Inspector
Inspector هو واجهة التصحيح الرسمية. ابنِ المشروع وشغّل خادمك من خلالها:
npm run build
npx @modelcontextprotocol/inspector node dist/index.js
افتح الرابط المحلي الذي يطبعه، واضغط Connect، ثم استخدم تبويب Tools لعرض الأدوات وتشغيل add_note مع وسيط JSON. يعرض لوح السجل حركة JSON-RPC الخام، وهي أسرع طريقة لاكتشاف مخطط لا يطابق ما قصدته.
التسجيل في Claude Code ثم Claude Desktop
يسجّل Claude Code خادماً محلياً بأمر واحد، وخادم HTTP بعلامة نقل:
claude mcp add notes -- node /absolute/path/mcp-notes-server/dist/index.js
claude mcp add --transport http notes-remote http://localhost:3000/mcp
أما Claude Desktop فيقرأ ملف JSON بدلاً من ذلك (claude_desktop_config.json):
💡 استخدم مسارات مطلقة في الحالتين. فالمسار النسبي يُحلّ مقابل مجلد عمل العميل لا مجلدك، ونادراً ما تذكر رسالة الفشل ذلك.
إضافة أدوات الصور والفيديو
يُثبت خادم الملاحظات النمط. وتُظهر أدوات الوسائط سبب جدواه، إذ تتضمن مهامًا بطيئة ومخرجات كبيرة و API خارجيًا له حدوده الخاصة. يتيح PicassoIA واجهة API للمطورين على نمط Replicate عبر https://api.picassoia.com/v1، وتتم المصادقة عليها باستخدام توكن Bearer يبدأ بالنص pia_sk_. يمكن الوصول إلى أربعة نماذج عبر API وموصّل MCP: PicassoIA Image لتحويل النص إلى صورة، وPicassoIA Image Editor Pro للتعديلات، و PicassoIA Video للفيديو، وأخيرًا Seedance 2.5 Lite للفيديو مع الصوت. المهام غير متزامنة: تنشئ تنبؤًا، ثم تستطلعه، ثم تقرأ النتيجة.
تغليف نقطة نهاية الصور
تخدم مساعدتان صغيرتان كل نموذج، لأن شكل API هو نفسه:
const API = "https://api.picassoia.com/v1";
const headers = {
Authorization: `Bearer ${process.env.PICASSOIA_API_TOKEN}`,
"Content-Type": "application/json",
};
export async function createPrediction(model: string, input: Record<string, unknown>) {
const res = await fetch(`${API}/models/${model}/predictions`, {
method: "POST",
headers,
body: JSON.stringify({ input }),
signal: AbortSignal.timeout(15_000),
});
if (!res.ok) throw new Error(`Create failed: HTTP ${res.status}`);
return (await res.json()) as { id: string };
}
export async function getPrediction(id: string) {
const res = await fetch(`${API}/predictions/${id}`, { headers });
if (!res.ok) throw new Error(`Status failed: HTTP ${res.status}`);
return (await res.json()) as { status: string; output?: unknown; error?: string };
}
تستخدم أداة الصور هذين المساعدين وتنتظر حتى دقيقتين للحصول على نتيجة:
server.registerTool(
"generate_image",
{
title: "Generate image",
description: "Create an image from a text prompt and return its URL.",
inputSchema: { prompt: z.string().min(10).max(4000) },
},
async ({ prompt }) => {
try {
const { id } = await createPrediction("picassoia/picassoia-image", { prompt });
for (let i = 0; i < 60; i++) {
const job = await getPrediction(id);
if (job.status === "succeeded") {
return { content: [{ type: "text", text: JSON.stringify(job.output) }] };
}
if (job.status === "failed") throw new Error(job.error ?? "Generation failed");
await new Promise((r) => setTimeout(r, 2000));
}
throw new Error("Timed out waiting for the image");
} catch (err) {
return { isError: true, content: [{ type: "text", text: String(err) }] };
}
}
);
يتطابق الحد الأقصى البالغ 4,000 حرف في المخطط مع حد الأوامر النصية في API، وتسرد كل صفحة نموذج حقول الإدخال الدقيقة وصيغة المخرجات. كما تحدّد API حدًّا قدره 5 تنبؤات متزامنة لكل حساب، مشتركة بين التوكنات واتصالات MCP، لذا ضع استدعاءات الأدوات المتوازية في قائمة انتظار بدلًا من إطلاقها كلها دفعة واحدة.
التعامل مع مهام الفيديو البطيئة
يستغرق الفيديو وقتاً أطول بكثير من الصورة، وقد يتجاوز استدعاء أداة يتعطّل لدقائق المهلةَ الخاصة بالعميل. قسّم العمل إلى أداتين: واحدة تبدأ المهمة وتعيد معرّفها فوراً، والأخرى تتحقق منها.
server.registerTool(
"start_video",
{
title: "Start video",
description: "Start a video job from a prompt. Returns a prediction id to check later.",
inputSchema: { prompt: z.string().min(10).max(4000) },
},
async ({ prompt }) => {
const { id } = await createPrediction("picassoia/picassoia-video", { prompt });
return { content: [{ type: "text", text: JSON.stringify({ predictionId: id }) }] };
}
);
server.registerTool(
"check_video",
{
title: "Check video",
description: "Return the status and output of a video job by prediction id.",
inputSchema: { predictionId: z.string().min(1) },
},
async ({ predictionId }) => {
const job = await getPrediction(predictionId);
return { content: [{ type: "text", text: JSON.stringify(job) }] };
}
);
يستدعي النموذج start_video، ويقوم بعمل آخر، ويستطلع check_video حتى تصبح الحالة succeeded. لا شيء يتعطل، والمهمة الفاشلة مجرد حالة أخرى تُبلَّغ. للفيديو مع الصوت، بدّل النموذج إلى picassoia/seedance-2.5-lite (Seedance 2.5 Lite)، فالمساعدان لا يتغيران.
الشحن بأمان
التحقق من المدخلات وحماية الأسرار
تعامل مع كل وسيط أداة على أنه غير موثوق. يمكن توجيه النموذج بنص يقرؤه على الويب أو في ملف، لذا قد تطلب صفحة مُحقَنة من أداتك فعل شيء لم تقصده. ثلاث عادات تقلل معظم المخاطر:
قيّد كل حقل في zod: min، max، enum، ثم regex للمعرّفات.
لا تمرر الوسائط أبداً إلى أمر شل أو إلى نص SQL. استخدم استعلامات معلمة، واحصر مسارات الملفات في مجلد أساسي واحد.
اقرأ التوكنات من متغيرات البيئة، ولا تكتبها في الكود أبداً، ولا تُعِدها في نتيجة أداة.
بالنسبة لعملاء stdio، اضبط الأسرار في كتلة env من إعداد العميل. أما خوادم HTTP، فاطلب ترويسة Authorization وتحقق منها قبل تشغيل handleRequest.
إصلاح الأخطاء الثلاثة الشائعة
console.log على stdio. بدّله إلى console.error.
امتدادات .js مفقودة. استيراد مثل ./server يفشل وقت التشغيل تحت Node16.
أوصاف غامضة. أداة اسمها run مع الوصف "يفعل أشياء" لا تُختار أبداً، أو تُختار بوسائط خاطئة. سمِّها باسم الفعل ووضّح متى تستخدمها.
للنشر، أبقِ سطر shebang في أعلى src/index.ts، ونفّذ npm run build، ثم npm publish. يستطيع أي شخص تسجيله بالأمر claude mcp add notes -- npx -y mcp-notes-server. تُشحن خوادم HTTP كحاوية، أو تعمل على أي مضيف Node.
جرّبه على Picasso IA
أصبح لديك الآن قالب يعمل محلياً، ويعمل عن بُعد، ويستطيع استدعاء نماذج الصور والفيديو. أسرع طريقة لرؤية ما تعيده هذه الأدوات هي تجربة النماذج يدوياً أولاً. افتح PicassoIA Image واكتب أمراً نصياً محدداً بقدر ما تكتبه حين ترسله من generate_image: الموضوع، والعدسة، والإضاءة، والمكان. ثم جرّب PicassoIA Video أو Seedance 2.5 Lite لتحريك الفكرة، ولاحظ أي صياغة تعطيك النتيجة التي تريدها قبل أن تثبّت القيم الافتراضية في خادمك. اختر أي نموذج من الكتالوج الكامل عبر picassoia.com/en/all-models وأوصله بالمساعدين أنفسهما. أنشئ صورك الخاصة على Picasso IA اليوم، ودع أول استدعاء لأداة MCP يسلّمك النتيجة.