18 دقائق قراءة

كيف تبني خادم MCP مخصص لتطبيق SaaS الخاص بك: مثال Todoist

دليل خطوة بخطوة لبناء أول خادم MCP بـ TypeScript، يربط Claude بـ Todoist API مع تسجيل الأدوات والتحقق من المدخلات ومعالجة الأخطاء.

دليل خادم MCP
Model Context Protocol
Todoist API
TypeScript
Claude
تكامل الذكاء الاصطناعي
بناء خادم MCP

كيف تبني خادم MCP مخصص لتطبيق SaaS الخاص بك: مثال Todoist

تريد من Claude أن يدير مهام Todoist الخاصة بك. ليس عبر النسخ واللصق أو إضافة متصفح، بل أصليًا: "أرني المهام المتأخرة"، "أنشئ مهمة لمراجعة تقرير الربع الثالث بحلول الجمعة"، "علّم ذلك كمكتمل". هذا الدليل يرشدك عبر بناء خادم MCP يحقق ذلك.

في النهاية، سيكون لديك خادم MCP يعمل بـ TypeScript مع ست أدوات تتيح لـ Claude عرض المشاريع وقراءة المهام وإنشاء وتحديث المهام وتعليمها كمكتملة وإضافة تعليقات. نفس الأنماط تنطبق على أي REST API، لذا بمجرد بناء هذا يمكنك ربط Claude بأي منصة SaaS عمليًا.


ما هو MCP؟

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

خادم MCP هو برنامج صغير:

  1. يعلن مجموعة من الأدوات (دوال يمكن للذكاء الاصطناعي استدعاؤها)
  2. يتواصل مع عميل الذكاء الاصطناعي عبر stdio (الإدخال/الإخراج القياسي)
  3. ينفذ استدعاءات API عندما يستدعي الذكاء الاصطناعي أداة ويعيد النتائج

عميل الذكاء الاصطناعي (Claude Desktop أو Claude Code أو Cursor) يكتشف أدواتك تلقائيًا ويقرر متى يستخدمها بناءً على محادثتك.


المتطلبات المسبقة

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


إعداد المشروع

أنشئ مجلدًا جديدًا وهيئ المشروع:

1mkdir mcp-todoist && cd mcp-todoist 2npm init -y

ثبت التبعيات:

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

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

  • @modelcontextprotocol/sdk هي SDK الرسمية لـ MCP بـ TypeScript. تتعامل مع البروتوكول وتسجيل الأدوات وطبقة النقل.
  • zod توفر التحقق من المدخلات أثناء التشغيل. كل أداة تحتاج مخططًا حتى يعرف Claude أي معاملات يرسل، وZod تتحقق منها قبل تشغيل كودك.
  • typescript و@types/node لفحص الأنماط وتعريفات أنماط Node.js.

أنشئ tsconfig.json:

1{ 2 "compilerOptions": { 3 "target": "ES2022", 4 "module": "Node16", 5 "moduleResolution": "Node16", 6 "outDir": "./dist", 7 "rootDir": "./src", 8 "strict": true, 9 "esModuleInterop": true, 10 "skipLibCheck": true 11 }, 12 "include": ["src"] 13}

حدّث package.json لإضافة سكريبت البناء وضبط نوع الوحدة:

1{ 2 "type": "module", 3 "scripts": { 4 "build": "tsc", 5 "start": "node dist/index.js" 6 } 7}

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

1mkdir src

هيكل الخادم

أنشئ src/index.ts مع هيكل خادم MCP الأساسي:

1import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 3import { z } from "zod"; 4 5// Todoist API configuration 6const TODOIST_API_BASE = "https://api.todoist.com/rest/v2"; 7const TODOIST_TOKEN = process.env.TODOIST_API_TOKEN; 8 9if (!TODOIST_TOKEN) { 10 process.stderr.write("TODOIST_API_TOKEN environment variable is required\n"); 11 process.exit(1); 12} 13 14// Helper for authenticated Todoist requests 15async function todoistRequest( 16 path: string, 17 options: RequestInit = {} 18): Promise<any> { 19 const url = `${TODOIST_API_BASE}${path}`; 20 const response = await fetch(url, { 21 ...options, 22 headers: { 23 Authorization: `Bearer ${TODOIST_TOKEN}`, 24 "Content-Type": "application/json", 25 ...options.headers, 26 }, 27 }); 28 29 if (!response.ok) { 30 const body = await response.text(); 31 throw new Error(`Todoist API error ${response.status}: ${body}`); 32 } 33 34 // Some endpoints (like close task) return 204 with no body 35 if (response.status === 204) return null; 36 return response.json(); 37} 38 39// Create the MCP server 40const server = new McpServer({ 41 name: "mcp-todoist", 42 version: "1.0.0", 43}); 44 45// Tools will be registered here 46 47// Connect via stdio transport 48async function main() { 49 const transport = new StdioServerTransport(); 50 await server.connect(transport); 51} 52 53main().catch((err) => { 54 process.stderr.write(`Fatal error: ${err.message}\n`); 55 process.exit(1); 56});

