0Pricing
AI Engineering Academy · Lezione

Esporre risorse del database tramite MCP

Crei risorse MCP che forniscano dinamicamente contenuti dal database, esponga strumenti di query che il modello possa chiamare e implementi la paginazione per grandi insiemi di risultati.

Esporre risorse del database tramite MCP è una lezione AI Engineering Academy gratuita su CoddyKit. Questa è la lezione 3 di 4. Puoi leggere la lezione completa qui gratuitamente — poi esercitati direttamente nel browser con un editor di codice integrato e un tutor IA disponibile 24/7. Fa parte del percorso di apprendimento AI Engineering Academy, e i tuoi progressi si sincronizzano tra il web e l'app CoddyKit. Il corso AI Engineering Academy include 4 lezioni in totale.

Perché i database hanno bisogno di MCP

I database sono tra le fonti di dati più preziose per gli assistenti IA, ma è troppo rischioso consentire a un LLM di accedervi direttamente. Un server MCP agisce da proxy controllato tra l'IA e il database, esponendo solo i dati e le operazioni esplicitamente consentiti, con convalida, limitazione della frequenza e auditing integrati.

Progettazione dello schema MCP del database

Prima di scrivere il codice, decida cosa l'IA deve poter vedere e fare. Definisca: quali tabelle sono leggibili, quali strumenti eseguono operazioni di scrittura, se presenti, quali dati devono essere filtrati in base al contesto dell'utente e quali colonne devono essere nascoste, come password e dati personali. Questa fase di progettazione previene l'esposizione accidentale dei dati.

  • Strumenti di lettura: list_records, get_record, search_records
  • Strumenti di scrittura (facoltativi e soggetti a restrizioni): create_record, update_record
  • Vietato: esecuzione di SQL grezzo, modifiche allo schema, tabelle di sistema

Configurazione della connessione al database

Utilizzi asyncpg (PostgreSQL) o aiosqlite (SQLite) per l'accesso asincrono al database nel server MCP. Crei un pool di connessioni all'avvio, così ogni chiamata a uno strumento non dovrà aprire una nuova connessione. Memorizzi l'URL del database in una variabile d'ambiente, mai nel codice.

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

Elenco dei record come strumento

Uno strumento list_records con filtri e impaginazione consente all'IA di esplorare i contenuti del database senza eseguire SQL arbitrario. Accetti i parametri di filtro, costruisca una query parametrizzata e applichi sempre un limite al numero di righe. Restituisca i risultati come stringa formattata che il modello possa leggere e citare nella risposta.

@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))]

Esposizione delle righe del database come risorse

I singoli record del database possono essere esposti come risorse MCP usando URI strutturati come db://products/42. Le risorse consentono al client IA di memorizzare nella cache e citare record specifici senza chiamare ogni volta uno strumento. Implementi list_resources per restituire i record più rilevanti e read_resource per recuperarne uno tramite 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}')

Impaginazione di grandi insiemi di risultati

Le tabelle dei database contengono spesso migliaia di righe. Implementi nei Suoi strumenti l'impaginazione basata su cursore, così l'IA potrà richiedere la pagina successiva dei risultati. Utilizzi last_id come cursore stabile, più affidabile dell'impaginazione basata sull'offset, che può saltare righe quando vengono effettuati inserimenti.

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'

Strumento di ricerca full-text

Aggiunga uno strumento di ricerca che utilizzi la ricerca full-text di PostgreSQL, consentendo all'IA di trovare record tramite query in linguaggio naturale anziché attraverso corrispondenze esatte dei campi. È particolarmente utile per cataloghi di prodotti, documentazione e basi di conoscenza.

@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))]

Operazioni di scrittura con conferma

Se il server MCP consente operazioni di scrittura, implementi un flusso di conferma in due passaggi. La prima chiamata allo strumento restituisce un'anteprima delle modifiche da applicare. Un secondo strumento confirm_action(action_id) esegue la modifica. In questo modo si impedisce all'IA di effettuare modifiche irreversibili a partire da un singolo prompt senza che l'utente ne sia consapevole.

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}')]

