بناء عميل MCP بـ TypeScript: الاتصال بأي خادم أدوات ذكاء اصطناعي

Noqta Team
بواسطة Noqta Team ·

جاري تحميل مشغل تحويل النص إلى كلام الصوتي...

تعلّمت كيفية بناء خوادم MCP. الآن ابنِ الجانب الآخر. في هذا الدرس، ستنشئ عميل MCP بـ TypeScript يتصل بأي خادم MCP، يكتشف إمكانياته، ويستدعي الأدوات برمجياً — الأساس لبناء تطبيقاتك الخاصة المدعومة بالذكاء الاصطناعي.

ما ستتعلمه

في نهاية هذا الدرس، ستتمكن من:

  • فهم بنية عميل-خادم MCP من منظور العميل
  • إنشاء عميل MCP بـ TypeScript باستخدام @modelcontextprotocol/sdk
  • الاتصال بخوادم MCP عبر نقل stdio وSSE
  • اكتشاف واستدعاء الأدوات التي يوفرها أي خادم MCP
  • قراءة الموارد واستخدام القوالب من الخوادم
  • بناء عميل متعدد الخوادم يتصل بعدة خوادم في وقت واحد
  • إنشاء واجهة سطر أوامر تفاعلية لاستكشاف MCP

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

قبل البدء، تأكد من توفر:

  • Node.js 20+ مثبت (node --version)
  • معرفة بـ TypeScript (الأنواع، async/await، الوحدات)
  • محرر أكواد — يُنصح بـ VS Code أو Cursor
  • الإلمام بـ مفاهيم بروتوكول MCP (مفيد لكن غير مطلوب)
  • فهم أساسي لـ JSON-RPC

ما هو عميل MCP؟

في بنية بروتوكول سياق النموذج (Model Context Protocol)، العميل هو المكوّن الذي يتصل بخوادم MCP ويستهلك إمكانياتها. بينما تحتوي مضيفات MCP مثل Claude Desktop وCursor على عملاء مدمجين، يمكنك بناء عميلك الخاص من أجل:

  • دمج أدوات MCP في تطبيقاتك — روبوتات الدردشة، أدوات سطر الأوامر، خطوط الأتمتة
  • بناء سير عمل ذكاء اصطناعي مخصص ينسّق عدة خوادم MCP
  • اختبار وتصحيح خوادم MCP أثناء التطوير
  • إنشاء واجهات متخصصة مصممة لحالات استخدام محددة

نظرة على البنية

┌─────────────────────────────────────────────────┐
│  تطبيقك (مضيف MCP)                              │
│                                                 │
│  ┌───────────┐  ┌───────────┐  ┌───────────┐  │
│  │  عميل 1  │  │  عميل 2  │  │  عميل 3  │  │
│  └─────┬─────┘  └─────┬─────┘  └─────┬─────┘  │
└────────┼──────────────┼──────────────┼──────────┘
         │              │              │
    ┌────▼────┐   ┌─────▼────┐  ┌─────▼────┐
    │ خادم A │   │ خادم B  │  │ خادم C  │
    │(stdio) │   │  (SSE)  │  │ (stdio) │
    └─────────┘   └──────────┘  └──────────┘

كل عميل يحافظ على اتصال 1:1 مع خادم واحد. تطبيقك (المضيف) يدير عدة عملاء للاتصال بعدة خوادم.


الخطوة 1: إعداد المشروع

أنشئ مشروع TypeScript جديد لعميل MCP:

mkdir mcp-client && cd mcp-client
npm init -y

ثبّت الحزم المطلوبة:

npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx

إليك ما تفعله كل حزمة:

الحزمةالغرض
@modelcontextprotocol/sdkSDK الرسمي لـ MCP مع فئة Client
zodالتحقق من المخططات لمعاملات الأدوات
typescriptمترجم TypeScript
tsxتشغيل ملفات TypeScript مباشرة

هيّئ TypeScript:

npx tsc --init

حدّث ملف tsconfig.json:

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

أنشئ مجلد المصدر:

mkdir src

أضف السكربتات إلى package.json:

