การเปิดให้เข้าถึงทรัพยากรฐานข้อมูลผ่าน MCP
สร้างทรัพยากร MCP ที่ให้บริการเนื้อหาจากฐานข้อมูลแบบไดนามิก เปิดให้โมเดลเรียกใช้เครื่องมือค้นหา และใช้การแบ่งหน้าเพื่อจัดการชุดผลลัพธ์ขนาดใหญ่
การเปิดให้เข้าถึงทรัพยากรฐานข้อมูลผ่าน MCP เป็นบทเรียน AI Engineering Academy ฟรีบน CoddyKit นี่คือบทเรียนที่ 3 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน AI Engineering Academy และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส AI Engineering Academy มีบทเรียนทั้งหมด 4 บทเรียน
เหตุผลที่ฐานข้อมูลต้องใช้ MCP
ฐานข้อมูลเป็นหนึ่งในแหล่งข้อมูลที่มีคุณค่ามากที่สุดสำหรับผู้ช่วย AI แต่การให้สิทธิ์เข้าถึงฐานข้อมูลโดยตรงแก่ LLM นั้นอันตรายเกินไป เซิร์ฟเวอร์ MCP ทำหน้าที่เป็น พร็อกซีที่ควบคุมได้ ระหว่าง AI กับฐานข้อมูล โดยเปิดเผยเฉพาะข้อมูลและการดำเนินการที่คุณอนุญาตไว้อย่างชัดเจน พร้อมการตรวจสอบความถูกต้อง การจำกัดอัตราการเรียกใช้ และการตรวจสอบติดตามในตัว
การออกแบบสคีมา MCP สำหรับฐานข้อมูล
ก่อนเขียนโค้ด ให้ตัดสินใจก่อนว่า AI ควรดูและดำเนินการอะไรได้บ้าง ให้กำหนดว่าตารางใดอ่านได้ เครื่องมือใดใช้ดำเนินการเขียนข้อมูล (ถ้ามี) ข้อมูลใดต้องกรองตามบริบทของผู้ใช้ และคอลัมน์ใดควรซ่อน เช่น รหัสผ่านและ PII ขั้นตอนการออกแบบนี้ช่วยป้องกันการเปิดเผยข้อมูลโดยไม่ได้ตั้งใจ
- เครื่องมืออ่าน:
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 ที่มีการกรองและการแบ่งหน้า ช่วยให้ AI เรียกดูเนื้อหาในฐานข้อมูลได้โดยไม่ต้องเรียกใช้ 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))]การเปิดเผยแถวฐานข้อมูลเป็น Resource
สามารถเปิดเผยระเบียนแต่ละรายการในฐานข้อมูลเป็น Resource ของ MCP โดยใช้ URI ที่มีโครงสร้าง เช่น db://products/42 Resource ช่วยให้ไคลเอ็นต์ AI แคชและอ้างอิงระเบียนเฉพาะได้โดยไม่ต้องเรียกใช้เครื่องมือทุกครั้ง นำ 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}')การแบ่งหน้าสำหรับชุดผลลัพธ์ขนาดใหญ่
ตารางฐานข้อมูลมักมีหลายพันแถว ให้นำการแบ่งหน้าโดยใช้เคอร์เซอร์ไปใช้ในเครื่องมือ เพื่อให้ AI ขอหน้าผลลัพธ์ถัดไปได้ ใช้ 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 เพื่อให้ AI ค้นหาระเบียนด้วยคำค้นภาษาธรรมชาติแทนการจับคู่ค่าฟิลด์แบบตรงตัว วิธีนี้มีประโยชน์อย่างยิ่งสำหรับแค็ตตาล็อกผลิตภัณฑ์ เอกสาร และฐานความรู้
@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) ครั้งที่สองจะดำเนินการเปลี่ยนแปลง รูปแบบนี้ช่วยป้องกันไม่ให้ AI ดำเนินการเขียนข้อมูลที่ย้อนกลับไม่ได้จาก Prompt เดียวโดยที่ผู้ใช้ไม่ทราบ
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 หรือ PII เว้นแต่เซิร์ฟเวอร์จะจำเป็นต้องใช้ข้อมูลดังกล่าวตามกรณีทางธุรกิจอย่างชัดเจน ใช้ส่วนคำสั่ง SELECT ที่ตัดคอลัมน์ละเอียดอ่อนออก และใช้การกรองระดับแถวตามสิทธิ์ของผู้ใช้ที่ผ่านการยืนยันตัวตน เซิร์ฟเวอร์ MCP ของคุณคือแนวป้องกันสุดท้ายก่อนที่ข้อมูลจะไปถึง 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 {}การบันทึกเพื่อตรวจสอบการเข้าถึงฐานข้อมูล
ควรบันทึกการเข้าถึงฐานข้อมูลทุกครั้งผ่านเซิร์ฟเวอร์ 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 เห็นเครื่องมือและเรียกใช้ได้อย่างถูกต้อง การทดสอบอัตโนมัติช่วยตรวจจับการถดถอยก่อนที่จะไปถึง AI
ตรวจสอบความเข้าใจ
ทดสอบความเข้าใจเกี่ยวกับการเปิดเผย Resource ของฐานข้อมูลผ่าน MCP
สรุปบทเรียน
ในบทเรียนนี้ คุณได้เรียนรู้ว่า เซิร์ฟเวอร์ฐานข้อมูล MCP ทำหน้าที่เป็นพร็อกซีที่ควบคุมได้ โดยมีเครื่องมือคำค้นที่กำหนดไว้อย่างชัดเจนและการดำเนินการแบบอ่านอย่างเดียวเป็นค่าเริ่มต้น แถวฐานข้อมูลสามารถเปิดเผยเป็น Resource ของ MCP ด้วย URI ที่มีโครงสร้างเพื่อให้ AI ใช้อ้างอิง และ การดำเนินการเขียนควรใช้รูปแบบแสดงตัวอย่างก่อนแล้วจึงยืนยัน เพื่อป้องกันการเปลี่ยนแปลงที่ไม่ได้ตั้งใจ ต่อไป เราจะรักษาความปลอดภัยให้เซิร์ฟเวอร์ MCP ด้วยการตรวจสอบสิทธิ์ OAuth 2.0 และการตรวจสอบข้อมูลนำเข้า
คำถามที่พบบ่อย
บทเรียน “การเปิดให้เข้าถึงทรัพยากรฐานข้อมูลผ่าน MCP” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “การเปิดให้เข้าถึงทรัพยากรฐานข้อมูลผ่าน MCP” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส AI Engineering Academy ให้อัปเกรดเป็น CoddyKit PRO คอร์ส AI Engineering Academy มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “การเปิดให้เข้าถึงทรัพยากรฐานข้อมูลผ่าน MCP”
สร้างทรัพยากร MCP ที่ให้บริการเนื้อหาจากฐานข้อมูลแบบไดนามิก เปิดให้โมเดลเรียกใช้เครื่องมือค้นหา และใช้การแบ่งหน้าเพื่อจัดการชุดผลลัพธ์ขนาดใหญ่ คุณปฏิบัติ AI Engineering Academy ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน AI Engineering Academy หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน AI Engineering Academy บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 3 จากทั้งหมด 4 บทเรียน
บทเรียน “การเปิดให้เข้าถึงทรัพยากรฐานข้อมูลผ่าน MCP” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน AI Engineering Academy นี้ได้ไหม
ได้ บทเรียน AI Engineering Academy ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- MCP คืออะไรและสำคัญอย่างไร
- การสร้างเซิร์ฟเวอร์ MCP แรกของคุณ
- การเปิดให้เข้าถึงทรัพยากรฐานข้อมูลผ่าน MCP
- ความปลอดภัยและการยืนยันตัวตนของ MCP