0Pricing
AI Engineering Academy · Lekcja

Udostępnianie zasobów bazy danych przez MCP

Utwórz zasoby MCP, które dynamicznie udostępniają zawartość bazy danych, udostępnij narzędzia zapytań wywoływane przez model i zaimplementuj stronicowanie dużych zbiorów wyników.

Udostępnianie zasobów bazy danych przez MCP to bezpłatna lekcja AI Engineering Academy na CoddyKit. To lekcja 3 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej AI Engineering Academy, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs AI Engineering Academy zawiera 4 lekcji w sumie.

Dlaczego bazy danych potrzebują MCP

Bazy danych należą do najcenniejszych źródeł danych dla asystentów AI — jednak bezpośredni dostęp LLM do surowej bazy danych jest zbyt niebezpieczny. Serwer MCP działa jako kontrolowany serwer proxy między AI a bazą danych, udostępniając wyłącznie dane i operacje wyraźnie przez Państwa dozwolone, z wbudowaną walidacją, ograniczaniem liczby żądań i audytem.

Projektowanie schematu MCP bazy danych

Przed napisaniem kodu zdecyduj, co AI powinno móc zobaczyć i zrobić. Określ: które tabele można odczytywać, które narzędzia wykonują operacje zapisu (jeśli w ogóle), jakie dane muszą być filtrowane według kontekstu użytkownika oraz które kolumny należy ukryć (hasła, dane osobowe). Ta faza projektowania zapobiega przypadkowemu ujawnieniu danych.

  • Narzędzia odczytu: list_records, get_record, search_records
  • Narzędzia zapisu (opcjonalne, ograniczone): create_record, update_record
  • Zabronione: wykonywanie surowego SQL, modyfikowanie schematu, tabele systemowe

Konfigurowanie połączenia z bazą danych

Użyj asyncpg (PostgreSQL) lub aiosqlite (SQLite) do asynchronicznego dostępu do bazy danych w serwerze MCP. Utwórz pulę połączeń podczas uruchamiania, aby każde wywołanie narzędzia nie otwierało nowego połączenia. Przechowuj adres URL bazy danych w zmiennej środowiskowej, nigdy w kodzie.

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

Udostępnianie rekordów jako narzędzia

Narzędzie list_records z filtrowaniem i paginacją pozwala AI przeglądać zawartość bazy danych bez wykonywania dowolnych zapytań SQL. Przyjmuj parametry filtrów, buduj parametryzowane zapytanie i zawsze stosuj limit liczby wierszy. Zwracaj wyniki jako sformatowany ciąg znaków, który model może odczytać i przywołać w swojej odpowiedzi.

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

Udostępnianie wierszy bazy danych jako zasobów

Pojedyncze rekordy bazy danych można udostępniać jako zasoby MCP za pomocą strukturalnych identyfikatorów URI, takich jak db://products/42. Zasoby pozwalają klientowi AI buforować konkretne rekordy i odwoływać się do nich bez wywoływania narzędzia za każdym razem. Zaimplementuj list_resources, aby zwracać najbardziej istotne rekordy, oraz read_resource, aby pobierać rekord według identyfikatora 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}')

Paginacja dużych zbiorów wyników

Tabele baz danych często zawierają tysiące wierszy. Zaimplementuj w narzędziach paginację opartą na kursorze, aby AI mogło poprosić o następną stronę wyników. Użyj last_id jako stabilnego kursora — jest bardziej niezawodny niż paginacja oparta na przesunięciu, która pomija wiersze dodane w międzyczasie.

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'

Narzędzie wyszukiwania pełnotekstowego

Dodaj narzędzie wyszukiwania korzystające z pełnotekstowego wyszukiwania PostgreSQL, pozwalające AI znajdować rekordy na podstawie zapytań w języku naturalnym, a nie tylko dokładnego dopasowania pól. Jest to szczególnie przydatne w katalogach produktów, dokumentacji i bazach wiedzy.

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

Operacje zapisu z potwierdzeniem