{
  "type": "module",
  "scripts": {
    "start": "tsx src/index.ts",
    "build": "tsc",
    "dev": "tsx watch src/index.ts"
  }
}

الخطوة 2: إنشاء عميل MCP أساسي

أنشئ ملف src/client.ts — غلاف العميل الأساسي:

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
 
export async function createMCPClient(
  serverCommand: string,
  serverArgs: string[] = [],
  env?: Record<string, string>
) {
  const transport = new StdioClientTransport({
    command: serverCommand,
    args: serverArgs,
    env: env ? { ...process.env, ...env } as Record<string, string> : undefined,
  });
 
  const client = new Client(
    {
      name: "my-mcp-client",
      version: "1.0.0",
    },
    {
      capabilities: {
        roots: { listChanged: true },
        sampling: {},
      },
    }
  );
 
  await client.connect(transport);
  return client;
}

لنشرح ما يحدث هنا:

  1. StdioClientTransport — يشغّل خادم MCP كعملية فرعية ويتواصل عبر stdin/stdout. هذا هو النقل الأكثر شيوعاً لخوادم MCP المحلية.

  2. Client — نسخة عميل MCP. المعامل الأول يحدد معلومات العميل (الاسم والإصدار). الثاني يعلن الإمكانيات التي يدعمها عميلك.

  3. client.connect(transport) — يؤسس الاتصال، ينفذ مصافحة MCP، ويتفاوض على الإمكانيات مع الخادم.


الخطوة 3: اكتشاف إمكانيات الخادم

بمجرد الاتصال، أول شيء يجب أن يفعله عميلك هو اكتشاف ما يقدمه الخادم. أنشئ ملف src/discover.ts:

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
 
export async function discoverCapabilities(client: Client) {
  const serverInfo = client.getServerVersion();
  console.log("متصل بالخادم:", serverInfo);
 
  const capabilities = client.getServerCapabilities();
  console.log("إمكانيات الخادم:", capabilities);
 
  if (capabilities?.tools) {
    const toolsResult = await client.listTools();
    console.log(`\nتم العثور على ${toolsResult.tools.length} أداة:`);
    for (const tool of toolsResult.tools) {
      console.log(`  - ${tool.name}: ${tool.description}`);
    }
  }
 
  if (capabilities?.resources) {
    const resourcesResult = await client.listResources();
    console.log(`\nتم العثور على ${resourcesResult.resources.length} مورد:`);
    for (const resource of resourcesResult.resources) {
      console.log(`  - ${resource.uri}: ${resource.name}`);
    }
  }
 
  if (capabilities?.prompts) {
    const promptsResult = await client.listPrompts();
    console.log(`\nتم العثور على ${promptsResult.prompts.length} قالب:`);
    for (const prompt of promptsResult.prompts) {
      console.log(`  - ${prompt.name}: ${prompt.description}`);
    }
  }
 
  return capabilities;
}

هذه الدالة تستعلم عن ثلاث فئات من ميزات الخادم:

  • الأدوات — الدوال التي يوفرها الخادم (مثل search_files، run_query، create_issue)
  • الموارد — مصادر البيانات التي يقدمها الخادم (مثل الملفات، سجلات قاعدة البيانات، استجابات API)
  • القوالب — قوالب prompt قابلة لإعادة الاستخدام مع معاملات

الخطوة 4: استدعاء الأدوات

الأدوات هي أقوى ميزة في MCP. تتيح لعميلك استدعاء دوال من جانب الخادم بمعاملات منظمة. أنشئ ملف src/tools.ts:

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
 
export async function callTool(
  client: Client,
  toolName: string,
  args: Record<string, unknown> = {}
) {
  const result = await client.callTool({
    name: toolName,
    arguments: args,
  });
 
  if (result.isError) {
    throw new Error(
      `فشلت الأداة "${toolName}": ${JSON.stringify(result.content)}`
    );
  }
 
  return result;
}
 
export async function listAndDescribeTools(client: Client) {
  const { tools } = await client.listTools();
 
  return tools.map((tool) => ({
    name: tool.name,
    description: tool.description,
    parameters: tool.inputSchema,
  }));
}

مثال: استدعاء أداة بحث الملفات

