بناء عميل MCP في TypeScript: دليل تعليمي لعام

٣ يونيو ٢٠٢٦

Build an MCP Client in TypeScript: A 2026 Tutorial

لبناء عميل MCP في TypeScript، قم بتثبيت @modelcontextprotocol/sdk المستقر، وأنشئ Client، وقم بتوصيله بناقل (transport) مثل StdioClientTransport، ثم استدعِ listTools() و callTool() لاكتشاف واستدعاء أدوات الخادم. هذا البرنامج التعليمي يبني واحداً من البداية للنهاية.

ملخص

معظم البرامج التعليمية لبروتوكول سياق النموذج (Model Context Protocol) تبني خوادم. هذا البرنامج يبني العميل — الجانب الذي يتصل بالخادم، ويكتشف أدواته، ويستدعيها. ستقوم بإنشاء هيكل مشروع TypeScript، وكتابة خادم تجريبي صغير للاتصال به، ثم كتابة عميل يتصل عبر stdio، ويسرد الأدوات/الموارد/المطالبات، ويستدعي أداة ويقرأ كلاً من content و structuredContent الخاصين بها، ويعالج الأخطاء بشكل صحيح، ويتحدث إلى خادم بعيد عبر Streamable HTTP، ويجري اختباراً داخلياً (in-process) باستخدام InMemoryTransport. يستخدم المشروع الإصدار المستقر من @modelcontextprotocol/sdk الإصدار 1.29.01 على Node.js 24 LTS. هناك ملاحظة واحدة تستحق المعرفة مسبقاً: وثائق الفرع main لـ GitHub الخاصة بـ SDK تصف بالفعل إصداراً غير مُصدر 2.0 بنظام الحزم المنفصلة API، لذا تمت كتابة هذا الدليل بناءً على الإصدار الذي تقوم بتثبيته فعلياً عبر npm install. تم فحص أنواع كل كتلة برمجية وتشغيلها في 3 يونيو 2026. الميزانية الزمنية حوالي 30 دقيقة.

ما ستتعلمه

  • إنشاء هيكل مشروع Node 24 TypeScript لعميل MCP باستخدام SDK المستقر
  • كتابة خادم MCP بسيط للاتصال به، ليكون البرنامج التعليمي مكتفياً ذاتياً
  • توصيل Client بخادم محلي عبر StdioClientTransport
  • اكتشاف أدوات الخادم، وموارده، ومطالباته، مع ميزة تقسيم الصفحات (pagination)
  • استدعاء أداة وقراءة كل من content (للنموذج) و structuredContent (للكود الخاص بك)
  • قراءة الموارد وجلب قوالب المطالبات مع الوسائط (arguments)
  • معالجة سطحي الخطأ المتميزين بشكل صحيح: isError للأداة مقابل خطأ McpError الملقى
  • الاتصال بخادم بعيد عبر StreamableHTTPClientTransport، مع رمز حامل (bearer token)
  • اختبار عميل داخلياً باستخدام InMemoryTransport، دون الحاجة لعملية فرعية (subprocess)

المتطلبات الأساسية

  • Node.js 24 LTS أو أحدث. Node 24 هو خط LTS النشط حالياً، والمدعوم حتى أبريل 20282. تحقق باستخدام node --version.
  • معرفة عملية بـ TypeScript و async/await.
  • طرفية (terminal). لا حاجة لمفاتيح API ولا حساب LLM — هذا البرنامج التعليمي يدور حول ميكانيكا العميل، وليس ربط نموذج.

التقنيات المستخدمة: @modelcontextprotocol/sdk الإصدار 1.29.01، و zod الإصدار 4.4.33 لمخططات الخادم التجريبي، و tsx الإصدار 4.22.44 حتى نتمكن من تشغيل ملفات .ts مباشرة. يتطلب SDK إصدار Node 18 أو أحدث، ولكن Node 24 هو إصدار LTS المستهدف في عام 20262.

ملاحظة قبل التثبيت. توجد حزمة @modelcontextprotocol/client على npm، ولكن حتى وقت كتابة هذا التقرير، لم يتم نشرها إلا كمعاينة 2.0.0-alpha لتخطيط الحزم المنفصلة المستقبلي5. العميل المستقر والمدعوم يتم شحنه داخل @modelcontextprotocol/sdk تحت المسار الفرعي /client. دليل العميل لفرع main الخاص بـ SDK يوثق بالفعل نظام API الخاص بإصدار alpha6، لذا إذا قمت بنسخ الكود من هناك فلن يتطابق مع 1.29.0. كل شيء أدناه يستخدم الحزمة المستقرة.

