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 loggerUdostę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 resultTestowanie 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
- Czym jest MCP i dlaczego ma znaczenie
- Budowanie pierwszego serwera MCP
- Udostępnianie zasobów bazy danych przez MCP
- Bezpieczeństwo i uwierzytelnianie MCP