بناء MCP Server للتعامل مع SQL Server: دليل عملي كامل

بناء MCP Server للتعامل مع SQL Server: دليل عملي كامل

يُعدّ بناء MCP Server للتعامل مع SQL Server من أكثر المشاريع العملية قيمةً لمن يبني تطبيقات مدعومة بالذكاء الاصطناعي فوق بيانات مؤسسية حقيقية. فبدلًا من نسخ مخطط الجداول ولصقه داخل المحادثة، أو منح النموذج اتصالًا مباشرًا بقاعدة البيانات، يمنحك Model Context Protocol طبقة وسيطة منضبطة تُحدّد فيها بدقة ما يستطيع المساعد رؤيته وما يُسمح له بتنفيذه.

الفكرة باختصار: تجعل قاعدة بياناتك مصدرًا موثوقًا يفهمه أي عميل يدعم MCP، مثل Claude Desktop أو VS Code أو أدوات داخلية، دون كتابة تكامل مخصص لكل عميل على حدة. في هذا الدليل نبني خادمًا كاملًا من الصفر، بأدوات آمنة، وموارد للمخطط، وإعدادات نشر جاهزة للإنتاج.

أهم النقاط التي ستتعلمها

  • ما هو Model Context Protocol ولماذا يغيّر طريقة ربط النماذج اللغوية بقواعد البيانات.
  • كيف تبني MCP Server بلغة TypeScript يتصل بـ SQL Server عبر حزمة mssql مع إدارة Connection Pool.
  • كيف تعرّف أدوات قراءة وكتابة آمنة تعتمد على الاستعلامات المُعاملة (Parameterized Queries) وليس تجميع النصوص.
  • كيف تحمي قاعدة البيانات من SQL Injection ومن الكتابة العشوائية بصلاحيات محدودة وحدود صفوف ومهل زمنية.
  • كيف تنشر الخادم وتختبره باستخدام MCP Inspector وDocker وملف إعداد Claude Desktop.

ما هو Model Context Protocol ولماذا ظهر؟

Model Context Protocol واختصاره MCP هو بروتوكول مفتوح أطلقته Anthropic أواخر عام 2024 لتوحيد طريقة اتصال نماذج اللغة الكبيرة بالأنظمة الخارجية. قبل ظهوره، كان كل منتج يعرّف أدواته بطريقته الخاصة، ما يعني كتابة تكامل جديد لكل نموذج وكل عميل، وهو عبء هندسي متكرر ومكلف.

يعتمد البروتوكول على نمط العميل/الخادم فوق JSON-RPC 2.0، ويعمل محليًا عبر stdio أو عن بعد عبر HTTP. هذا يعني أن الخادم الذي تبنيه مرة واحدة يمكن استخدامه من عدة عملاء دون تعديل.

المكونات الثلاثة في معمارية MCP

  • المضيف (Host): التطبيق الذي يشغّل النموذج، مثل Claude Desktop أو محرر أكواد أو واجهة دردشة داخلية.
  • العميل (Client): طبقة داخل المضيف تتولى إنشاء اتصال واحد مع كل خادم وتدير تبادل الرسائل.
  • الخادم (Server): البرنامج الذي تكتبه أنت، وهو المسؤول عن الإعلان عن الأدوات والموارد وتنفيذها فعليًا على SQL Server.

البدائيات التي يوفّرها الخادم

يفهم البروتوكول خمس قدرات، لكننا سنركز في هذا المقال على الثلاث الأولى لأنها الأهم عند التعامل مع قواعد البيانات:

  • الأدوات (Tools): دوال قابلة للاستدعاء من النموذج مثل list_tables أو run_select، مع مخطط إدخال واضح.
  • الموارد (Resources): بيانات للقراءة فقط يعرضها الخادم مثل مخطط قاعدة البيانات أو تقرير حالة الخادم.
  • الموجهات (Prompts): قوالب رسائل جاهزة يستدعيها المستخدم صراحةً مثل «حلّل استعلامًا بطيئًا».
  • Sampling: طلب توليد نص من النموذج داخل الخادم نفسه، ويُستخدم في المهام المركّبة.
  • Roots: تحديد نطاق الملفات أو الموارد التي يُسمح للخادم بالعمل عليها.

لماذا تحتاج إلى MCP Server للتعامل مع SQL Server؟

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

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

مقارنة الطرق الثلاث للتعامل مع SQL Server

  • النسخ واللصق اليدوي: بسيط في البداية، لكنه غير قابل للتوسع، ويُسرّب أسماء الجداول إلى سجل المحادثة، ويعطي نتائج قديمة بمجرد تغيّر المخطط.
  • كود مخصص لكل عميل: مرن لكنه يكرّر المنطق في كل مرة تتغير فيها الأداة المستخدمة أو النموذج.
  • MCP Server: تكتب المنطق مرة واحدة، وتضيف سياسات الأمان والتدقيق في مكان واحد، ويعمل مع أي عميل متوافق.

الفوائد العملية المباشرة

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

معمارية MCP Server للتعامل مع SQL Server خطوة بخطوة

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

الطبقات الخمس في التصميم المقترح

  1. طبقة النقل (Transport): stdio للاستخدام المحلي أو Streamable HTTP للاستخدام المشترك.
  2. طبقة البروتوكول: يتولاها SDK، وتشمل التسجيل والإعلان عن القدرات وإدارة الجلسات.
  3. طبقة الأدوات: تعريف الأدوات والتحقق من صحة المدخلات عبر مخطط Zod أو ما يعادله.
  4. طبقة الوصول للبيانات: استعلامات مُعاملة مع Pool واحد مشترك وحدود صفوف ومهل زمنية.
  5. طبقة قاعدة البيانات: SQL Server نفسه بحساب محدود الصلاحيات ومخطط واضح.

