0Pricing
AI Engineering Academy · درس

إتاحة موارد قواعد البيانات عبر MCP

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

إتاحة موارد قواعد البيانات عبر MCP درس مجاني في AI Engineering Academy على CoddyKit. هذا هو الدرس 3 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في AI Engineering Academy، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة AI Engineering Academy 4 دروس في المجموع.

لماذا تحتاج قواعد البيانات إلى MCP

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

تصميم مخطط MCP لقاعدة البيانات

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

  • أدوات القراءة: list_records، get_record، search_records
  • أدوات الكتابة (اختيارية ومقيّدة): create_record، update_record
  • محظور: تنفيذ SQL خام، وتعديل المخطط، وجداول النظام

إعداد الاتصال بقاعدة البيانات

استخدموا asyncpg مع PostgreSQL أو aiosqlite مع SQLite للوصول غير المتزامن إلى قاعدة البيانات في خادم MCP الخاص بكم. أنشئوا تجمع اتصالات عند بدء التشغيل حتى لا تفتح كل استدعاءات الأدوات اتصالًا جديدًا. خزّنوا عنوان URL لقاعدة البيانات في متغير بيئي، وليس في الشيفرة مطلقًا.

import asyncpg
import os
from mcp.server import Server
from mcp import types
import asyncio

app = Server('database-mcp-server')
pool = None  # Global connection pool

async def get_pool():
    global pool
    if pool is None:
        pool = await asyncpg.create_pool(
            os.environ['DATABASE_URL'],
            min_size=2,
            max_size=10,
            command_timeout=30
        )
    return pool

# Initialize pool at startup
async def startup():
    await get_pool()
    print('Database pool initialized', flush=False)  # stderr only via logger

إدراج السجلات كأداة

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

@app.list_tools()
async def list_tools():
    return [
        types.Tool(
            name='list_products',
            description='List products from the catalog with optional filtering by category and price range.',
            inputSchema={
                'type': 'object',
                'properties': {
                    'category': {'type': 'string', 'description': 'Filter by product category.'},
                    'max_price': {'type': 'number', 'description': 'Maximum price in USD.'},
                    'limit': {'type': 'integer', 'default': 20, 'maximum': 100}
                },
                'required': []
            }
        )
    ]

@app.call_tool()
async def call_tool(name: str, arguments: dict):
    if name == 'list_products':
        return await list_products_handler(arguments)

async def list_products_handler(args: dict):
    pool = await get_pool()
    query = 'SELECT id, name, category, price, stock FROM products WHERE 1=1'
    params = []

    if 'category' in args:
        params.append(args['category'])
        query += f' AND category = ${len(params)}'
    if 'max_price' in args:
        params.append(args['max_price'])
        query += f' AND price <= ${len(params)}'

    limit = min(args.get('limit', 20), 100)
    params.append(limit)
    query += f' ORDER BY name LIMIT ${len(params)}'

    async with pool.acquire() as conn:
        rows = await conn.fetch(query, *params)

    if not rows:
        return [types.TextContent(type='text', text='No products found matching your criteria.')]

    lines = ['id | name | category | price | stock']
    lines += [f'{r["id"]} | {r["name"]} | {r["category"]} | ${r["price"]} | {r["stock"]}' for r in rows]
    return [types.TextContent(type='text', text='\n'.join(lines))]

إتاحة صفوف قاعدة البيانات كموارد

يمكن إتاحة سجلات قاعدة البيانات الفردية كموارد MCP باستخدام عناوين URI منظّمة مثل db://products/42. تتيح الموارد لعميل الذكاء الاصطناعي تخزين سجلات محددة مؤقتًا والرجوع إليها من دون استدعاء أداة في كل مرة. نفّذوا list_resources لإعادة السجلات الأكثر صلة، وread_resource لجلب سجل واحد بحسب URI.