const result = await callTool(client, "search_files", {
  query: "authentication",
  path: "./src",
});
 
for (const content of result.content) {
  if (content.type === "text") {
    console.log(content.text);
  }
}

التعامل مع أنواع المحتوى المختلفة

أدوات MCP يمكنها إرجاع أنواع محتوى مختلفة. إليك كيفية التعامل معها:

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
 
export function processToolResult(result: Awaited<ReturnType<Client["callTool"]>>) {
  const outputs: string[] = [];
 
  for (const content of result.content) {
    switch (content.type) {
      case "text":
        outputs.push(content.text);
        break;
      case "image":
        outputs.push(`[صورة: ${content.mimeType}, ${content.data.length} بايت]`);
        break;
      case "resource":
        outputs.push(`[مورد: ${content.resource.uri}]`);
        break;
      default:
        outputs.push(`[نوع محتوى غير معروف]`);
    }
  }
 
  return outputs.join("\n");
}

الخطوة 5: قراءة الموارد

الموارد توفر وصولاً للقراءة فقط إلى البيانات التي يوفرها الخادم. أنشئ ملف src/resources.ts:

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
 
export async function readResource(client: Client, uri: string) {
  const result = await client.readResource({ uri });
 
  for (const content of result.contents) {
    if (content.text) {
      return content.text;
    }
    if (content.blob) {
      return `[بيانات ثنائية: ${content.mimeType}]`;
    }
  }
 
  return null;
}
 
export async function listAllResources(client: Client) {
  const resources: Array<{ uri: string; name: string; description?: string }> = [];
 
  const result = await client.listResources();
  resources.push(...result.resources);
 
  let cursor = result.nextCursor;
  while (cursor) {
    const nextPage = await client.listResources({ cursor });
    resources.push(...nextPage.resources);
    cursor = nextPage.nextCursor;
  }
 
  return resources;
}

قوالب الموارد

بعض الخوادم توفر قوالب موارد — عناوين URI بمعاملات تنشئ موارد ديناميكياً:

export async function listResourceTemplates(client: Client) {
  const result = await client.listResourceTemplates();
 
  for (const template of result.resourceTemplates) {
    console.log(`القالب: ${template.uriTemplate}`);
    console.log(`  الاسم: ${template.name}`);
    console.log(`  الوصف: ${template.description}`);
  }
 
  return result.resourceTemplates;
}

على سبيل المثال، خادم GitHub MCP قد يوفر قالباً مثل github://repos/{owner}/{repo}/issues/{number}. عميلك يملأ المعاملات لقراءة موارد محددة.


الخطوة 6: العمل مع القوالب (Prompts)

القوالب هي نماذج رسائل قابلة لإعادة الاستخدام يحددها الخادم للمهام الشائعة. أنشئ ملف src/prompts.ts:

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
 
export async function getPrompt(
  client: Client,
  promptName: string,
  args: Record<string, string> = {}
) {
  const result = await client.getPrompt({
    name: promptName,
    arguments: args,
  });
 
  console.log(`القالب: ${result.description}`);
 
  for (const message of result.messages) {
    console.log(`[${message.role}]:`);
    if (message.content.type === "text") {
      console.log(message.content.text);
    }
  }
 
  return result;
}

القوالب مفيدة للحصول على تعليمات جاهزة من الخادم. على سبيل المثال، خادم MCP لقاعدة بيانات قد يوفر قالب write_query يتضمن مخطط قاعدة البيانات وأفضل ممارسات SQL.


الخطوة 7: بناء واجهة سطر أوامر تفاعلية

الآن لنجمع كل شيء في واجهة سطر أوامر تفاعلية. أنشئ ملف src/index.ts:

import * as readline from "node:readline";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { createMCPClient } from "./client.js";
import { discoverCapabilities } from "./discover.js";
import { callTool, listAndDescribeTools, processToolResult } from "./tools.js";
import { readResource, listAllResources } from "./resources.js";
import { getPrompt } from "./prompts.js";
 
class MCPExplorer {
  private client: Client | null = null;
  private rl: readline.Interface;
 
  constructor() {
    this.rl = readline.createInterface({
      input: process.stdin,
      output: process.stdout,
    });
  }
 