مسار الطلب من السؤال إلى النتيجة

  1. المستخدم يسأل: «ما هي أفضل خمسة عملاء من حيث إجمالي الطلبات هذا العام؟»
  2. المضيف يمرّر السؤال مع قائمة الأدوات المتاحة إلى النموذج.
  3. النموذج يختار أداة مثل get_top_customers ويمرّر المعاملات المطلوبة.
  4. الخادم يتحقق من المعاملات ثم ينفذ الاستعلام المُعامل على SQL Server.
  5. الخادم يعيد النتيجة كنص منظم، فيصوغ النموذج الإجابة النهائية للمستخدم.

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

المتطلبات الأساسية وإعداد بيئة التطوير

قبل البدء، تأكد من توفر المتطلبات التالية. معظم مشكلات المبتدئين تكون بسبب إصدار Node أو بسبب صلاحيات حساب قاعدة البيانات، وتُحل هذه المشكلات بسهولة إن ضبطتها مبكرًا.

  • Node.js بإصدار 18 أو أحدث، وnpm أو pnpm.
  • SQL Server 2019 أو 2022، أو Azure SQL Database.
  • حساب قاعدة بيانات مخصص للخادم، وليس حساب sa.
  • Python 3.10 أو أحدث إن أردت النسخة البديلة بـ FastMCP.
  • ODBC Driver 18 for SQL Server عند استخدام Python مع pyodbc.

إنشاء حساب قراءة فقط على SQL Server

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

CREATE LOGIN mcp_readonly WITH PASSWORD = 'كلمة-مرور-قوية-هنا';
GO
USE SalesDB;
GO
CREATE USER mcp_readonly FOR LOGIN mcp_readonly;
GO
ALTER ROLE db_datareader ADD MEMBER mcp_readonly;
GO
GRANT VIEW DEFINITION TO mcp_readonly;
GO

صلاحية VIEW DEFINITION مهمة لأنها تسمح للخادم بقراءة المخطط من INFORMATION_SCHEMA دون الحاجة إلى صلاحيات إدارية أوسع.

متغيرات البيئة وإدارة الأسرار

لا تضع بيانات الاتصال في الكود أبدًا. استخدم متغيرات بيئة يقرأها الخادم عند الإقلاع:

export MSSQL_SERVER=localhost
export MSSQL_PORT=1433
export MSSQL_DATABASE=SalesDB
export MSSQL_USER=mcp_readonly
export MSSQL_PASSWORD=كلمة-المرور-هنا

هذا الفصل يسمح بنقل نفس النسخة من الكود بين بيئات التطوير والاختبار والإنتاج دون أي تعديل، ويحمي الأسرار من الوصول إلى مستودع Git.

إنشاء هيكل مشروع MCP Server بلغة TypeScript

الـ SDK الرسمي لـ TypeScript هو الخيار الأكثر نضجًا اليوم، وباقي الأمثلة في هذا المقال مبنية عليه. سنستخدم حزمة mssql للاتصال، وهي تعمل بمحرك JavaScript خالص فلا تحتاج إلى تثبيت أي مشغل أصلي من نوع ODBC.

mkdir mcp-sqlserver-server
cd mcp-sqlserver-server
npm init -y
npm install @modelcontextprotocol/sdk mssql zod
npm install -D typescript @types/node @types/mssql tsx

بعد التثبيت، اضبط ملف tsconfig.json ليدعم وحدات ES الحديثة. إعداد moduleResolution على NodeNext ضروري لأن SDK يوزّع ملفاته بامتدادات صريحة.

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

أضف سكربتات التشغيل إلى package.json لتسهيل دورة التطوير، ثم أنشئ ملف src/index.ts كنقطة انطلاق.

الهيكل الأساسي للخادم

الكود التالي ينشئ خادمًا يعمل عبر stdio ويسجّل أداة واحدة بسيطة للتحقق من عمله:

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

const server = new McpServer({
  name: 'sqlserver-mcp',
  version: '1.0.0'
});

server.registerTool(
  'ping',
  {
    title: 'Ping',
    description: 'يتحقق من أن الخادم يعمل ويستجيب',
    inputSchema: {}
  },
  async () => ({
    content: [{ type: 'text', text: 'pong من MCP Server' }]
  })
);

const transport = new StdioServerTransport();
await server.connect(transport);
console.error('sqlserver-mcp يعمل عبر stdio');

لاحظ استخدام console.error وليس console.log. سبب ذلك أن الطرفية القياسية للإخراج محجوزة لرسائل البروتوكول، وأي طباعة عشوائية عليها ستُفسد الاتصال وتسبب أخطاء تحليل غامضة.

إدارة الاتصال بـ SQL Server باستخدام Connection Pool

فتح اتصال جديد مع كل استدعاء أداة هو أكثر خطأ أداء شيوعًا. الاتصال في SQL Server عملية مكلفة، والحل هو Connection Pool واحد يعاد استخدامه طوال عمر العملية.

import sql from 'mssql';