@app.list_resources()
async def list_resources():
    pool = await get_pool()
    async with pool.acquire() as conn:
        # List most recently updated products as resources
        rows = await conn.fetch(
            'SELECT id, name FROM products ORDER BY updated_at DESC LIMIT 50'
        )
    return [
        types.Resource(
            uri=f'db://products/{row["id"]}',
            name=row['name'],
            description=f'Product record for {row["name"]}',
            mimeType='application/json'
        )
        for row in rows
    ]

@app.read_resource()
async def read_resource(uri: str) -> str:
    import json
    if uri.startswith('db://products/'):
        product_id = int(uri.split('/')[-1])
        pool = await get_pool()
        async with pool.acquire() as conn:
            row = await conn.fetchrow(
                'SELECT id, name, category, price, description, stock FROM products WHERE id = $1',
                product_id
            )
        if row is None:
            raise ValueError(f'Product {product_id} not found')
        return json.dumps(dict(row), default=str)
    raise ValueError(f'Unknown resource URI: {uri}')

ترقيم صفحات مجموعات النتائج الكبيرة

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

async def paginated_list(table: str, limit: int = 20, after_id: int = 0) -> tuple:
    '''Returns (rows, has_more, next_cursor).'''
    pool = await get_pool()
    async with pool.acquire() as conn:
        rows = await conn.fetch(
            f'SELECT * FROM {table} WHERE id > $1 ORDER BY id LIMIT $2',
            after_id,
            limit + 1  # Fetch one extra to detect if more pages exist
        )

    has_more = len(rows) > limit
    rows = rows[:limit]
    next_cursor = rows[-1]['id'] if rows else None
    return rows, has_more, next_cursor

# In your tool result, include pagination info:
# 'Showing 20 products. To see the next page, call list_products with after_id=120'

أداة البحث في النص الكامل

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

@app.list_tools()
async def list_tools():
    return [
        types.Tool(
            name='search_products',
            description='Full-text search across product names and descriptions. Use for finding products by natural language, not by category filter.',
            inputSchema={
                'type': 'object',
                'properties': {
                    'query': {'type': 'string', 'description': 'Natural language search query.'},
                    'limit': {'type': 'integer', 'default': 10, 'maximum': 50}
                },
                'required': ['query']
            }
        )
    ]

async def search_products_handler(args: dict):
    pool = await get_pool()
    async with pool.acquire() as conn:
        rows = await conn.fetch(
            '''SELECT id, name, price, ts_rank(search_vector, query) AS rank
               FROM products, plainto_tsquery('english', $1) AS query
               WHERE search_vector @@ query
               ORDER BY rank DESC
               LIMIT $2''',
            args['query'],
            args.get('limit', 10)
        )
    if not rows:
        return [types.TextContent(type='text', text=f'No products found matching "{args["query"]}".  ')]
    lines = [f'{r["id"]}: {r["name"]} — ${r["price"]}' for r in rows]
    return [types.TextContent(type='text', text='\n'.join(lines))]

عمليات الكتابة مع التأكيد

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

import uuid
pending_actions = {}  # In-memory store; use Redis in production

async def create_order_preview(args: dict) -> list:
    action_id = str(uuid.uuid4())[:8]
    pending_actions[action_id] = {'type': 'create_order', 'data': args}
    return [types.TextContent(
        type='text',
        text=f'Preview: Create order for customer {args["customer_id"]} with {len(args["items"])} items, '
             f'total ${args["total"]}. Confirm with: confirm_action(action_id="{action_id}")')
    ]

async def confirm_action_handler(args: dict) -> list:
    action_id = args.get('action_id')
    action = pending_actions.pop(action_id, None)
    if not action:
        return [types.TextContent(type='text', text='Action expired or not found.')]
    # Execute the pending action
    result = await execute_action(action)
    return [types.TextContent(type='text', text=f'Done: {result}')]

تصفية البيانات الحساسة

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