بعض الأشياء الملاحظة:

  • الأخطاء تُكتب إلى process.stderr، وليس console.log أبدًا. بروتوكول MCP يستخدم stdio للاتصال، لذا أي شيء يُكتب إلى stdout سيفسد رسائل البروتوكول.
  • مساعد todoistRequest يتعامل مع المصادقة واستجابات الأخطاء في مكان واحد.
  • الرمز يأتي من متغير بيئة. لا تشفر رموز API أبدًا في الكود.

أول أداة: list_projects

لنسجل أبسط أداة أولاً:

1server.registerTool( 2 "list_projects", 3 { 4 description: 5 "List all projects in the user's Todoist account. Returns project names, IDs, and colors.", 6 inputSchema: {}, 7 }, 8 async () => { 9 const projects = await todoistRequest("/projects"); 10 return { 11 content: [ 12 { 13 type: "text" as const, 14 text: JSON.stringify(projects, null, 2), 15 }, 16 ], 17 }; 18 } 19);

كل تسجيل أداة له ثلاثة أجزاء:

  1. اسم ("list_projects"). هذا كيف يشير Claude للأداة.
  2. بيانات وصفية بـ description وinputSchema. الوصف يساعد Claude في تقرير متى يستخدم الأداة.
  3. دالة معالج تُنفذ عندما يستدعي Claude الأداة. يجب أن ترجع كائنًا بمصفوفة content تحتوي كتل نصية أو صور.

إضافة أدوات المهام

get_tasks

هذه الأداة تعرض المهام، مع إمكانية التصفية حسب المشروع أو فلتر بحث:

1server.registerTool( 2 "get_tasks", 3 { 4 description: 5 "Get active tasks from Todoist. Can filter by project ID or use Todoist filter syntax (e.g. 'overdue', 'today', 'p1').", 6 inputSchema: { 7 project_id: z.string().optional().describe("Filter tasks by project ID"), 8 filter: z 9 .string() 10 .optional() 11 .describe("Todoist filter query, e.g. 'overdue', 'today & p1', '#Work'"), 12 }, 13 }, 14 async ({ project_id, filter }) => { 15 const params = new URLSearchParams(); 16 if (project_id) params.set("project_id", project_id); 17 if (filter) params.set("filter", filter); 18 19 const query = params.toString(); 20 const path = `/tasks${query ? `?${query}` : ""}`; 21 const tasks = await todoistRequest(path); 22 23 const formatted = tasks.map((t: any) => ({ 24 id: t.id, 25 content: t.content, 26 description: t.description, 27 priority: t.priority, 28 priorityLabel: ["P4", "P3", "P2", "P1"][t.priority - 1], 29 due: t.due, 30 project_id: t.project_id, 31 labels: t.labels, 32 url: t.url, 33 })); 34 35 return { 36 content: [ 37 { type: "text" as const, text: JSON.stringify(formatted, null, 2) }, 38 ], 39 }; 40 } 41);

ملاحظة تستحق المعرفة: قيم أولوية Todoist API معكوسة مقارنة بواجهة المستخدم. أولوية API 4 تعني "الأولوية 1" (الأحمر، الأعلى إلحاحًا) في تطبيق Todoist، وأولوية API 1 تعني "الأولوية 4" (بلا أولوية).

create_task

1server.registerTool( 2 "create_task", 3 { 4 description: 5 "Create a new task in Todoist. Supports setting content, description, due dates, priority, labels, and project.", 6 inputSchema: { 7 content: z.string().describe("The task title (required)"), 8 description: z.string().optional().describe("Detailed description of the task"), 9 project_id: z.string().optional().describe("Project ID to add the task to"), 10 due_string: z 11 .string() 12 .optional() 13 .describe("Natural language due date, e.g. 'tomorrow', 'every monday', 'Jan 15'"), 14 due_date: z.string().optional().describe("Specific due date in YYYY-MM-DD format"), 15 priority: z 16 .number() 17 .min(1) 18 .max(4) 19 .optional() 20 .describe("Task priority: 4 = highest (P1 in UI), 3 = high, 2 = medium, 1 = no priority"), 21 labels: z.array(z.string()).optional().describe("Array of label names to apply"), 22 }, 23 }, 24 async ({ content, description, project_id, due_string, due_date, priority, labels }) => { 25 const body: Record<string, any> = { content }; 26 if (description) body.description = description; 27 if (project_id) body.project_id = project_id; 28 if (due_string) body.due_string = due_string; 29 if (due_date) body.due_date = due_date; 30 if (priority) body.priority = priority; 31 if (labels) body.labels = labels; 32 33 const task = await todoistRequest("/tasks", { 34 method: "POST", 35 body: JSON.stringify(body), 36 headers: { "X-Request-Id": crypto.randomUUID() }, 37 }); 38 39 return { 40 content: [ 41 { 42 type: "text" as const, 43 text: `Task created: "${task.content}" (ID: ${task.id})\n${JSON.stringify(task, null, 2)}`, 44 }, 45 ], 46 }; 47 } 48);