const config: sql.config = {
  server: process.env.MSSQL_SERVER ?? 'localhost',
  port: Number(process.env.MSSQL_PORT ?? 1433),
  database: process.env.MSSQL_DATABASE ?? 'SalesDB',
  user: process.env.MSSQL_USER,
  password: process.env.MSSQL_PASSWORD,
  options: {
    encrypt: true,
    trustServerCertificate: false,
    enableArithAbort: true
  },
  pool: { max: 5, min: 0, idleTimeoutMillis: 30000 },
  requestTimeout: 15000,
  connectionTimeout: 15000
};

let poolPromise: Promise<sql.ConnectionPool> | null = null;

export function getPool(): Promise<sql.ConnectionPool> {
  if (!poolPromise) {
    poolPromise = sql.connect(config);
  }
  return poolPromise;
}

export async function closePool(): Promise<void> {
  if (poolPromise) {
    const pool = await poolPromise;
    await pool.close();
    poolPromise = null;
  }
}

هذا الملف هو نقطة الاتصال الوحيدة في المشروع. ضبط max على قيمة صغيرة مثل 5 مقصود، لأن الخادم يخدم نموذجًا لغويًا لا آلاف المستخدمين المتزامنين، وإفراط عدد الاتصالات قد يستهلك موارد قاعدة البيانات بلا داعٍ.

إغلاق نظيف عند إيقاف الخادم

عند إيقاف العملية يجب إغلاق الـ Pool بلطف لتجنّب اتصالات معلّقة على الجهة الأخرى:

async function shutdown(signal: string): Promise<void> {
  console.error(`استقبال الإشارة ${signal}، جارٍ الإغلاق`);
  await closePool();
  process.exit(0);
}

process.on('SIGINT', () => void shutdown('SIGINT'));
process.on('SIGTERM', () => void shutdown('SIGTERM'));

معالجة الإشارات مهمة في بيئات الحاويات، لأن Docker يرسل SIGTERM قبل الإنهاء القسري بعد مهلة قصيرة.

تعريف الأدوات Tools: القواعد الذهبية

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

  • اجعل الاسم بصيغة الفعل والاسم معًا، مثل get_customer_orders وlist_tables، لا query أو data.
  • اكتب الوصف بالإنجليزية أو العربية بشكل ثابت، واذكر متى تُستخدم الأداة ومتى لا تُستخدم.
  • عرّف مخطط إدخال صارمًا بأنواع واضحة وحدود دنيا وعليا لكل رقم.
  • أعد نتائج صغيرة ومنظمة، فإعادة عشرة آلاف صف تستهلك السياق دون فائدة.
  • اجعل كل أداة تفعل شيئًا واحدًا؛ الأداة الضخمة متعددة الأغراض يصعب على النموذج استخدامها بثبات.

الأدوات الموصى بها لبداية عملية

  1. list_tables: استكشاف الجداول المتاحة.
  2. describe_table: معرفة الأعمدة والأنواع.
  3. get_*: أدوات قراءة محددة لكل كيان مهم مثل العملاء والطلبات والمنتجات.
  4. run_select: أداة استكشافية مقيدة باستعلامات SELECT فقط.
  5. update_*: أدوات كتابة محدودة جدًا وبعدد قليل من الأعمدة المسموح بتعديلها.

بناء أدوات قراءة آمنة من SQL Server

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

const LIST_TABLES_SQL = `
SELECT TABLE_SCHEMA, TABLE_NAME
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_TYPE = 'BASE TABLE'
ORDER BY TABLE_SCHEMA, TABLE_NAME;`;

server.registerTool(
  'list_tables',
  {
    title: 'List Tables',
    description: 'يعرض جميع الجداول الأساسية في قاعدة البيانات الحالية مع أسماء المخططات',
    inputSchema: {
      limit: z.number().int().min(1).max(500).default(200)
    }
  },
  async ({ limit }) => {
    const pool = await getPool();
    const result = await pool.request().query(LIST_TABLES_SQL);
    const rows = result.recordset.slice(0, limit);
    const text = rows.map((r) => `${r.TABLE_SCHEMA}.${r.TABLE_NAME}`).join('\n');
    return { content: [{ type: 'text', text: text || 'لا توجد جداول' }] };
  }
);

النتيجة نص بسيط يفهمه النموذج مباشرة، وحجمها محدود بمعامل limit حتى في قواعد بيانات كبيرة تحتوي مئات الجداول.

وصف الجدول باستعلام معامل

هنا نستخدم Parameterized Query لأول مرة. القاعدة بسيطة: لا تُدرج قيمة قادمة من النموذج داخل نص الاستعلام إطلاقًا.

server.registerTool(
  'describe_table',
  {
    title: 'Describe Table',
    description: 'يعرض أعمدة جدول محدد مع النوع وإمكانية NULL والطول الأقصى',
    inputSchema: {
      schema: z.string().default('dbo'),
      table: z.string().min(1)
    }
  },
  async ({ schema, table }) => {
    const pool = await getPool();
    const result = await pool
      .request()
      .input('schemaName', sql.NVarChar(128), schema)
      .input('tableName', sql.NVarChar(128), table)
      .query(`
        SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, CHARACTER_MAXIMUM_LENGTH
        FROM INFORMATION_SCHEMA.COLUMNS
        WHERE TABLE_SCHEMA = @schemaName AND TABLE_NAME = @tableName
        ORDER BY ORDINAL_POSITION;`);

    if (result.recordset.length === 0) {
      return { content: [{ type: 'text', text: 'الجدول غير موجود' }], isError: true };
    }

    const lines = result.recordset.map(
      (c) => `${c.COLUMN_NAME} | ${c.DATA_TYPE} | NULL: ${c.IS_NULLABLE}`
    );
    return { content: [{ type: 'text', text: lines.join('\n') }] };
  }
);