Filtraggio dei dati sensibili

Non esponga mai colonne sensibili come password, chiavi API o dati personali, a meno che il server non ne abbia esplicitamente bisogno per il caso d'uso aziendale. Utilizzi clausole SELECT che escludano le colonne sensibili e applichi filtri a livello di riga in base alle autorizzazioni dell'utente autenticato. Il server MCP è l'ultima linea di difesa prima che i dati raggiungano l'IA.

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 {}

Logging di audit per l'accesso al database

Ogni accesso al database tramite il server MCP deve essere registrato per motivi di sicurezza e conformità. Registri lo strumento chiamato, gli argomenti passati, le righe restituite, il timestamp e qualsiasi contesto di sessione o utente disponibile. Questa traccia di audit consente di rilevare schemi anomali e soddisfare i requisiti di conformità.

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

Test del server MCP del database

Testi il server MCP del database a tre livelli: test unitari per le singole funzioni di query, usando un database di test o mock; test di integrazione che avviano il server MCP completo e chiamano gli strumenti tramite il client SDK; e test end-to-end che verificano che Claude Desktop visualizzi gli strumenti e li chiami correttamente. I test automatizzati rilevano le regressioni prima che raggiungano l'IA.

Verifica rapida

Verifichi la Sua comprensione dell'esposizione delle risorse del database tramite MCP.

Riepilogo della lezione

In questa lezione ha imparato che: i server MCP per database agiscono da proxy controllati, con strumenti di query espliciti e operazioni in sola lettura come impostazione predefinita, le righe del database possono essere esposte come risorse MCP con URI strutturati a cui l'IA può fare riferimento e le operazioni di scrittura devono usare un modello di anteprima e successiva conferma per prevenire modifiche indesiderate. Nella prossima lezione proteggeremo il server MCP con l'autenticazione OAuth 2.0 e la convalida degli input.

Domande Frequenti

La lezione «Esporre risorse del database tramite MCP» è gratuita?

Sì — il testo completo di «Esporre risorse del database tramite MCP» è gratuito qui sul web. Per esercitarvi in modo interattivo (un editor di codice integrato e un tutor IA 24/7) e sbloccare il resto del corso AI Engineering Academy, passa a CoddyKit PRO. Il corso AI Engineering Academy include 4 lezioni in totale.

Cosa imparerò in «Esporre risorse del database tramite MCP»?

Crei risorse MCP che forniscano dinamicamente contenuti dal database, esponga strumenti di query che il modello possa chiamare e implementi la paginazione per grandi insiemi di risultati. Eserciti AI Engineering Academy con codice pratico che esegui direttamente nel browser, e un tutor IA 24/7 risponde alle tue domande mentre lavori sulla lezione.

Ho bisogno di esperienza per iniziare AI Engineering Academy?

Non è richiesta alcuna esperienza precedente. AI Engineering Academy su CoddyKit è strutturato per principianti e studenti avanzati, quindi puoi iniziare da qui o dall'inizio e procedere al tuo ritmo. Questa è la lezione 3 di 4.

Quanto tempo richiede la lezione «Esporre risorse del database tramite MCP»?

La maggior parte delle lezioni CoddyKit richiede circa 5–10 minuti. Ogni lezione è breve e interattiva, quindi fai progressi costanti e riprendi esattamente da dove hai lasciato su web e app.

Posso scrivere ed eseguire codice in questa lezione AI Engineering Academy?

Sì. Ogni lezione AI Engineering Academy include un editor di codice integrato, quindi scrivi ed esegui codice reale direttamente nel tuo browser e ricevi feedback istantaneo dall'IA — nessuna configurazione locale necessaria.

Tutte le lezioni di questo corso

  1. Che cos'è MCP e perché è importante
  2. Creare il primo server MCP
  3. Esporre risorse del database tramite MCP
  4. Sicurezza e autenticazione di MCP
← Torna a AI Engineering Academy