  private prompt(question: string): Promise<string> {
    return new Promise((resolve) => {
      this.rl.question(question, resolve);
    });
  }
 
  async connect(command: string, args: string[] = []) {
    console.log(`جاري الاتصال بالخادم: ${command} ${args.join(" ")}`);
    this.client = await createMCPClient(command, args);
    await discoverCapabilities(this.client);
    console.log("\nتم إنشاء الاتصال. اكتب 'help' للأوامر.\n");
  }
 
  async run() {
    if (!this.client) {
      const serverCmd = process.argv[2];
      const serverArgs = process.argv.slice(3);
 
      if (!serverCmd) {
        console.log("الاستخدام: tsx src/index.ts <server-command> [args...]");
        console.log("مثال: tsx src/index.ts npx -y @modelcontextprotocol/server-everything");
        process.exit(1);
      }
 
      await this.connect(serverCmd, serverArgs);
    }
 
    while (true) {
      const input = await this.prompt("mcp> ");
      const [command, ...params] = input.trim().split(" ");
 
      try {
        await this.handleCommand(command, params);
      } catch (error) {
        console.error("خطأ:", (error as Error).message);
      }
    }
  }
 
  private async handleCommand(command: string, params: string[]) {
    if (!this.client) return;
 
    switch (command) {
      case "help":
        this.showHelp();
        break;
 
      case "tools":
        const tools = await listAndDescribeTools(this.client);
        for (const tool of tools) {
          console.log(`\n${tool.name}`);
          console.log(`  ${tool.description}`);
          if (tool.parameters) {
            console.log(`  المعاملات: ${JSON.stringify(tool.parameters, null, 2)}`);
          }
        }
        break;
 
      case "call": {
        const toolName = params[0];
        if (!toolName) {
          console.log("الاستخدام: call <tool-name> [json-args]");
          break;
        }
        const argsStr = params.slice(1).join(" ");
        const args = argsStr ? JSON.parse(argsStr) : {};
        const result = await callTool(this.client, toolName, args);
        console.log(processToolResult(result));
        break;
      }
 
      case "resources": {
        const resources = await listAllResources(this.client);
        for (const r of resources) {
          console.log(`  ${r.uri} — ${r.name}`);
        }
        break;
      }
 
      case "read": {
        const uri = params[0];
        if (!uri) {
          console.log("الاستخدام: read <resource-uri>");
          break;
        }
        const content = await readResource(this.client, uri);
        console.log(content);
        break;
      }
 
      case "prompts": {
        const promptsResult = await this.client.listPrompts();
        for (const p of promptsResult.prompts) {
          console.log(`  ${p.name}: ${p.description}`);
        }
        break;
      }
 
      case "prompt": {
        const promptName = params[0];
        if (!promptName) {
          console.log("الاستخدام: prompt <prompt-name> [json-args]");
          break;
        }
        const promptArgs = params.slice(1).join(" ");
        const parsedArgs = promptArgs ? JSON.parse(promptArgs) : {};
        await getPrompt(this.client, promptName, parsedArgs);
        break;
      }
 
      case "quit":
      case "exit":
        await this.client.close();
        this.rl.close();
        process.exit(0);
 
      default:
        console.log(`أمر غير معروف: ${command}. اكتب 'help' للخيارات.`);
    }
  }
 
  private showHelp() {
    console.log(`
الأوامر المتاحة:
  tools              عرض جميع الأدوات المتاحة
  call <name> [args] استدعاء أداة مع معاملات JSON اختيارية
  resources          عرض جميع الموارد المتاحة
  read <uri>         قراءة مورد بالعنوان
  prompts            عرض جميع القوالب المتاحة
  prompt <name>      الحصول على قالب مع معاملات JSON اختيارية
  help               عرض رسالة المساعدة
  quit               قطع الاتصال والخروج
    `);
  }
}
 
const explorer = new MCPExplorer();
explorer.run().catch(console.error);

الاختبار مع خادم Everything

يوفر مشروع MCP خادم اختبار يوفر أدوات وموارد وقوالب نموذجية:

npx tsx src/index.ts npx -y @modelcontextprotocol/server-everything