عند إرسال اسم جدول غير موجود نحصل على رسالة واضحة مع isError: true، وهذا يمنع النموذج من الاستمرار في تخمين أسماء أعمدة وهمية.

أداة SELECT استكشافية مع حواجز

أحيانًا نحتاج مرونة أعلى، خاصة في مرحلة الاستكشاف. الحل هو أداة تقبل SQL لكن مع فحوصات صارمة قبل التنفيذ:

const FORBIDDEN = /\b(insert|update|delete|drop|alter|truncate|merge|exec|grant|revoke|create)\b/i;
const MULTI_STATEMENT = /;\s*\S/;

server.registerTool(
  'run_select',
  {
    title: 'Run Select',
    description: 'ينفّذ استعلام SELECT للقراءة فقط ويعيد النتائج بصيغة JSON',
    inputSchema: {
      query: z.string().min(10).max(4000),
      maxRows: z.number().int().min(1).max(500).default(100)
    }
  },
  async ({ query, maxRows }) => {
    if (!/^\s*(select|with)\b/i.test(query)) {
      return { content: [{ type: 'text', text: 'مسموح باستعلامات SELECT أو CTE فقط' }], isError: true };
    }
    if (FORBIDDEN.test(query) || MULTI_STATEMENT.test(query)) {
      return { content: [{ type: 'text', text: 'الاستعلام يحتوي كلمات أو أنماطًا غير مسموحة' }], isError: true };
    }

    const pool = await getPool();
    const request = pool.request();
    request.timeout = 15000;
    const result = await request.query(query);
    const rows = result.recordset.slice(0, maxRows);

    return {
      content: [
        { type: 'text', text: `عدد الصفوف المعادة: ${rows.length}` },
        { type: 'text', text: JSON.stringify(rows, null, 2) }
      ]
    };
  }
);

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

الأداة المفضلة: استعلام مسمّى بمعاملات

الأداة الأكثر أمانًا هي التي لا يقترب فيها النموذج من SQL إطلاقًا، بل يمرّر معاملات فقط:

server.registerTool(
  'get_customer_orders',
  {
    title: 'Get Customer Orders',
    description: 'يجلب أحدث طلبات عميل محدد باستخدام معرّف العميل',
    inputSchema: {
      customerId: z.number().int().positive(),
      limit: z.number().int().min(1).max(100).default(20)
    }
  },
  async ({ customerId, limit }) => {
    const pool = await getPool();
    const result = await pool
      .request()
      .input('customerId', sql.Int, customerId)
      .input('limit', sql.Int, limit)
      .query(`
        SELECT TOP (@limit) SalesOrderID, OrderDate, TotalDue, Status
        FROM Sales.SalesOrderHeader
        WHERE CustomerID = @customerId
        ORDER BY OrderDate DESC;`);

    return {
      content: [{ type: 'text', text: JSON.stringify(result.recordset, null, 2) }]
    };
  }
);

لاحظ أن TOP (@limit) يقبل معاملًا في SQL Server الحديث، وهو أفضل من تضمين الرقم في النص. حتى لو حاول النموذج تمرير قيمة غريبة، فإن التحقق من النوع في مخطط الإدخال يرفضها قبل الوصول إلى قاعدة البيانات.

بناء أدوات كتابة محكومة

الكتابة هي المنطقة التي تتحول فيها الأخطاء الصغيرة إلى كوارث بيانات. لذلك نتبع ثلاث قواعد: تأكيد صريح، معاملة واحدة (Transaction)، وسجل تدقيق.

server.registerTool(
  'update_product_price',
  {
    title: 'Update Product Price',
    description: 'يحدّث سعر منتج مع تسجيل القيمة القديمة والجديدة في جدول تدقيق',
    inputSchema: {
      productId: z.number().int().positive(),
      newPrice: z.number().positive().max(1000000),
      confirm: z.literal(true).describe('يجب تمرير true لتأكيد التنفيذ')
    }
  },
  async ({ productId, newPrice }) => {
    const pool = await getPool();
    const transaction = new sql.Transaction(pool);
    await transaction.begin();

    try {
      const current = await new sql.Request(transaction)
        .input('productId', sql.Int, productId)
        .query('SELECT ListPrice FROM Production.Product WHERE ProductID = @productId');

      if (current.recordset.length === 0) {
        await transaction.rollback();
        return { content: [{ type: 'text', text: 'المنتج غير موجود' }], isError: true };
      }

      const oldPrice = current.recordset[0].ListPrice;

      await new sql.Request(transaction)
        .input('productId', sql.Int, productId)
        .input('newPrice', sql.Decimal(18, 2), newPrice)
        .query('UPDATE Production.Product SET ListPrice = @newPrice, ModifiedDate = SYSUTCDATETIME() WHERE ProductID = @productId');

      await new sql.Request(transaction)
        .input('productId', sql.Int, productId)
        .input('oldPrice', sql.Decimal(18, 2), oldPrice)
        .input('newPrice', sql.Decimal(18, 2), newPrice)
        .query('INSERT INTO dbo.PriceAudit (ProductID, OldPrice, NewPrice, ChangedAt) VALUES (@productId, @oldPrice, @newPrice, SYSUTCDATETIME())');

      await transaction.commit();
      return { content: [{ type: 'text', text: `تم تحديث السعر من ${oldPrice} إلى ${newPrice}` }] };
    } catch (error) {
      await transaction.rollback();
      throw error;
    }
  }
);