async def get_safe_customer(customer_id: int) -> dict:
    '''Fetch customer data with sensitive fields excluded.'''
    pool = await get_pool()
    async with pool.acquire() as conn:
        row = await conn.fetchrow(
            '''SELECT id, name, created_at, country, tier
               FROM customers
               WHERE id = $1
               -- NEVER select: password_hash, api_key, full_address, payment_method_id
            ''',
            customer_id
        )
    return dict(row) if row else {}

تسجيل عمليات التدقيق للوصول إلى قاعدة البيانات

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

import logging
import sys
import time

audit_logger = logging.getLogger('mcp.audit')
audit_logger.setLevel(logging.INFO)
handler = logging.StreamHandler(sys.stderr)
audit_logger.addHandler(handler)

async def audited_tool_call(name: str, arguments: dict, session_id: str = None) -> list:
    start = time.time()
    result = await call_tool(name, arguments)
    elapsed_ms = round((time.time() - start) * 1000)

    audit_logger.info({
        'event': 'tool_call',
        'tool': name,
        'args': {k: v for k, v in arguments.items() if k != 'password'},
        'result_count': len(result),
        'elapsed_ms': elapsed_ms,
        'session_id': session_id
    })
    return result

اختبار خادم MCP لقاعدة البيانات

اختبروا خادم MCP لقاعدة البيانات على ثلاثة مستويات: اختبارات الوحدات لوظائف الاستعلام الفردية، باستخدام قاعدة بيانات اختبار أو كائنات محاكاة؛ واختبارات التكامل التي تبدأ تشغيل خادم MCP الكامل وتستدعي الأدوات عبر عميل SDK؛ واختبارات شاملة تتحقق من أن Claude Desktop يرى الأدوات ويستدعيها بشكل صحيح. تكشف الاختبارات المؤتمتة حالات التراجع قبل وصولها إلى الذكاء الاصطناعي.

تحقق سريع

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

مراجعة الدرس

تعلّمتم في هذا الدرس أن: خوادم قواعد بيانات MCP تعمل كوسطاء مضبوطين يوفّرون أدوات استعلام صريحة وعمليات للقراءة فقط افتراضيًا، وأن صفوف قاعدة البيانات يمكن إتاحتها كموارد MCP باستخدام عناوين URI منظّمة كي يرجع إليها الذكاء الاصطناعي، وأن عمليات الكتابة ينبغي أن تستخدم نمط المعاينة ثم التأكيد لمنع التغييرات غير المقصودة. في الخطوة التالية، سنؤمّن خادم MCP الخاص بنا باستخدام مصادقة OAuth 2.0 والتحقق من الإدخال.

الأسئلة الشائعة

هل درس «إتاحة موارد قواعد البيانات عبر MCP» مجاني؟

نعم — نص درس «إتاحة موارد قواعد البيانات عبر MCP» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة AI Engineering Academy، انتقل إلى CoddyKit PRO. تتضمن دورة AI Engineering Academy 4 دروس في المجموع.

ماذا ستتعلم في «إتاحة موارد قواعد البيانات عبر MCP»؟

أنشئوا موارد MCP تقدّم محتوى قاعدة البيانات ديناميكيًا، وأتيحوا أدوات استعلام يمكن للنموذج استدعاؤها، ونفذوا تقسيم النتائج إلى صفحات لمجموعات النتائج الكبيرة. تتمرن على AI Engineering Academy مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.

هل أحتاج إلى خبرة سابقة لأبدأ AI Engineering Academy؟

لا تُشترط خبرة سابقة. AI Engineering Academy على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 3 من أصل 4.

كم من الوقت يستغرق درس «إتاحة موارد قواعد البيانات عبر MCP»؟

معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.

هل يمكنني كتابة وتشغيل أكواد في درس AI Engineering Academy هذا؟

نعم. كل درس في AI Engineering Academy يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.

جميع الدروس في هذه الدورة

  1. ما هو MCP ولماذا يهم
  2. بناء خادم MCP الأول لكم
  3. إتاحة موارد قواعد البيانات عبر MCP
  4. أمان MCP والمصادقة
← العودة إلى AI Engineering Academy