يجب أن ترى مخرجات مثل:

متصل بالخادم: { name: "everything", version: "1.0.0" }
إمكانيات الخادم: { tools: {}, resources: {}, prompts: {} }

تم العثور على 2 أداة:
  - echo: يعيد المدخل
  - add: يجمع رقمين

تم العثور على 2 مورد:
  - file:///example.txt: ملف نموذجي
  - file:///data.json: بيانات نموذجية

تم إنشاء الاتصال. اكتب 'help' للأوامر.

mcp> call echo {"message": "مرحبا MCP!"}
مرحبا MCP!

mcp> call add {"a": 5, "b": 3}
8

الخطوة 8: الاتصال عبر نقل SSE

بعض خوادم MCP تعمل كخدمات HTTP مستقلة وتستخدم Server-Sent Events (SSE) للتواصل. أنشئ ملف src/sse-client.ts:

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
 
export async function createSSEClient(serverUrl: string) {
  const transport = new SSEClientTransport(new URL(serverUrl));
 
  const client = new Client(
    {
      name: "my-mcp-client",
      version: "1.0.0",
    },
    {
      capabilities: {
        roots: { listChanged: true },
        sampling: {},
      },
    }
  );
 
  await client.connect(transport);
  return client;
}

متى تستخدم SSE مقابل Stdio

النقلحالة الاستخدامكيف يعمل
Stdioخوادم محلية، أدوات CLIالعميل يشغّل الخادم كعملية فرعية
SSEخوادم بعيدة، خدمات مشتركةالعميل يتصل بخادم HTTP قيد التشغيل

للخوادم البعيدة (كتلك التي تعمل في السحابة)، SSE هو الخيار الصحيح. للتطوير المحلي والأدوات الشخصية، stdio أبسط.


الخطوة 9: بناء عميل متعدد الخوادم

التطبيقات الواقعية غالباً تحتاج الاتصال بعدة خوادم MCP في وقت واحد. أنشئ ملف src/multi-client.ts:

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { createMCPClient } from "./client.js";
import { createSSEClient } from "./sse-client.js";
 
interface ServerConfig {
  name: string;
  transport: "stdio" | "sse";
  command?: string;
  args?: string[];
  url?: string;
  env?: Record<string, string>;
}
 
export class MultiServerClient {
  private clients = new Map<string, Client>();
 
  async addServer(config: ServerConfig) {
    let client: Client;
 
    if (config.transport === "stdio" && config.command) {
      client = await createMCPClient(
        config.command,
        config.args ?? [],
        config.env
      );
    } else if (config.transport === "sse" && config.url) {
      client = await createSSEClient(config.url);
    } else {
      throw new Error(`إعداد خادم غير صالح لـ "${config.name}"`);
    }
 
    this.clients.set(config.name, client);
    console.log(`تم الاتصال بـ "${config.name}"`);
    return client;
  }
 
  async listAllTools() {
    const allTools: Array<{
      server: string;
      name: string;
      description?: string;
    }> = [];
 
    for (const [serverName, client] of this.clients) {
      const capabilities = client.getServerCapabilities();
      if (!capabilities?.tools) continue;
 
      const { tools } = await client.listTools();
      for (const tool of tools) {
        allTools.push({
          server: serverName,
          name: tool.name,
          description: tool.description,
        });
      }
    }
 
    return allTools;
  }
 
  async callTool(
    serverName: string,
    toolName: string,
    args: Record<string, unknown> = {}
  ) {
    const client = this.clients.get(serverName);
    if (!client) {
      throw new Error(`الخادم "${serverName}" غير موجود`);
    }
 
    return client.callTool({ name: toolName, arguments: args });
  }
 
  async disconnectAll() {
    for (const [name, client] of this.clients) {
      await client.close();
      console.log(`تم قطع الاتصال من "${name}"`);
    }
    this.clients.clear();
  }
}

مثال الاستخدام

import { MultiServerClient } from "./multi-client.js";
 
const manager = new MultiServerClient();
 
await manager.addServer({
  name: "filesystem",
  transport: "stdio",
  command: "npx",
  args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
});
 