معامل confirm: z.literal(true) حيلة عملية: يجبر النموذج على تمرير قيمة تأكيد صريحة، ما يقلل حالات التنفيذ العرضي. كما أن سجل التدقيق يمنحك مسارًا للمراجعة لاحقًا.

متى تتجنب أداة الكتابة تمامًا

  • إذا كان المستخدم النهائي لا يملك صلاحية التعديل أصلًا، فالأفضل حجب الأداة بالكامل.
  • إذا كان التغيير يمس بيانات مالية أو قانونية، اجعله يمر عبر طلب موافقة بشري.
  • إذا احتجت إلى عمليات جماعية، أنشئ أداة خاصة تعتمد على جدول مؤقت أو ملف مرجعي بدل تمرير قائمة ضخمة.

استخدام Resources لعرض مخطط قاعدة البيانات

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

const SCHEMA_SQL = `
SELECT t.TABLE_SCHEMA, t.TABLE_NAME, c.COLUMN_NAME, c.DATA_TYPE
FROM INFORMATION_SCHEMA.TABLES t
JOIN INFORMATION_SCHEMA.COLUMNS c
  ON c.TABLE_NAME = t.TABLE_NAME AND c.TABLE_SCHEMA = t.TABLE_SCHEMA
WHERE t.TABLE_TYPE = 'BASE TABLE'
ORDER BY t.TABLE_SCHEMA, t.TABLE_NAME, c.ORDINAL_POSITION;`;

server.registerResource(
  'database-schema',
  'schema://database',
  {
    title: 'مخطط قاعدة البيانات',
    description: 'ملخص كامل للجداول والأعمدة في قاعدة البيانات',
    mimeType: 'application/json'
  },
  async (uri) => {
    const pool = await getPool();
    const result = await pool.request().query(SCHEMA_SQL);
    return {
      contents: [
        {
          uri: uri.href,
          mimeType: 'application/json',
          text: JSON.stringify(result.recordset)
        }
      ]
    };
  }
);

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

مورد حالة الخادم

مورد بسيط آخر مفيد للتشغيل: يكشف هل الاتصال سليم وما هي النسخة الحالية للخدمة.

server.registerResource(
  'server-status',
  'status://server',
  { title: 'حالة الخادم', mimeType: 'application/json' },
  async (uri) => {
    const pool = await getPool();
    const info = await pool.request().query('SELECT DB_NAME() AS db, SUSER_SNAME() AS loginName');
    return {
      contents: [
        {
          uri: uri.href,
          mimeType: 'application/json',
          text: JSON.stringify({ ok: true, ...info.recordset[0] })
        }
      ]
    };
  }
);

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

إضافة Prompts جاهزة لتسريع العمل

الموجهات توفّر على المستخدم كتابة نفس التعليمة في كل مرة. هي قوالب رسائل، وليست تنفيذًا فعليًا، لكنها تحسّن جودة المخرجات بشكل ملحوظ.

server.registerPrompt(
  'analyze_slow_query',
  {
    title: 'تحليل استعلام بطيء',
    description: 'يبني رسالة جاهزة لتحليل استعلام SQL واقتراح فهارس مناسبة',
    argsSchema: { query: z.string(), tableName: z.string() }
  },
  ({ query, tableName }) => ({
    messages: [
      {
        role: 'user',
        content: {
          type: 'text',
          text: `حلّل الاستعلام التالي على الجدول ${tableName} واقترح فهارس مناسبة، ثم اذكر المخاطر المحتملة:\n${query}`
        }
      }
    ]
  })
);

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

Transport: stdio أم Streamable HTTP؟

اختيار طبقة النقل يحدد شكل النشر بالكامل، وهناك فرق واضح بين الحالتين:

  • stdio: الأنسب للاستخدام الفردي المحلي، لا يحتاج منافذ ولا مصادقة شبكية، ويعمل مباشرة مع Claude Desktop وVS Code.
  • Streamable HTTP: الأنسب للفِرَق والخدمات المشتركة، يحتاج منفذًا ومصادقة وإدارة جلسات وعزلًا بين المستخدمين.

مثال مبسّط لنقطة نهاية HTTP. لاحظ أن كل جلسة جديدة تحتاج نسخة مستقلة من الخادم:

import express from 'express';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { randomUUID } from 'node:crypto';

const app = express();
app.use(express.json({ limit: '1mb' }));

const sessions = new Map();

app.post('/mcp', async (req, res) => {
  const sessionId = req.headers['mcp-session-id'];
  let transport = sessionId ? sessions.get(sessionId) : undefined;

  if (!transport) {
    transport = new StreamableHTTPServerTransport({
      sessionIdGenerator: () => randomUUID(),
      onsessioninitialized: (id) => sessions.set(id, transport)
    });
    await createMcpServer().connect(transport);
  }

  await transport.handleRequest(req, res, req.body);
});

app.listen(3100, () => console.error('MCP HTTP listening on 3100'));

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

الأمان في MCP Server للتعامل مع SQL Server

الأمان ليس بندًا اختياريًا في هذا النوع من المشاريع، لأن الوسيط هنا نموذج لغوي قد يُستدرج بسهولة إلى تنفيذ ما لا تقصده. الدفاع الحقيقي طبقات متعددة.