لاحظ ترويسة X-Request-Id. Todoist تستخدمها لعدم التكرار على نقاط نهاية التعديل. إذا تسبب خطأ شبكة في إعادة المحاولة، إرسال نفس معرف الطلب يضمن إنشاء المهمة مرة واحدة فقط.

complete_task

1server.registerTool( 2 "complete_task", 3 { 4 description: "Mark a task as completed in Todoist.", 5 inputSchema: { 6 task_id: z.string().describe("The ID of the task to complete"), 7 }, 8 }, 9 async ({ task_id }) => { 10 await todoistRequest(`/tasks/${task_id}/close`, { method: "POST" }); 11 return { 12 content: [ 13 { type: "text" as const, text: `Task ${task_id} marked as complete.` }, 14 ], 15 }; 16 } 17);

لماذا Zod مهمة للتحقق من المدخلات

قد تتساءل لماذا نستخدم مخططات Zod بدلاً من قبول أي كائن. ثلاثة أسباب:

  1. Claude يقرأ المخططات. مخطط الإدخال يُرسل إلى Claude كجزء من تعريف الأداة. تعليقات .describe() المفصلة تساعد Claude في فهم أي قيم يرسل.

  2. المدخلات غير الصالحة تُلتقط مبكرًا. إذا أرسل Claude أولوية 5، Zod ترفضها قبل تشغيل معالجك.

  3. أنماط TypeScript مُستنتجة. أنماط معاملات المعالج تُشتق تلقائيًا من مخطط Zod.


أنماط معالجة الأخطاء

مساعد todoistRequest يلتقط بالفعل أخطاء HTTP، لكن الأدوات يجب أن تتعامل مع الأخطاء بلطف حتى يتمكن Claude من الإبلاغ عنها للمستخدم بدلاً من الانهيار. غلّف كل معالج بـ try/catch:

1try { 2 // ... implementation 3 return { content: [{ type: "text" as const, text: result }] }; 4} catch (error: any) { 5 return { 6 content: [ 7 { type: "text" as const, text: `Error: ${error.message}` }, 8 ], 9 isError: true, 10 }; 11}

علامة isError: true تخبر عميل الذكاء الاصطناعي أن استدعاء الأداة فشل. يمكن لـ Claude حينها شرح الخطأ للمستخدم أو اقتراح إصلاحات.


الاختبار مع Claude Desktop

ابنِ المشروع:

1npm run build

الآن أعد Claude Desktop لاستخدام خادمك. افتح ملف إعداد Claude Desktop:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json
  • Linux: ~/.config/Claude/claude_desktop_config.json

أضف خادمك لقسم mcpServers:

1{ 2 "mcpServers": { 3 "todoist": { 4 "command": "node", 5 "args": ["/absolute/path/to/mcp-todoist/dist/index.js"], 6 "env": { 7 "TODOIST_API_TOKEN": "your-todoist-api-token-here" 8 } 9 } 10 } 11}

أعد تشغيل Claude Desktop. يجب أن ترى أيقونة المطرقة تشير إلى توفر أدوات MCP. جرب هذه المحثات:

  • "أرني جميع مشاريع Todoist"
  • "ما المهام المتأخرة؟"
  • "أنشئ مهمة لمراجعة التقرير الربعي بحلول الجمعة القادمة، أولوية عالية"
  • "علّم المهمة [المعرف] كمكتملة"
  • "أضف تعليقًا على تلك المهمة يقول أن المراجعة اكتملت"

إذا كنت تستخدم Claude Code بدلاً من ذلك، أضف الخادم لملف .mcp.json في مشروعك أو ~/.claude.json العام.


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

بمجرد أن يعمل خادمك، إليك بعض الأفكار لتوسيعه:

  • أضف get_comments لقراءة التعليقات الحالية على مهمة
  • أضف delete_task لعمليات التنظيف
  • أضف دعم الأقسام لتنظيم المهام داخل المشاريع
  • نفّذ تتبع حد المعدل بقراءة ترويسة استجابة X-Ratelimit-Remaining
  • أضف list_labels حتى يتمكن Claude من اكتشاف العلامات المتاحة قبل إنشاء المهام
  • حزّمها كوحدة npm حتى يتمكن الآخرون من تثبيتها بـ npx

الأنماط التي تعلمتها هنا، تسجيل الأدوات ومخططات Zod ونقل stdio ومعالجة الأخطاء، تنطبق على أي API. استبدل نقاط نهاية Todoist بـ Notion أو Linear أو Jira أو Stripe أو API داخلي خاص بك، وسيكون لديك خادم MCP جديد.


هل تحتاج خادم MCP للإنتاج؟

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

© 2026 عبد الباقي بركاتي. جميع الحقوق محفوظة.