await manager.addServer({
  name: "github",
  transport: "stdio",
  command: "npx",
  args: ["-y", "@modelcontextprotocol/server-github"],
  env: { GITHUB_TOKEN: process.env.GITHUB_TOKEN ?? "" },
});
 
const allTools = await manager.listAllTools();
console.log("جميع الأدوات المتاحة عبر الخوادم:");
for (const tool of allTools) {
  console.log(`  [${tool.server}] ${tool.name}: ${tool.description}`);
}
 
const files = await manager.callTool("filesystem", "list_directory", {
  path: "/tmp",
});
 
await manager.disconnectAll();

الخطوة 10: التكامل مع نموذج لغوي كبير

القوة الحقيقية لعميل MCP تظهر عند ربطه بنموذج لغوي كبير (LLM). العميل يصبح الجسر بين نموذج الذكاء الاصطناعي والأدوات الخارجية. إليك مثال مبسط باستخدام Anthropic SDK:

import Anthropic from "@anthropic-ai/sdk";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
 
export async function runAgentLoop(
  anthropic: Anthropic,
  mcpClient: Client,
  userMessage: string
) {
  const { tools: mcpTools } = await mcpClient.listTools();
 
  const anthropicTools: Anthropic.Tool[] = mcpTools.map((tool) => ({
    name: tool.name,
    description: tool.description ?? "",
    input_schema: tool.inputSchema as Anthropic.Tool.InputSchema,
  }));
 
  const messages: Anthropic.MessageParam[] = [
    { role: "user", content: userMessage },
  ];
 
  let response = await anthropic.messages.create({
    model: "claude-sonnet-4-20250514",
    max_tokens: 4096,
    tools: anthropicTools,
    messages,
  });
 
  while (response.stop_reason === "tool_use") {
    const toolUseBlocks = response.content.filter(
      (block): block is Anthropic.ToolUseBlock => block.type === "tool_use"
    );
 
    const toolResults: Anthropic.ToolResultBlockParam[] = [];
 
    for (const toolUse of toolUseBlocks) {
      console.log(`استدعاء الأداة: ${toolUse.name}`);
      const result = await mcpClient.callTool({
        name: toolUse.name,
        arguments: toolUse.input as Record<string, unknown>,
      });
 
      const textContent = result.content
        .filter((c): c is { type: "text"; text: string } => c.type === "text")
        .map((c) => c.text)
        .join("\n");
 
      toolResults.push({
        type: "tool_result",
        tool_use_id: toolUse.id,
        content: textContent,
      });
    }
 
    messages.push({ role: "assistant", content: response.content });
    messages.push({ role: "user", content: toolResults });
 
    response = await anthropic.messages.create({
      model: "claude-sonnet-4-20250514",
      max_tokens: 4096,
      tools: anthropicTools,
      messages,
    });
  }
 
  const finalText = response.content
    .filter((block): block is Anthropic.TextBlock => block.type === "text")
    .map((block) => block.text)
    .join("\n");
 
  return finalText;
}

هذا ينشئ حلقة وكيل: النموذج اللغوي يتلقى رسالة المستخدم مع أدوات MCP المتاحة، يقرر أي أدوات يستدعي، عميلك ينفذها، والنتائج تعود للنموذج حتى ينتج إجابة نهائية.


الخطوة 11: معالجة الأخطاء وإعادة الاتصال

عملاء MCP الإنتاجيون يحتاجون معالجة أخطاء قوية. أنشئ ملف src/resilient-client.ts:

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { createMCPClient } from "./client.js";
 
export class ResilientMCPClient {
  private client: Client | null = null;
  private command: string;
  private args: string[];
  private maxRetries: number;
 
  constructor(command: string, args: string[] = [], maxRetries = 3) {
    this.command = command;
    this.args = args;
    this.maxRetries = maxRetries;
  }
 