الطبقة الأولى: صلاحيات قاعدة البيانات

  • استخدم حساب قراءة فقط للحساب الافتراضي، ولا تُضف الكتابة إلا لجداول محددة.
  • امنع الوصول إلى الجداول الحساسة عبر DENY صريح بدل الاعتماد على النسيان.
  • لا تمنح أبدًا db_owner أو sysadmin لحساب الخادم.

الطبقة الثانية: الاستعلامات المُعاملة

كل قيمة قادمة من النموذج تمر عبر .input() وليس عبر تجميع نصي. تجميع النصوص هو الباب الأشهر لـ SQL Injection، وهو خطر أكبر هنا لأن مدخلات النموذج غير متوقعة بطبيعتها.

الطبقة الثالثة: حدود التنفيذ

  • حد أقصى لعدد الصفوف في كل نتيجة.
  • مهلة زمنية صريحة على مستوى الاستعلام وعلى مستوى الاتصال.
  • قائمة بيضاء للجداول أو المخططات المسموح بالوصول إليها.
  • منع تنفيذ إجراءات مخزنة تتلقى SQL ديناميكيًا.

الطبقة الرابعة: المراقبة والتدقيق

سجّل كل استدعاء أداة: اسم الأداة، المعاملات (مع إخفاء القيم الحساسة)، المدة، عدد الصفوف، وهوية الجلسة. هذه السجلات هي ما ستحتاجه عند التحقيق في أي سلوك غريب.

الطبقة الخامسة: إدارة الأسرار والشبكة

  • بيانات الاتصال في متغيرات بيئة أو مدير أسرار، لا في الكود ولا في ملف إعداد يشارَك بالخطأ.
  • استخدم encrypt: true وtrustServerCertificate: false في الإنتاج.
  • عند استخدام HTTP، ضع الخادم خلف بوابة API مع مصادقة وتحقق من الأصل.
  • قيّد الوصول الشبكي إلى SQL Server بمنافذ محددة وقائمة عناوين معروفة.

الأداء والتحسين

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

أهم ممارسات الأداء

  • أعد استخدام الـ Pool وامنع إنشاء اتصال لكل استدعاء.
  • استخدم TOP وOFFSET FETCH لتقسيم النتائج الكبيرة بدل إعادتها كاملة.
  • لا تشغّل SELECT * أبدًا في أدوات جاهزة؛ حدد الأعمدة المطلوبة فقط.
  • أضف فهارس مناسبة على الأعمدة المستخدمة في شروط أدواتك الأساسية.
  • خزّن نتائج المخطط مؤقتًا لأنها تتغير نادرًا.

مثال على التقسيم الصحيح للنتائج داخل أداة قراءة:

const PAGE_SQL = `
SELECT SalesOrderID, OrderDate, TotalDue
FROM Sales.SalesOrderHeader
ORDER BY SalesOrderID
OFFSET @offset ROWS FETCH NEXT @pageSize ROWS ONLY;`;

const result = await pool
  .request()
  .input('offset', sql.Int, offset)
  .input('pageSize', sql.Int, pageSize)
  .query(PAGE_SQL);

هذا النمط يمنع أداة واحدة من إرجاع ملايين الصفوف إلى السياق، وهو سبب شائع لتضخّم التكلفة وبطء الاستجابة.

قياس الأداء فعليًا

لا تعتمد على الإحساس. استخدم SET STATISTICS TIME ON أثناء التطوير، أو راجع sys.dm_exec_query_stats لمعرفة الاستعلامات الأكثر استهلاكًا. غالبًا ستكتشف أن أداة واحدة فقط هي مصدر معظم البطء.

اختبار MCP Server وتصحيح الأخطاء

لا تربط الخادم بالعميل النهائي قبل التأكد من عمله. هناك أداة رسمية مخصصة لهذا الغرض: MCP Inspector، وتعمل كعميل مرئي يعرض الأدوات والموارد ويسمح باستدعائها يدويًا.

npx @modelcontextprotocol/inspector node dist/index.js

بعد فتح الواجهة ستتمكن من رؤية قائمة الأدوات، تمرير المعاملات، وفحص الاستجابة الخام. هذه الطريقة تكشف بسرعة أخطاء المخططات ورسائل الخطأ غير الواضحة.

اختبار الوحدات بمعزل عن قاعدة البيانات

افصل منطق التحقق عن الوصول للبيانات ليكون قابلًا للاختبار وحده:

export function isSelectOnly(query: string): boolean {
  if (!/^\s*(select|with)\b/i.test(query)) return false;
  if (/\b(insert|update|delete|drop|alter|truncate|merge|exec)\b/i.test(query)) return false;
  return !/;\s*\S/.test(query);
}

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

أخطاء تشغيلية شائعة وحلولها

  • لا تظهر أي أداة في العميل: غالبًا تطبع رسالة على stdout أو أن مسار الملف التنفيذي خاطئ.
  • فشل المصافحة: تحقق من إصدار Node ومن إصدار SDK.
  • انتهاء المهلة دائمًا: تحقق من جدار الحماية وإعداد encrypt.
  • رفض تسجيل الدخول: تأكد من أن الحساب موجود على مستوى السيرفر وعلى مستوى قاعدة البيانات معًا.

النشر والتشغيل في الإنتاج

بعد أن يعمل الخادم محليًا، الخطوة التالية تغليفه في صورة قابلة للتوزيع. الحاوية هي الخيار الأكثر شيوعًا لأنها تعزل الاعتماديات وتوحّد البيئات.