الخطوة 1 — إنشاء هيكل المشروع

أنشئ مشروعاً، واضبطه على وحدات ES، وقم بتثبيت SDK بالإضافة إلى أدوات التطوير. قم بتثبيت الإصدارات المحددة باستخدام --save-exact حتى لا ينجرف أمر npm install لاحقاً إلى إصدار فرعي أعلى.

mkdir mcp-client-demo && cd mcp-client-demo
npm init -y
npm pkg set type=module
npm install --save-exact @modelcontextprotocol/sdk@1.29.0 zod@4.4.3
npm install --save-exact -D tsx@4.22.4 TypeScript@6.0.3 @types/node@24.12.4

أضف ملف tsconfig.json صارم. مطلوب دقة NodeNext لتصديرات المسارات الفرعية لـ SDK، و "types": ["node"] تجعل المتغير العالمي process يعمل تحت verbatimModuleSyntax.

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "verbatimModuleSyntax": true,
    "skipLibCheck": true,
    "lib": ["ES2022"],
    "types": ["node"],
    "outDir": "dist"
  },
  "include": ["src"]
}

أنشئ مجلد المصدر: mkdir src. هناك شيئان يجب تذكرهما مع NodeNext و ESM: محددات الاستيراد تشير إلى ملفات .js المجمعة لـ SDK (على سبيل المثال @modelcontextprotocol/sdk/client/index.js)، كما تحتاج استيراداتك النسبية أيضاً إلى امتداد .js. هذا الأمر يربك الجميع تقريباً في أول عميل MCP لهم.

الخطوة 2 — كتابة خادم للاتصال به

يحتاج العميل إلى شيء يتحدث إليه. بدلاً من الاعتماد على خادم خارجي، قم ببناء خادم بسيط ليعمل البرنامج التعليمي بالكامل دون اتصال بالإنترنت. إذا كنت تريد خادماً للإنتاج — OAuth، Streamable HTTP، النشر — فاتبع برنامج تعليمي لخادم MCP للإنتاج المصاحب؛ هنا نحتاج فقط إلى هدف يحتوي على أداة واحدة، ومورد واحد، ومطالبة واحدة.

أنشئ src/server.ts:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const server = new McpServer(
  { name: "demo-server", version: "1.0.0" },
  { instructions: "A demo server with a calculator, a config resource, and a prompt." },
);

