Exponera databaseresurser via MCP
Skapa MCP-resurser som dynamiskt tillhandahåller databasinnehåll, exponera frågeverktyg som modellen kan anropa och implementera paginering för stora resultatmängder.
Exponera databaseresurser via MCP är en gratis lektion i AI Engineering Academy på CoddyKit. Detta är lektion 3 av 4. Ni kan läsa hela lektionen gratis nedan och sedan öva praktiskt i webbläsaren med en inbyggd kodredigerare och en AI-handledare som är tillgänglig dygnet runt. Den ingår i lärvägen för AI Engineering Academy, och Era framsteg synkroniseras mellan webben och CoddyKit-appen. Kursen i AI Engineering Academy innehåller totalt 4 lektioner.
Varför databaser behöver MCP
Databaser hör till de mest värdefulla datakällorna för AI-assistenter – men det är för riskabelt att ge en LLM direkt åtkomst till en rå databas. En MCP-server fungerar som en kontrollerad proxy mellan AI:n och databasen och exponerar endast de data och åtgärder som du uttryckligen tillåter, med inbyggd validering, hastighetsbegränsning och granskning.
Utforma MCP-schemat för databasen
Bestäm vad AI:n ska kunna se och göra innan du skriver kod. Definiera vilka tabeller som får läsas, vilka verktyg som eventuellt får utföra skrivåtgärder, vilka data som måste filtreras utifrån användarens kontext och vilka kolumner som ska döljas, till exempel lösenord och personligt identifierbar information (PII). Den här designfasen förhindrar oavsiktlig dataexponering.
- Läsverktyg:
list_records,get_record,search_records - Skrivverktyg (valfritt, begränsat):
create_record,update_record - Förbjudet: körning av rå SQL, ändringar av schemat, systemtabeller
Konfigurera databasanslutningen
Använd asyncpg (PostgreSQL) eller aiosqlite (SQLite) för asynkron databasåtkomst i MCP-servern. Skapa en anslutningspool vid uppstart så att varje verktygsanrop inte behöver öppna en ny anslutning. Lagra databasens URL i en miljövariabel, aldrig i koden.
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 loggerVisa poster som ett verktyg
Ett verktyg som heter list_records och har filtrering och paginering låter AI:n bläddra i databasinnehåll utan att köra godtycklig SQL. Ta emot filterparametrar, bygg en parametriserad fråga och använd alltid en begränsning av antalet rader. Returnera resultaten som en formaterad sträng som modellen kan läsa och hänvisa till i sitt svar.
@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))]Exponera databasrader som resurser
Enskilda dataposter kan exponeras som MCP-resurser med hjälp av strukturerade URI:er som db://products/42. Resurser gör det möjligt för AI-klienten att cacha och hänvisa till specifika poster utan att anropa ett verktyg varje gång. Implementera list_resources för att returnera de mest relevanta posterna och read_resource för att hämta en post via dess 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}')Paginering för stora resultatmängder
Databastabeller innehåller ofta tusentals rader. Implementera markörbaserad paginering i verktygen så att AI:n kan begära nästa resultatsida. Använd last_id som en stabil markör. Det är mer tillförlitligt än offsetbaserad paginering, som kan hoppa över rader när poster infogas.
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'Verktyg för fulltextsökning
Lägg till ett sökverktyg som använder PostgreSQLs fulltextsökning, så att AI:n kan hitta poster med naturliga språkfrågor i stället för exakta fältmatchningar. Det är särskilt användbart för produktkataloger, dokumentation och kunskapsbaser.
@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))]Skrivåtgärder med bekräftelse
Om MCP-servern tillåter skrivåtgärder bör du införa ett bekräftelseförfarande i två steg. Det första verktygsanropet returnerar en förhandsvisning av vad som kommer att ändras. Ett andra verktyg, confirm_action(action_id), genomför ändringen. På så sätt förhindras AI:n från att utföra oåterkalleliga skrivåtgärder utifrån en enda instruktion utan att användaren är medveten om det.
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}')]Filtrera känsliga data
Exponera aldrig känsliga kolumner som lösenord, API-nycklar eller PII om servern inte uttryckligen behöver dem för det aktuella användningsfallet. Använd SELECT-satser som utesluter känsliga kolumner och tillämpa filtrering på radnivå utifrån den autentiserade användarens behörigheter. MCP-servern är den sista försvarslinjen innan data når AI:n.
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 {}Granskningsloggning för databasåtkomst
All databasåtkomst via MCP-servern bör loggas av säkerhets- och efterlevnadsskäl. Registrera vilket verktyg som anropades, vilka argument som skickades, vilka rader som returnerades, tidsstämpeln samt eventuell tillgänglig sessions- eller användarkontext. Detta granskningsspår gör det möjligt att upptäcka avvikande mönster och uppfylla efterlevnadskrav.
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 resultTesta MCP-servern för databasen
Testa MCP-servern för databasen på tre nivåer: enhetstester för enskilda frågefunktioner med hjälp av en testdatabas eller mockar, integrationstester som startar hela MCP-servern och anropar verktyg via SDK-klienten samt end-to-end-tester som verifierar att Claude Desktop ser verktygen och anropar dem korrekt. Automatiserade tester upptäcker regressioner innan de når AI:n.
Snabbtest
Testa dina kunskaper om att exponera databasresurser via MCP.
Sammanfattning av lektionen
I den här lektionen har du lärt dig att MCP-databasservrar fungerar som kontrollerade proxyservrar med uttryckliga frågeverktyg och skrivskyddade åtgärder som standard, att databasrader kan exponeras som MCP-resurser med strukturerade URI:er som AI:n kan hänvisa till samt att skrivåtgärder bör använda ett förhandsgransknings- och bekräftelseförfarande för att förhindra oavsiktliga ändringar. Nästa steg är att säkra MCP-servern med OAuth 2.0-autentisering och indatavalidering.
Lär dig Python med en AI-lärare – gratis
Skriv och kör riktig kod i webbläsaren, få omedelbar hjälp av en AI-lärare dygnet runt och fortsätt där du slutade – på webben eller i appen.
- Kurser
- 30
- Lektioner
- 120
Vanliga frågor
Är lektionen ”Exponera databaseresurser via MCP” gratis?
Ja – hela texten till ”Exponera databaseresurser via MCP” kan läsas gratis här på webben. Om Ni vill öva interaktivt med en inbyggd kodredigerare och en AI-handledare som är tillgänglig dygnet runt och låsa upp resten av kursen i AI Engineering Academy, kan Ni uppgradera till CoddyKit PRO. Kursen i AI Engineering Academy innehåller totalt 4 lektioner.
Vad lär jag mig i ”Exponera databaseresurser via MCP”?
Skapa MCP-resurser som dynamiskt tillhandahåller databasinnehåll, exponera frågeverktyg som modellen kan anropa och implementera paginering för stora resultatmängder. Ni övar på AI Engineering Academy med praktisk kod som körs direkt i webbläsaren, medan en AI-handledare som är tillgänglig dygnet runt svarar på Era frågor under lektionen.
Behöver jag någon erfarenhet för att börja lära mig AI Engineering Academy?
Du behöver inga förkunskaper. Utbildningen i AI Engineering Academy på CoddyKit är upplagd för allt från nybörjare till avancerade elever, så att du kan börja här eller från början och gå fram i din egen takt. Detta är lektion 3 av 4.
Hur lång tid tar lektionen ”Exponera databaseresurser via MCP”?
De flesta CoddyKit-lektioner tar cirka 5–10 minuter. Varje lektion är kort och interaktiv, så att du gör stadiga framsteg och kan fortsätta precis där du slutade – på webben eller i appen.
Kan jag skriva och köra kod i den här AI Engineering Academy-lektionen?
Ja. Varje AI Engineering Academy-lektion innehåller en inbyggd kodredigerare, så att du kan skriva och köra riktig kod direkt i webbläsaren och få omedelbar AI-feedback – utan lokal installation.
Alla lektioner i den här kursen
- Vad är MCP och varför är det viktigt
- Bygg er första MCP-server
- Exponera databaseresurser via MCP
- MCP-säkerhet och autentisering