FROM node:20-bookworm-slim

WORKDIR /app

COPY package*.json ./
RUN npm ci --omit=dev

COPY dist ./dist

ENV NODE_ENV=production

CMD ["node", "dist/index.js"]

لاحظ أن حزمة mssql لا تحتاج أي مشغّل ODBC أصلي في Node، لذلك لا داعي لتثبيت حزم مايكروسوفت داخل الصورة. أما نسخة Python بـ pyodbc فتحتاج إلى msodbcsql18 داخل الحاوية.

ربط الخادم بعميل MCP

عند الاستخدام المحلي، يوضع إعداد الخادم في ملف تهيئة العميل. في Claude Desktop يكون الملف عادةً باسم claude_desktop_config.json:

{
  "mcpServers": {
    "sqlserver": {
      "command": "node",
      "args": ["C:/mcp/sqlserver-mcp/dist/index.js"],
      "env": {
        "MSSQL_SERVER": "localhost",
        "MSSQL_DATABASE": "SalesDB",
        "MSSQL_USER": "mcp_readonly",
        "MSSQL_PASSWORD": "ضع-كلمة-المرور-هنا"
      }
    }
  }
}

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

قائمة تحقق قبل الإنتاج

  1. هل الحساب قراءة فقط افتراضيًا؟
  2. هل كل المعاملات تمر عبر مُعاملة وليست مجمّعة نصيًا؟
  3. هل توجد مهل زمنية وحدود صفوف؟
  4. هل تُسجّل الاستدعاءات مع إخفاء البيانات الحساسة؟
  5. هل الأسرار خارج الكود تمامًا؟
  6. هل تم اختبار الأدوات عبر Inspector قبل الربط؟

نسخة Python سريعة باستخدام FastMCP

إذا كان فريقك يعمل بلغة Python، فإن حزمة mcp الرسمية توفّر واجهة FastMCP تختصر التعريف بشكل كبير بالاعتماد على الديكوريترات.

pip install mcp pyodbc

وتثبيت مشغّل ODBC مطلوب على النظام، ثم يمكن كتابة خادم صغير في ملف واحد:

import os
import pyodbc
from mcp.server.fastmcp import FastMCP

mcp = FastMCP('sqlserver-mcp')

CONNECTION_STRING = (
    'DRIVER={ODBC Driver 18 for SQL Server};'
    f"SERVER={os.environ['MSSQL_SERVER']};"
    f"DATABASE={os.environ['MSSQL_DATABASE']};"
    f"UID={os.environ['MSSQL_USER']};"
    f"PWD={os.environ['MSSQL_PASSWORD']};"
    'Encrypt=yes;TrustServerCertificate=no;'
)

@mcp.tool()
def list_tables() -> list[str]:
    """يعرض أسماء الجداول الأساسية في قاعدة البيانات."""
    with pyodbc.connect(CONNECTION_STRING) as conn:
        cursor = conn.cursor()
        cursor.execute(
            "SELECT TABLE_SCHEMA + '.' + TABLE_NAME FROM INFORMATION_SCHEMA.TABLES "
            "WHERE TABLE_TYPE = 'BASE TABLE' ORDER BY 1"
        )
        return [row[0] for row in cursor.fetchall()]

if __name__ == '__main__':
    mcp.run()

النقطة المهمة هنا أن نص الوصف داخل الديكوريتر يتحول تلقائيًا إلى وصف الأداة الذي يراه النموذج، لذلك اكتبه بعناية كما تفعل في نسخة TypeScript. الفرق الوحيد أن نسخة Python تفتح اتصالًا لكل استدعاء، وهو أمر مقبول في الاستخدام الخفيف لكنه يحتاج Pool عند الضغط العالي.

الأخطاء الشائعة وكيفية تجنبها

  • تجميع SQL يدويًا من مدخلات النموذج: استخدم دائمًا معاملات مربوطة، ولا تثق بأي تنقية نصية يدوية.
  • منح صلاحيات واسعة للراحة: ابدأ بأقل صلاحية ممكنة ثم زد تدريجيًا بعد الحاجة الفعلية.
  • الطباعة على stdout: يفسد البروتوكول فورًا، واقتصر دائمًا على stderr.
  • وصف أدوات غامض: يؤدي إلى اختيار خاطئ للأداة أو تمرير معاملات ناقصة.
  • إعادة نتائج ضخمة: تستهلك السياق وتزيد التكلفة وتشتت النموذج.
  • غياب المهلة الزمنية: استعلام واحد بطيء قد يجمّد الخادم كله.
  • حذف أو تحديث بدون Transaction: يفقدك القدرة على التراجع عند فشل جزئي.
  • إهمال التدقيق: تصبح غير قادر على معرفة من عدّل ماذا ومتى.

أفضل الممارسات لبناء MCP Server احترافي

  1. اجعل كل أداة مسؤولة عن مهمة واحدة واضحة، ولا تدمج القراءة والكتابة في نفس الأداة.
  2. ضع تحققًا مزدوجًا: مخطط إدخال صارم، ثم تحقق منطقي داخل التنفيذ.
  3. أعد النتائج بصيغة نصية مختصرة مع أسماء أعمدة واضحة.
  4. استخدم رسائل خطأ مفهومة للنموذج، لأنها تساعده على تصحيح مساره بدل التوقف.
  5. افصل منطق قاعدة البيانات في ملف مستقل ليصبح قابلًا للاختبار.
  6. وثّق كل أداة بمثال استدعاء ومثال نتيجة داخل التعليقات.
  7. أضف نسخة احتياطية أو بيئة اختبار مطابقة قبل تجربة أدوات الكتابة.
  8. راقب عدد الصفوف وزمن التنفيذ وأضف تنبيهًا عند تجاوز الحدود.