Jeśli serwer MCP zezwala na operacje zapisu, zastosuj dwuetapowy mechanizm potwierdzenia. Pierwsze wywołanie narzędzia zwraca podgląd zmian, które zostaną wprowadzone. Drugie narzędzie confirm_action(action_id) wykonuje zmianę. Zapobiega to wykonywaniu przez AI nieodwracalnych zapisów na podstawie pojedynczego promptu, bez wiedzy użytkownika.

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

Filtrowanie poufnych danych

Nigdy nie udostępniaj poufnych kolumn, takich jak hasła, klucze API lub dane osobowe, chyba że serwer wyraźnie ich potrzebuje w konkretnym zastosowaniu biznesowym. Używaj klauzul SELECT wykluczających poufne kolumny i stosuj filtrowanie na poziomie wierszy na podstawie uprawnień uwierzytelnionego użytkownika. Serwer MCP jest ostatnią linią obrony, zanim dane dotrą do AI.

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

Rejestrowanie audytowe dostępu do bazy danych

Każdy dostęp do bazy danych za pośrednictwem serwera MCP powinien być rejestrowany ze względów bezpieczeństwa i zgodności z przepisami. Zapisuj wywołane narzędzie, przekazane argumenty, zwrócone wiersze, znacznik czasu oraz dostępny kontekst sesji lub użytkownika. Taki ślad audytowy pozwala wykrywać nietypowe wzorce i spełniać wymagania dotyczące zgodności.

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

Testowanie serwera MCP bazy danych

Testuj serwer MCP bazy danych na trzech poziomach: testy jednostkowe poszczególnych funkcji zapytań (z użyciem testowej bazy danych lub atrap), testy integracyjne uruchamiające cały serwer MCP i wywołujące narzędzia za pomocą klienta SDK oraz testy end-to-end sprawdzające, czy Claude Desktop widzi narzędzia i poprawnie je wywołuje. Testy automatyczne wykrywają regresje, zanim dotrą one do AI.

Szybki test

Sprawdź, czy rozumiesz, jak udostępniać zasoby bazy danych za pośrednictwem MCP.

Podsumowanie lekcji

W tej lekcji nauczyłeś się, że: serwery MCP baz danych działają jako kontrolowane serwery proxy z wyraźnie określonymi narzędziami zapytań i domyślnie operacjami tylko do odczytu, wiersze bazy danych można udostępniać jako zasoby MCP ze strukturalnymi identyfikatorami URI, do których AI może się odwoływać, a także że operacje zapisu powinny stosować schemat podglądu, a następnie potwierdzenia, aby zapobiegać niezamierzonym zmianom. Następnie zabezpieczymy serwer MCP uwierzytelnianiem OAuth 2.0 i walidacją danych wejściowych.

Często zadawane pytania

Czy lekcja „Udostępnianie zasobów bazy danych przez MCP” jest bezpłatna?

Tak — pełny tekst „Udostępnianie zasobów bazy danych przez MCP” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu AI Engineering Academy, przejdź na CoddyKit PRO. Kurs AI Engineering Academy zawiera 4 lekcji w sumie.

Co nauczysz się w „Udostępnianie zasobów bazy danych przez MCP”?

Utwórz zasoby MCP, które dynamicznie udostępniają zawartość bazy danych, udostępnij narzędzia zapytań wywoływane przez model i zaimplementuj stronicowanie dużych zbiorów wyników. Ćwiczysz AI Engineering Academy z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.

Czy potrzebuję doświadczenia, aby zacząć AI Engineering Academy?

Nie wymagamy żadnego doświadczenia. AI Engineering Academy w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 3 z 4.

Ile czasu zajmuje lekcja „Udostępnianie zasobów bazy danych przez MCP”?

Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.

Czy mogę pisać i uruchamiać kod w tej lekcji AI Engineering Academy?

Tak. Każda lekcja AI Engineering Academy zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.

Wszystkie lekcje w tym kursie

  1. Czym jest MCP i dlaczego ma znaczenie
  2. Budowanie pierwszego serwera MCP
  3. Udostępnianie zasobów bazy danych przez MCP
  4. Bezpieczeństwo i uwierzytelnianie MCP
← Powrót do AI Engineering Academy