  async connect(): Promise<Client> {
    for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
      try {
        this.client = await createMCPClient(this.command, this.args);
        return this.client;
      } catch (error) {
        console.error(`محاولة الاتصال ${attempt} فشلت:`, error);
        if (attempt === this.maxRetries) throw error;
        const delay = Math.min(1000 * Math.pow(2, attempt), 10000);
        await new Promise((r) => setTimeout(r, delay));
      }
    }
    throw new Error("فشل الاتصال بعد جميع المحاولات");
  }
 
  async callToolSafely(
    toolName: string,
    args: Record<string, unknown> = {}
  ) {
    if (!this.client) {
      await this.connect();
    }
 
    try {
      return await this.client!.callTool({ name: toolName, arguments: args });
    } catch (error) {
      console.error(`فشل استدعاء الأداة، جاري إعادة الاتصال...`);
      await this.connect();
      return this.client!.callTool({ name: toolName, arguments: args });
    }
  }
 
  async disconnect() {
    if (this.client) {
      await this.client.close();
      this.client = null;
    }
  }
}

أنماط معالجة الأخطاء الرئيسية

  1. التراجع الأسي — انتظر وقتاً أطول بين كل محاولة إعادة
  2. إعادة الاتصال التلقائي — إذا فشل استدعاء أداة بسبب اتصال مكسور، أعد الاتصال وحاول مرة أخرى
  3. الإغلاق السلس — أغلق العميل دائماً عند الانتهاء لتنظيف العمليات الفرعية

الخطوة 12: دعم ملف الإعدادات

عملاء MCP الحقيقيون يحمّلون إعدادات الخوادم من ملف، تماماً كما يستخدم Claude Desktop ملف claude_desktop_config.json. أنشئ ملف src/config.ts:

import { readFile } from "node:fs/promises";
import { MultiServerClient } from "./multi-client.js";
 
interface MCPConfig {
  mcpServers: Record<
    string,
    {
      command: string;
      args?: string[];
      env?: Record<string, string>;
    }
  >;
}
 
export async function loadFromConfig(configPath: string) {
  const raw = await readFile(configPath, "utf-8");
  const config: MCPConfig = JSON.parse(raw);
 
  const manager = new MultiServerClient();
 
  for (const [name, server] of Object.entries(config.mcpServers)) {
    await manager.addServer({
      name,
      transport: "stdio",
      command: server.command,
      args: server.args,
      env: server.env,
    });
  }
 
  return manager;
}

مثال ملف الإعدادات (mcp-config.json):

{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "/home/user/projects"]
    },
    "github": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-github"],
      "env": {
        "GITHUB_TOKEN": "ghp_your_token_here"
      }
    }
  }
}

هذا يطابق الصيغة التي يستخدمها Claude Desktop، مما يسهل مشاركة إعدادات الخوادم بين عميلك المخصص وClaude Desktop.


اختبار التطبيق

استخدام MCP Inspector

يوفر مشروع MCP أداة فحص للاختبار:

npx @modelcontextprotocol/inspector

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

كتابة اختبارات الوحدات

import { describe, it, expect } from "vitest";
import { createMCPClient } from "./client.js";
 
describe("MCP Client", () => {
  it("يجب أن يتصل بخادم everything", async () => {
    const client = await createMCPClient("npx", [
      "-y",
      "@modelcontextprotocol/server-everything",
    ]);
 
    const version = client.getServerVersion();
    expect(version).toBeDefined();
 
    const { tools } = await client.listTools();
    expect(tools.length).toBeGreaterThan(0);
 
    await client.close();
  });
 
  it("يجب أن يستدعي أداة echo", async () => {
    const client = await createMCPClient("npx", [
      "-y",
      "@modelcontextprotocol/server-everything",
    ]);
 
    const result = await client.callTool({
      name: "echo",
      arguments: { message: "اختبار" },
    });
 
    expect(result.content).toBeDefined();
    await client.close();
  });
});

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

المشاكل الشائعة

"Server process exited unexpectedly"

  • تحقق أن أمر الخادم يعمل بشكل مستقل: npx -y @modelcontextprotocol/server-everything
  • تأكد أن ملف الخادم مثبت وفي PATH
  • تأكد من تثبيت Node.js 20+

"Transport error: connection refused"

  • لنقل SSE، تحقق أن عنوان URL صحيح والخادم قيد التشغيل
  • تحقق من قواعد جدار الحماية التي تحظر منفذ الاتصال