server.registerTool(
  "add",
  {
    title: "Add two numbers",
    description: "Returns the sum of a and b.",
    inputSchema: { a: z.number(), b: z.number() },
    outputSchema: { sum: z.number() },
  },
  async ({ a, b }) => {
    const sum = a + b;
    return {
      content: [{ type: "text", text: `${a} + ${b} = ${sum}` }],
      structuredContent: { sum ,
    };
  },
);

server.registerResource(
  "app-config",
  "config://app",
  { title: "App config", mimeType: "application/json" },
  async (uri) => ({
    contents: [
      { uri: uri.href, mimeType: "application/json", text: JSON.stringify({ theme: "dark", locale: "en" }) },
    ],
  }),
);

server.registerPrompt(
  "summarize",
  { title: "Summarize text", argsSchema: { text: z.string() } },
  ({ text }) => ({
    messages: [
      { role: "user", content: { type: "text", text: `Summarize this in one sentence:\n\n${text}` } },
    ],
  }),
);

await server.connect(new StdioServerTransport());

خادم stdio يقرأ JSON-RPC من stdin ويكتب في stdout، لذا يجب ألا يستخدم أبداً console.log للكتابة في stdout — لأن ذلك سيؤدي إلى إفساد تدفق البروتوكول. استخدم stderr للتسجيل إذا كنت بحاجة لذلك.

الخطوة 3 — الاتصال عبر stdio

الآن العميل. يقوم StdioClientTransport بتشغيل الخادم كعملية تابعة ويتحدث JSON-RPC عبر stdin/stdout الخاص بها، وهو بالضبط الطريقة التي تطلق بها المضيفات المكتبية الخوادم المحلية. أنشئ src/client.ts:

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";

const client = new Client(
  { name: "demo-client", version: "1.0.0" },
  { capabilities: {} },
);

const transport = new StdioClientTransport({
  command: process.execPath,            // the current node binary
  args: ["--import", "tsx", "src/server.ts"],
});

await client.connect(transport);

console.log("Connected to:", client.getServerVersion());
console.log("Instructions:", client.getInstructions());

الوسيط الأول لـ Client هو هوية عميلك؛ والثاني يعلن عن القدرات (اتركه فارغاً الآن — نضيف sampling و elicitation فقط عندما يحتاج الخادم إلى معاودة الاتصال بالعميل). command: process.execPath هو المسار إلى ثنائي Node قيد التشغيل، و --import tsx يسمح لـ Node بتنفيذ خادم TypeScript دون خطوة بناء. client.getInstructions() تعيد سلسلة instructions الاختيارية التي أرسلها الخادم عند التهيئة — قم بدمجها في مطالبة النظام الخاصة بنموذجك عندما تقوم بربط LLM.

الخطوة 4 — اكتشاف الأدوات والموارد والمطالبات

الاكتشاف هو كيفية تعلم عميل MCP لما يمكن للخادم القيام به. تعيد listTools() صفحة من الأدوات و nextCursor اختيارياً؛ استمر في التكرار حتى يختفي المؤشر. تتبع listResources() و listPrompts() نفس الشكل. أضف إلى src/client.ts:

import type { Tool } from "@modelcontextprotocol/sdk/types.js";

const allTools: Tool[] = [];
let cursor: string | undefined;
do {
  const page = await client.listTools(cursor ? { cursor : {});
  allTools.push(...page.tools);
  cursor = page.nextCursor;
} while (cursor);

console.log("Tools:", allTools.map((t) => t.name));

تحمل كل Tool اسماً (name)، ووصفاً اختيارياً (description)، ومخطط إدخال (inputSchema) (JSON Schema). هذا المخطط هو ما تسلمه للنموذج ليعرف كيفية ملء الوسائط — إنه الجسر بين MCP وحلقة استخدام الأدوات في LLM، وهو موضوع برنامجنا التعليمي حول حلقة Claude الوكيل لاستخدام الأدوات.

الخطوة 5 — استدعاء أداة

تقوم callTool() باستدعاء أداة بالاسم مع كائن arguments. النتيجة تحتوي على حمولتين: content وهي قابلة للقراءة من قِبل البشر أو النموذج (نصوص، صور، موارد مدمجة)، و structuredContent وهي كائن JSON محدد النوع يمكن لتطبيقك استخدامه مباشرة عندما تعلن الأداة عن outputSchema. اقرأ أيهما يناسب استخدامك.

const result = await client.callTool({ name: "add", arguments: { a: 21, b: 21 } });

console.log("content:", result.content);                 // [{ type: 'text', text: '21 + 21 = 42' }]
console.log("structuredContent:", result.structuredContent); // { sum: 42 }

استخدم content عندما تقوم بتغذية النتيجة مرة أخرى إلى نموذج، و structuredContent عندما يحتاج الكود الخاص بك إلى القيمة. بالنسبة للآلة الحاسبة، هذا التمييز بسيط؛ أما بالنسبة لأداة تُرجع صفاً من حقول قاعدة البيانات، فهو الفرق بين تحليل سلسلة نصية وقراءة كائن.

الخطوة 6 — قراءة الموارد والمطالبات (Prompts)

الموارد هي بيانات للقراءة فقط — ملفات، إعدادات، مخططات (schemas) — يتم الوصول إليها عبر URI. تُرجع readResource() مصفوفة contents التي تكون عناصرها اتحاداً بين أشكال نصية وثنائية (blob)، لذا قم بالتضييق باستخدام فحص in قبل لمس .text:

const res = await client.readResource({ uri: "config://app" });
const first = res.contents[0];
if (first && "text" in first) {
  console.log("resource:", first.text);  // {"theme":"dark","locale":"en"}
}

المطالبات (Prompts) هي قوالب رسائل قابلة لإعادة الاستخدام. تأخذ getPrompt() اسماً بالإضافة إلى وسيطات وتُرجع messages جاهزة للإرسال:

const prompt = await client.getPrompt({
  name: "summarize",
  arguments: { text: "MCP standardizes tool access." },
});
console.log("prompt messages:", prompt.messages.length); // 1

الخطوة 7 — التعامل مع الأخطاء بالطريقة الصحيحة

هذا هو المكان الذي تخطئ فيه عملاء MCP غالباً، لأن SDK يحتوي على سطحين للخطأ ويتصرفان بشكل مختلف. فشل الأداة لا يؤدي إلى رمي استثناء (throw) — بل يتم حل callTool() مع isError: true والرسالة في content. يغطي ذلك اسم أداة غير معروف، وسيطات غير صالحة، واستثناء تم رميه داخل معالج الأداة؛ يلتقط الخادم الثلاثة جميعاً ويبلغ عنها كنتيجة للأداة.

const bad = await client.callTool({ name: "does-not-exist", arguments: {} });
if (bad.isError) {
console.log("tool error:", bad.content); // [{ type: 'text', text: 'MCP error -32602: Tool does-not-exist not found' }]
}

أما فشل الطلب — قراءة مورد مفقود، جلب مطالبة غير معروفة، انتهاء وقت الطلب، انقطاع الاتصال — فيؤدي إلى الرفض مع McpError يجب عليك استخدام try/catch معه. افحص err.code مقابل تعداد ErrorCode بدلاً من المطابقة مع نص الرسالة:

import { McpError } from "@modelcontextprotocol/sdk/types.js";

try {
  await client.readResource({ uri: "config://missing" });
} catch (err) {
  if (err instanceof McpError) {
    console.log(`McpError code=${err.code}: ${err.message}`);
  } else {
    throw err;
  }
}

القاعدة العامة: تحقق من result.isError بعد كل callTool()، وقم بلف عمليات قراءة الموارد/المطالبات (وأي استدعاء طويل الأمد) في try/catch لـ McpError. تستخدم الطلبات مهلة افتراضية مدتها 60 ثانية (DEFAULT_REQUEST_TIMEOUT_MSEC)؛ يمكنك تجاوزها لكل استدعاء باستخدام { timeout: 120_000 } في وسيط الخيارات، وسيؤدي انتهاء المهلة إلى الرفض مع McpError بكود هو ErrorCode.RequestTimeout6.

الخطوة 8 — الاتصال بخادم بعيد

عادةً ما تعمل خوادم الإنتاج عبر HTTP، وليس كعملية منبثقة. استبدل النقل بـ StreamableHTTPClientTransport ووجهه إلى نقطة نهاية /mcp الخاصة بالخادم — تظل جميع الاستدعاءات الأخرى (listTools، callTool، وما إلى ذلك) متطابقة.

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";

const client = new Client({ name: "demo-client", version: "1.0.0" }, { capabilities: {} });

const transport = new StreamableHTTPClientTransport(
  new URL("http://localhost:3000/mcp"),
);

await client.connect(transport);

بالنسبة للخادم الذي يتوقع رمز حامل (bearer token) يتم إدارته خارج SDK — مفتاح API أو رمز صادر عن بوابة — قم بإرفاق ترويسة Authorization من خلال خيار requestInit الخاص بالنقل، والذي يتم دمجه في كل عملية جلب (fetch) يقوم بها النقل:

const transport = new StreamableHTTPClientTransport(
  new URL("http://localhost:3000/mcp"),
  { requestInit: { headers: { Authorization: `Bearer ${process.env.MCP_TOKEN ?? ""}` } } },
);

يتم دعم OAuth التفاعلي الكامل — تدفق كود التفويض في المتصفح مع تسجيل العميل الديناميكي — من خلال خيار authProvider الخاص بالنقل، والذي يأخذ تطبيقاً لـ OAuthClientProvider (مثل tokens()، saveTokens()، redirectToAuthorization()، وغيرها). يغطي درس خادم MCP للإنتاج المصاحب جانب الخادم من تلك المصافحة.

التحقق

قم بتشغيل العميل. سيقوم بتشغيل الخادم التجريبي تلقائياً، لذا تحتاج فقط لتشغيل أمر واحد:

npx tsx src/client.ts

المخرجات المتوقعة:

Connected to: { name: 'demo-server', version: '1.0.0' }
Instructions: A demo server with a calculator, a config resource, and a prompt.
Tools: [ 'add' ]
content: [ { type: 'text', text: '21 + 21 = 42' } ]
structuredContent: { sum: 42 }
resource: {"theme":"dark","locale":"en"}
prompt messages: 1
tool error: [ { type: 'text', text: 'MCP error -32602: Tool does-not-exist not found' } ]
McpError code=-32602: ... Resource config://missing not found

تحقق من أنواع المشروع بالكامل باستخدام npx tsc --noEmit — يجب أن يمر بنجاح تحت الإعدادات الصارمة.

إضافة — اختبار عميل باستخدام InMemoryTransport

بالنسبة لاختبارات الوحدة، لا ترغب في تشغيل عملية فرعية. تُرجع InMemoryTransport.createLinkedPair() وسيلتي نقل متصلتين ببعضهما البعض، بحيث يمكن للعميل والخادم التحدث في نفس العملية. أنشئ src/inmemory.ts:

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { z } from "zod";

const server = new McpServer({ name: "test-server", version: "1.0.0" });
server.registerTool(
  "add",
  { inputSchema: { a: z.number(), b: z.number() } },
  async ({ a, b }) => ({ content: [{ type: "text", text: String(a + b) }] }),
);

const client = new Client({ name: "test-client", version: "1.0.0" });
const [clientSide, serverSide] = InMemoryTransport.createLinkedPair();
await Promise.all([server.connect(serverSide), client.connect(clientSide)]);

const result = await client.callTool({ name: "add", arguments: { a: 2, b: 3 } });
console.log(result.content); // [{ type: 'text', text: '5' }]

await client.close();

قم بتشغيله باستخدام npx tsx src/inmemory.ts. نظراً لعدم وجود عملية فرعية ولا مقبس (socket)، فإن هذا النمط سريع بما يكفي للتشغيل في CI مع كل عملية دفع كود (commit).

استكشاف الأخطاء وإصلاحها

  • Cannot find module '@modelcontextprotocol/client' — لقد قمت بنسخ كود من وثائق فرع main الخاص بـ SDK، والتي تصف حزم 2.0 المنفصلة التي لم يتم إصدارها بعد6. في الإصدار المستقر، استورد من @modelcontextprotocol/sdk/client/... بدلاً من ذلك5.
  • ERR_MODULE_NOT_FOUND أو "Cannot use import statement outside a module" — ملف package.json الخاص بك يفتقد إلى "type": "module"، أو أن عملية الاستيراد تفتقد إلى امتداد .js. تحت NodeNext، تحتاج كل من استيراداتك النسبية ومسارات SDK الفرعية إلى .js.
  • Cannot find name 'process' — أضف "types": ["node"] إلى tsconfig.json (وتأكد من تثبيت @types/node). لن يقوم verbatimModuleSyntax بجلب متغيرات Node العالمية بدون ذلك.
  • العميل يتوقف عند connect() عبر stdio — من المحتمل أن الخادم يكتب إلى stdout. يجب أن يحافظ خادم MCP الذي يعمل عبر stdio على نظافة stdout لـ JSON-RPC؛ انقل أي تسجيل (logging) إلى stderr.
  • callTool() "ينجح" ولكن لم يحدث شيء — لم تقم بالتحقق من isError. إخفاقات الأدوات يتم حلها ولا يتم رميها كاستثناءات. افحص result.isError و result.content بعد كل استدعاء.

الخطوات التالية ومزيد من القراءة

لديك الآن عميل يتصل عبر stdio و HTTP، ويكتشف قدرات الخادم، ويستدعي الأدوات، ويتعامل مع كلا سطحي الخطأ. من هنا:

يدعم SDK العميل أيضاً إشعارات التقدم للأدوات الطويلة، واشتراكات الموارد، وأخذ العينات والاستنباط بمبادرة من الخادم، ورموز استئناف SSE — وكلها موثقة في دليل عميل SDK، مع ملاحظة أنه يتبع الإصدار الأحدث API6. التزم بالإصدار المستقر 1.29.0 حتى تصل حزم 2.0 إلى إصدار مستقر. مراجعة البروتوكول الحالية التي يتفاوض عليها SDK هي 2025-11-257.

Footnotes

  1. @modelcontextprotocol/sdk على npm، الإصدار 1.29.0 (تم نشره في 2026-03-30). https://www.npmjs.com/package/@modelcontextprotocol/sdk 2

  2. إصدارات Node.js — دخل Node 24 مرحلة Active LTS في 2025-10-28 وهو مدعوم حتى 2028-04-30. https://nodejs.org/en/about/previous-releases 2

  3. zod على npm، الإصدار 4.4.3. https://www.npmjs.com/package/zod

  4. tsx على npm، الإصدار 4.22.4. https://www.npmjs.com/package/tsx

  5. @modelcontextprotocol/client على npm — تم نشره فقط كمعاينة 2.0.0-alpha اعتباراً من يونيو 2026. https://www.npmjs.com/package/@modelcontextprotocol/client 2

  6. دليل عميل MCP TypeScript SDK (فرع GitHub main)، والذي يوثق إصدار 2.0 API القادم بنظام الحزم المنفصلة. https://GitHub.com/modelcontextprotocol/TypeScript-sdk/blob/main/docs/client.md 2 3 4

  7. مواصفات Model Context Protocol، مراجعة 2025-11-25 (المستقرة الحالية). https://modelcontextprotocol.io/specification/2025-11-25


نشرة أسبوعية مجانية

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

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

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