شرح خادم MCP بلغة TypeScript: مثال SDK وقالب يمكنك نسخه

خادم MCP يعمل بلغة TypeScript، من npm install حتى Claude Code. انسخ قالب SDK، وسجّل أداة ومورداً وأمراً نصياً، واختر بين stdio وStreamable HTTP، واختبر في Inspector، ثم أضف أدوات للصور والفيديو تستدعي API حقيقية دون أن تعطّل العميل.

شرح خادم MCP بلغة TypeScript: مثال SDK وقالب يمكنك نسخه
Cristian Da Conceicao
مؤسس Picasso IA

يمكنك ربط نموذج لغوي بكودك الخاص في نحو أربعين سطراً. يبني MCP server TypeScript tutorial هذا بالضبط: خادماً يعمل على SDK الرسمي، وقالب مشروع يمكنك نسخه، والنوعين الأهم من النقل، stdio للعملاء المحليين وStreamable HTTP للعملاء البعيدين. ستنتهي بأداة ومورد وأمر نصي، تختبرها في Inspector وتسجّلها في Claude Code. ثم نضيف أدوات الصور والفيديو، لأن هنا يتوقف خادم Model Context Protocol عن كونه عرضاً تجريبياً ويبدأ بإنجاز عمل حقيقي. تعمل كل المقتطفات على Node.js 20 أو أحدث مع حزمة @modelcontextprotocol/sdk.

مطور يكتب TypeScript على حاسوب محمول على مكتب خشبي في ضوء صباحي ناعم

ما الذي يفعله خادم 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 للعميل، ويتحقق من كل وسيط وارد قبل تشغيل معالجك.

mkdir mcp-notes-server && cd mcp-notes-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node

ضبط package.json وtsconfig

حوّل المشروع إلى وحدات ES وأضف سكربت بناء:

{
  "type": "module",
  "bin": { "mcp-notes-server": "dist/index.js" },
  "files": ["dist"],
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js"
  }
}

ثم أضف tsconfig.json يتوافق مع طريقة حلّ Node للوحدات:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "dist",
    "rootDir": "src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*"]
}

بنية المجلدات بسيطة عمداً:

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 آخر. إذا كنت تحتاج إلى إشعارات يبدأها الخادم أو تدفقات قابلة للاستئناف، فانتقل إلى الوضع ذي الحالة باستخدام معرّفات الجلسات.

stdioStreamable 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):

{
  "mcpServers": {
    "notes": {
      "command": "node",
      "args": ["/absolute/path/mcp-notes-server/dist/index.js"]
    }
  }
}

💡 استخدم مسارات مطلقة في الحالتين. فالمسار النسبي يُحلّ مقابل مجلد عمل العميل لا مجلدك، ونادراً ما تذكر رسالة الفشل ذلك.

مطور عند مكتب قائم بشاشتين يركّز على تصحيح الأخطاء في طرفية

إضافة أدوات الصور والفيديو

يُثبت خادم الملاحظات النمط. وتُظهر أدوات الوسائط سبب جدواه، إذ تتضمن مهامًا بطيئة ومخرجات كبيرة و 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.

يد تُدخل رمز أمان عتادي من الفولاذ المصقول في حاسوب محمول

إصلاح الأخطاء الثلاثة الشائعة

  1. console.log على stdio. بدّله إلى console.error.
  2. امتدادات .js مفقودة. استيراد مثل ./server يفشل وقت التشغيل تحت Node16.
  3. أوصاف غامضة. أداة اسمها 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 يسلّمك النتيجة.

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

اختر لغتك

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