حالات استخدام واقعية

بعد بناء الخادم، تبدأ القيمة الحقيقية في الظهور. هذه أبرز السيناريوهات التي نراها عمليًا:

  • مساعد تحليل المبيعات: أدوات تُعيد أعلى العملاء والمنتجات والإيرادات بحسب الفترة، فيسأل مدير المبيعات بلغة طبيعية ويحصل على أرقام من قاعدة البيانات مباشرة.
  • دعم فني داخلي: أداة تعرض حالة طلب أو تذكرة عميل بمجرد تمرير المعرّف، دون فتح استعلامات يدوية.
  • مراجعة البيانات: أداة تقرأ عينات من صفوف مشكوك في صحتها وتقترح قواعد تحقق.
  • تسريع ترحيل المخططات: مورد المخطط يعطي صورة كاملة قبل كتابة سكربتات الترقية.
  • بناء التقارير المتكررة: موجهات جاهزة تحوّل أسئلة الفريق المتكررة إلى تقارير منظمة بنفس الشكل.

في كل هذه الحالات، الأدوات الثابتة المحدودة تتفوق على أداة SQL حرة، لأن النتائج تصبح قابلة للتوقع وقابلة للتدقيق.

الأسئلة الشائعة حول بناء MCP Server للتعامل مع SQL Server

هل أحتاج إلى تثبيت مشغّل ODBC مع Node.js؟

لا، حزمة mssql تعتمد على محرك JavaScript خالص وتتصل مباشرة بـ SQL Server. مشغّل ODBC مطلوب فقط عند استخدام Python مع pyodbc أو أدوات أخرى مشابهة.

هل يمكن استخدام نفس الخادم مع أكثر من قاعدة بيانات؟

نعم، عبر تمرير اسم قاعدة البيانات كمعامل في أدوات محددة، لكن الأفضل تشغيل نسخة منفصلة لكل قاعدة بيانات لتبسيط الصلاحيات والمراقبة ومنع الوصول العابر بين البيئات.

ما الفرق بين الأدوات والموارد في هذا السياق؟

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

كيف أمنع النموذج من تنفيذ استعلامات خطيرة؟

اجمع بين ثلاثة عناصر: حساب قاعدة بيانات محدود الصلاحيات، وأدوات مسمّاة بمعاملات بدل SQL حر، وفحوصات نصية ترفض أي عبارة غير SELECT. لا تعتمد على الطبقة النصية وحدها.

هل يمكن تشغيل الخادم على خادم بعيد؟

نعم، عبر نقل Streamable HTTP مع مصادقة قوية. لكن ضع في الحسبان إدارة الجلسات والعزل بين المستخدمين، وهي أمور لا تحتاجها في وضع stdio المحلي.

كيف أتعامل مع النتائج الكبيرة جدًا؟

استخدم التقسيم بـ OFFSET وFETCH، وحدد حدًا أعلى لعدد الصفوف في مخطط الأداة، واطلب من النموذج استخدام أدوات تجميعية مثل COUNT وSUM بدل جلب الصفوف الخام.

هل يدعم MCP Server عمليات الكتابة بشكل آمن؟

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

ما أفضل طريقة لاختبار الأدوات؟

ابدأ بـ MCP Inspector لاختبار يدوي سريع، ثم أضف اختبارات وحدات لمنطق التحقق، وأخيرًا اختبر ضد قاعدة بيانات اختبار مطابقة قبل أي تجربة على الإنتاج.

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

إذا وصلت إلى هنا، فأنت تملك صورة كاملة عن بناء MCP Server للتعامل مع SQL Server من الفكرة حتى الإنتاج. النصيحة الأهم: لا تحاول بناء كل شيء في جلسة واحدة.

  1. ابدأ بخادم صغير يحتوي أداتين فقط: list_tables وdescribe_table، وتأكد من ظهورهما في العميل.
  2. أضف أداة قراءة واحدة مسمّاة للكيان الأكثر استخدامًا في شركتك.
  3. انقل بيانات الاتصال إلى متغيرات بيئة وأنشئ حساب قراءة فقط.
  4. أضف مورد المخطط ليصبح لدى النموذج سياق دائم عن البنية.
  5. اختبر كل شيء عبر MCP Inspector، ثم اربطه بالعميل الفعلي.
  6. أضف أدوات كتابة لاحقًا فقط عند وجود حاجة حقيقية، ومع تأكيد صريح وسجل تدقيق.
  7. راقب السجلات أسبوعيًا، وحسّن الاستعلامات الأكثر استهلاكًا.

القيمة الحقيقية لهذا المشروع ليست في الكود نفسه، بل في الطبقة المنضبطة التي تبنيها بين النموذج وبياناتك. هذه الطبقة هي ما يجعل المساعد الذكي موثوقًا بدل أن يكون مصدرًا لاستعلامات عشوائية على قواعد بيانات الإنتاج.

#mcp server #sql server #model context protocol #typescript #nodejs #fastmcp #python #claude desktop #ai agents #database security #database integration #backend development

اشترك في نشرتنا البريدية

12k+

المشتركون

أسبوعيًا

التكرار

مجاني

دائمًا