"Tool not found"

  • شغّل listTools() لمعرفة أسماء الأدوات المتاحة
  • أسماء الأدوات حساسة لحالة الأحرف
  • قد يكون الخادم حدّث قائمة أدواته — أعد الفحص بعد إعادة الاتصال

"Invalid arguments"

  • تحقق من inputSchema الخاص بالأداة للمعاملات المطلوبة
  • تأكد أن أنواع المعاملات متطابقة (string مقابل number مقابل boolean)
  • استخدم JSON.stringify() لتصحيح المعاملات المرسلة

هيكل المشروع

إليك التخطيط النهائي للمشروع:

mcp-client/
├── src/
│   ├── index.ts           # نقطة دخول CLI
│   ├── client.ts          # مصنع عميل Stdio
│   ├── sse-client.ts      # مصنع عميل SSE
│   ├── multi-client.ts    # مدير متعدد الخوادم
│   ├── discover.ts        # اكتشاف الإمكانيات
│   ├── tools.ts           # أدوات استدعاء الوظائف
│   ├── resources.ts       # أدوات قراءة الموارد
│   ├── prompts.ts         # معالجة القوالب
│   ├── resilient-client.ts # غلاف معالجة الأخطاء
│   └── config.ts          # محمّل ملف الإعدادات
├── mcp-config.json        # إعداد الخوادم
├── package.json
└── tsconfig.json

الخطوات التالية

الآن بعد أن أصبح لديك عميل MCP يعمل، فكّر في:

  • إضافة مزيد من وسائل النقل — SDK الخاص بـ MCP يدعم أيضاً Streamable HTTP للنشر الحديث
  • بناء واجهة ويب — أنشئ واجهة Next.js تتصل بخوادم MCP عبر عميلك
  • التكامل مع تطبيق الذكاء الاصطناعي — استخدم نمط تكامل LLM من الخطوة 10 في روبوت الدردشة أو المساعد الخاص بك
  • إنشاء نظام إضافات — دع المستخدمين يعدّدون خوادم MCP للاتصال بها ديناميكياً
  • إضافة التسجيل والمقاييس — تتبع وقت استدعاء الأدوات ومعدلات الأخطاء وأنماط الاستخدام
  • استكشف درس خادم MCP لبناء خوادمك الخاصة

الخلاصة

لقد بنيت عميل MCP كامل الوظائف بـ TypeScript يمكنه:

  • الاتصال بأي خادم MCP عبر stdio أو SSE
  • اكتشاف واستدعاء الأدوات وقراءة الموارد واستخدام القوالب
  • إدارة الاتصالات بعدة خوادم في وقت واحد
  • معالجة الأخطاء مع إعادة الاتصال التلقائي
  • التكامل مع النماذج اللغوية لسير عمل استدعاء الأدوات
  • تحميل إعدادات الخوادم من ملفات

منظومة MCP تنمو بسرعة، مع نشر خوادم جديدة يومياً لقواعد البيانات وواجهات API والخدمات السحابية وأدوات المطورين. عميلك المخصص يمنحك تحكماً كاملاً في كيفية تفاعل تطبيقاتك مع هذه المنظومة — دون التقيد بأي تطبيق مضيف محدد.


هل تريد قراءة المزيد من الدروس التعليمية؟ تحقق من أحدث درس تعليمي لدينا على 9 أساسيات Laravel 11: قوالب Blade.

ناقش مشروعك معنا

نحن هنا للمساعدة في احتياجات تطوير الويب الخاصة بك. حدد موعدًا لمناقشة مشروعك وكيف يمكننا مساعدتك.

دعنا نجد أفضل الحلول لاحتياجاتك.

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

بناء وكلاء ذكاء اصطناعي جاهزين للإنتاج باستخدام Claude Agent SDK و TypeScript

تعلم كيف تبني وكلاء ذكاء اصطناعي مستقلين باستخدام Claude Agent SDK من Anthropic بلغة TypeScript. يغطي هذا الدرس العملي حلقة الوكيل، الأدوات المدمجة، أدوات MCP المخصصة، الوكلاء الفرعيين، أوضاع الصلاحيات، وأنماط النشر للإنتاج.

35 د قراءة·