MCP Server cho PostgreSQL/MySQL — Claude truy vấn database trực tiếp
Điểm nổi bật
Nhấn để đến mục tương ứng
- 1 AND TABLE_NAME = ?", [process.env.DB_NAME, table_name] ); Bảo mật kết nối: Tạo user read-only Nguyên tắc quan trọng nhất: MCP Server chỉ nên kết nối database bằng user read-only.
- 2 Tiếp theo, hãy tìm hiểu cách bảo mật MCP Server toàn diện hơn cho môi trường production hoặc khám phá các MCP Server khác cho file system, API và dịch vụ web.
- 3 "Tổng doanh thu tháng này so với tháng trước tăng bao nhiêu phần trăm?" 3.
- 4 Model Context Protocol (MCP) cho phép Claude kết nối với các nguồn dữ liệu bên ngoài thông qua các server trung gian.
- 5 "Khách hàng nào đã mua hơn 5 lần trong quý này?" 4.
Model Context Protocol (MCP) cho phép Claude kết nối với các nguồn dữ liệu bên ngoài thông qua các server trung gian. Một trong những ứng dụng thực tiễn nhất là cho phép Claude truy vấn database trực tiếp — bạn hỏi bằng tiếng Việt hoặc tiếng Anh, Claude tự viết SQL, thực thi và trả về kết quả đã phân tích. Bài viết này hướng dẫn thiết lập MCP Server cho PostgreSQL và MySQL với các biện pháp bảo mật cần thiết.
MCP Database Server là gì?
MCP Database Server là một ứng dụng trung gian chạy trên máy tính của bạn (hoặc server), đóng vai trò cầu nối giữa Claude và database. Luồng hoạt động:
- Bạn hỏi Claude bằng ngôn ngữ tự nhiên: "Cho tôi top 10 khách hàng có doanh thu cao nhất quý này"
- Claude phân tích câu hỏi, xem schema database (thông qua MCP), viết câu SQL phù hợp
- Claude gửi câu SQL đến MCP Server để thực thi
- MCP Server kiểm tra quyền, chạy query trên database và trả kết quả
- Claude nhận kết quả, phân tích và trình bày cho bạn dưới dạng dễ hiểu
Cài đặt MCP Server cho PostgreSQL
Yêu cầu hệ thống
- Node.js 18 trở lên
- PostgreSQL 12 trở lên (local hoặc remote)
- Claude Desktop (phiên bản hỗ trợ MCP)
Bước 1: Tạo project MCP Server
mkdir mcp-postgres-server
cd mcp-postgres-server
npm init -y
npm install @modelcontextprotocol/sdk pg
Bước 2: Viết MCP Server
// server.js
const { McpServer } = require("@modelcontextprotocol/sdk/server/mcp.js");
const { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js");
const { Pool } = require("pg");
const { z } = require("zod");
const pool = new Pool({
host: process.env.DB_HOST || "localhost",
port: parseInt(process.env.DB_PORT || "5432"),
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
max: 5
});
const server = new McpServer({
name: "postgres-query",
version: "1.0.0"
});
// Tool: Xem danh sách bảng
server.tool(
"list_tables",
"Liệt kê tất cả bảng trong database",
{},
async () => {
const result = await pool.query(
"SELECT table_name, table_type " +
"FROM information_schema.tables " +
"WHERE table_schema = 'public' " +
"ORDER BY table_name"
);
return {
content: [{
type: "text",
text: JSON.stringify(result.rows, null, 2)
}]
};
}
);
// Tool: Xem cấu trúc bảng
server.tool(
"describe_table",
"Xem cấu trúc chi tiết của một bảng (columns, types, constraints)",
{ table_name: z.string().describe("Tên bảng cần xem cấu trúc") },
async ({ table_name }) => {
// Validate table name chống SQL injection
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(table_name)) {
return { content: [{ type: "text", text: "Tên bảng không hợp lệ." }] };
}
const result = await pool.query(
"SELECT column_name, data_type, is_nullable, " +
"column_default, character_maximum_length " +
"FROM information_schema.columns " +
"WHERE table_schema = 'public' AND table_name = $1 " +
"ORDER BY ordinal_position",
[table_name]
);
return {
content: [{
type: "text",
text: JSON.stringify(result.rows, null, 2)
}]
};
}
);
// Tool: Thực thi query READ-ONLY
server.tool(
"run_query",
"Thực thi câu SQL read-only (SELECT) trên database. KHÔNG hỗ trợ INSERT, UPDATE, DELETE.",
{
sql: z.string().describe("Câu SQL cần thực thi (chỉ SELECT)"),
params: z.array(z.any()).optional()
.describe("Tham số cho prepared statement")
},
async ({ sql, params }) => {
// Kiểm tra read-only
const normalized = sql.trim().toUpperCase();
const forbidden = ["INSERT", "UPDATE", "DELETE", "DROP",
"CREATE", "ALTER", "TRUNCATE", "GRANT", "REVOKE"];
for (const keyword of forbidden) {
if (normalized.startsWith(keyword)) {
return {
content: [{
type: "text",
text: "Chỉ cho phép câu lệnh SELECT. Các thao tác ghi không được hỗ trợ."
}]
};
}
}
// Giới hạn kết quả
if (!normalized.includes("LIMIT")) {
sql = sql.replace(/;?s*$/, " LIMIT 100;");
}
try {
const result = await pool.query(sql, params || []);
return {
content: [{
type: "text",
text: `Kết quả (${result.rowCount} dòng):\n${JSON.stringify(result.rows, null, 2)}`
}]
};
} catch (error) {
return {
content: [{
type: "text",
text: `Lỗi thực thi query: ${error.message}`
}]
};
}
}
);
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
main();
Bước 3: Cấu hình Claude Desktop
Thêm MCP Server vào file cấu hình Claude Desktop:
// Trên macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
// Trên Windows: %APPDATA%/Claude/claude_desktop_config.json
{
"mcpServers": {
"postgres": {
"command": "node",
"args": ["/path/to/mcp-postgres-server/server.js"],
"env": {
"DB_HOST": "localhost",
"DB_PORT": "5432",
"DB_NAME": "my_database",
"DB_USER": "readonly_user",
"DB_PASSWORD": "secure_password"
}
}
}
}
Cài đặt cho MySQL
Quy trình tương tự PostgreSQL với một vài thay đổi:
# Cài đặt dependency cho MySQL
npm install @modelcontextprotocol/sdk mysql2
Thay đổi chính trong code:
// Thay thế phần kết nối PostgreSQL
const mysql = require("mysql2/promise");
const pool = mysql.createPool({
host: process.env.DB_HOST || "localhost",
port: parseInt(process.env.DB_PORT || "3306"),
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
connectionLimit: 5
});
// Thay đổi query schema introspection cho MySQL
// Liệt kê bảng:
const [tables] = await pool.query(
"SELECT TABLE_NAME, TABLE_TYPE FROM information_schema.TABLES WHERE TABLE_SCHEMA = ?",
[process.env.DB_NAME]
);
// Xem cấu trúc bảng:
const [columns] = await pool.query(
"SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_DEFAULT FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?",
[process.env.DB_NAME, table_name]
);
Bảo mật kết nối: Tạo user read-only
Nguyên tắc quan trọng nhất: MCP Server chỉ nên kết nối database bằng user read-only. Tuyệt đối không dùng user root hoặc user có quyền ghi.
PostgreSQL: Tạo read-only user
-- Tạo role read-only
CREATE ROLE mcp_readonly LOGIN PASSWORD 'strong_password_here';
-- Cấp quyền kết nối
GRANT CONNECT ON DATABASE my_database TO mcp_readonly;
-- Cấp quyền đọc tất cả bảng hiện tại
GRANT USAGE ON SCHEMA public TO mcp_readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO mcp_readonly;
-- Cấp quyền đọc bảng tạo trong tương lai
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT ON TABLES TO mcp_readonly;
-- Giới hạn số kết nối đồng thời
ALTER ROLE mcp_readonly CONNECTION LIMIT 5;
-- Giới hạn thời gian thực thi query (tránh query nặng)
ALTER ROLE mcp_readonly SET statement_timeout = '30s';
MySQL: Tạo read-only user
-- Tạo user read-only
CREATE USER 'mcp_readonly'@'localhost'
IDENTIFIED BY 'strong_password_here';
-- Cấp quyền SELECT trên toàn bộ database
GRANT SELECT ON my_database.* TO 'mcp_readonly'@'localhost';
-- Giới hạn tài nguyên
ALTER USER 'mcp_readonly'@'localhost'
WITH MAX_QUERIES_PER_HOUR 1000
MAX_CONNECTIONS_PER_HOUR 100
MAX_USER_CONNECTIONS 5;
FLUSH PRIVILEGES;
Schema Introspection — Claude hiểu cấu trúc database
Để Claude viết SQL chính xác, nó cần hiểu cấu trúc database. Đó là lý do tool "list_tables" và "describe_table" rất quan trọng. Khi bạn hỏi Claude, quy trình diễn ra như sau:
- Claude gọi "list_tables" để biết database có những bảng nào
- Claude gọi "describe_table" cho các bảng liên quan đến câu hỏi
- Dựa trên schema, Claude viết câu SQL phù hợp
- Claude gọi "run_query" để thực thi và nhận kết quả
Bạn có thể bổ sung thêm tool cho schema introspection chi tiết hơn:
// Tool bổ sung: Xem foreign keys và relationships
server.tool(
"list_relationships",
"Xem quan hệ giữa các bảng (foreign keys)",
{ table_name: z.string().optional() },
async ({ table_name }) => {
let sql = `
SELECT
tc.table_name AS source_table,
kcu.column_name AS source_column,
ccu.table_name AS target_table,
ccu.column_name AS target_column
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
JOIN information_schema.constraint_column_usage ccu
ON ccu.constraint_name = tc.constraint_name
WHERE tc.constraint_type = 'FOREIGN KEY'
`;
const params = [];
if (table_name) {
sql += " AND tc.table_name = $1";
params.push(table_name);
}
const result = await pool.query(sql, params);
return {
content: [{
type: "text",
text: JSON.stringify(result.rows, null, 2)
}]
};
}
);
// Tool bổ sung: Xem indexes
server.tool(
"list_indexes",
"Liệt kê indexes của một bảng (hữu ích cho tối ưu query)",
{ table_name: z.string() },
async ({ table_name }) => {
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(table_name)) {
return { content: [{ type: "text", text: "Tên bảng không hợp lệ." }] };
}
const result = await pool.query(`
SELECT indexname, indexdef
FROM pg_indexes
WHERE tablename = $1
`, [table_name]);
return {
content: [{
type: "text",
text: JSON.stringify(result.rows, null, 2)
}]
};
}
);
Tạo và thực thi query bằng ngôn ngữ tự nhiên
Sau khi cấu hình xong, bạn có thể hỏi Claude bằng tiếng Việt. Một số ví dụ thực tế:
Ví dụ các câu hỏi bạn có thể hỏi Claude:
1. "Cho tôi danh sách 10 đơn hàng mới nhất hôm nay"
2. "Tổng doanh thu tháng này so với tháng trước tăng bao nhiêu phần trăm?"
3. "Khách hàng nào đã mua hơn 5 lần trong quý này?"
4. "Sản phẩm nào có tỷ lệ hoàn trả cao nhất?"
5. "Thống kê số đơn hàng theo từng tỉnh thành, sắp xếp giảm dần"
Claude sẽ tự động:
- Xem schema để hiểu cấu trúc bảng
- Viết câu SQL phù hợp
- Thực thi và phân tích kết quả
- Trình bày dưới dạng bảng hoặc tóm tắt dễ hiểu
Format kết quả trả về
Claude tự động format kết quả nhưng bạn có thể yêu cầu cụ thể:
Truy vấn doanh thu theo tháng trong năm nay.
Trình bày kết quả dưới dạng:
1. Bảng tổng hợp có cột: Tháng | Doanh thu | Số đơn | Giá trị TB/đơn
2. So sánh tháng hiện tại với tháng trước (tăng/giảm %)
3. Tháng có doanh thu cao nhất và thấp nhất
4. Xu hướng chung (tăng/giảm/ổn định)
Kiểm soát truy cập nâng cao
Ngoài read-only user, bạn có thể thêm các lớp bảo mật trong MCP Server:
Danh sách bảng được phép truy vấn
// Chỉ cho phép truy vấn một số bảng nhất định
const ALLOWED_TABLES = [
"orders", "products", "customers",
"order_items", "categories"
];
// Bảng chứa dữ liệu nhạy cảm - KHÔNG cho phép
const BLOCKED_TABLES = [
"users", "passwords", "api_keys",
"payment_details", "personal_info"
];
function validateTableAccess(sql) {
const normalized = sql.toLowerCase();
for (const table of BLOCKED_TABLES) {
if (normalized.includes(table)) {
return {
allowed: false,
reason: `Bảng "${table}" chứa dữ liệu nhạy cảm và không được phép truy vấn.`
};
}
}
return { allowed: true };
}
Giới hạn kết quả và thời gian
// Cấu hình giới hạn
const QUERY_LIMITS = {
maxRows: 500, // Tối đa 500 dòng kết quả
timeoutMs: 30000, // Timeout 30 giây
maxQueryLength: 2000 // Độ dài query tối đa 2000 ký tự
};
function enforceQueryLimits(sql) {
if (sql.length > QUERY_LIMITS.maxQueryLength) {
throw new Error("Query quá dài. Giới hạn " +
QUERY_LIMITS.maxQueryLength + " ký tự.");
}
// Đảm bảo có LIMIT
const upper = sql.toUpperCase();
if (!upper.includes("LIMIT")) {
sql = sql.replace(/;?s*$/, ` LIMIT ${QUERY_LIMITS.maxRows};`);
}
return sql;
}
Giới hạn về hiệu năng
Một số lưu ý khi cho phép Claude truy vấn database trực tiếp:
- Query phức tạp: Claude có thể tạo ra các query với nhiều JOIN hoặc subquery lồng nhau, gây tải cho database. Sử dụng statement_timeout ở mức database để tự động kill query chạy quá lâu.
- Full table scan: Với bảng lớn (hàng triệu dòng), Claude có thể vô tình viết query không dùng index. Tool "list_indexes" giúp Claude biết index nào tồn tại để viết query tối ưu hơn.
- Kết quả lớn: Luôn enforce LIMIT để tránh trả về hàng nghìn dòng. Claude chỉ cần mẫu đại diện để phân tích.
- Connection pool: Giới hạn số connection từ MCP Server đến database (5 connection trong ví dụ trên) để không chiếm tài nguyên production.
Sử dụng thực tế: Database production vs staging
Khuyến nghị mạnh mẽ: Kết nối MCP Server vào read replica hoặc database staging, không kết nối trực tiếp vào production primary.
// Cấu hình kết nối read replica
{
"mcpServers": {
"postgres-readonly": {
"command": "node",
"args": ["/path/to/server.js"],
"env": {
"DB_HOST": "read-replica.example.com",
"DB_PORT": "5432",
"DB_NAME": "my_database",
"DB_USER": "mcp_readonly",
"DB_PASSWORD": "secure_password"
}
}
}
}
Audit logging
Ghi log mọi query được thực thi qua MCP Server để kiểm soát và debug:
const fs = require("fs");
const path = require("path");
function logQuery(sql, params, rowCount, duration) {
const entry = {
timestamp: new Date().toISOString(),
sql: sql,
params: params || [],
rowCount: rowCount,
durationMs: duration
};
const logFile = path.join(__dirname, "query-log.jsonl");
fs.appendFileSync(logFile, JSON.stringify(entry) + "\n");
}
// Sử dụng trong tool run_query
const start = Date.now();
const result = await pool.query(sql, params || []);
logQuery(sql, params, result.rowCount, Date.now() - start);
Xử lý lỗi và edge cases
Khi cho phép Claude truy vấn database, cần xử lý kỹ các tình huống lỗi:
Timeout handling
// Bọc query trong timeout
async function queryWithTimeout(sql, params, timeoutMs = 30000) {
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error("Query timeout")), timeoutMs)
);
try {
const result = await Promise.race([
pool.query(sql, params),
timeoutPromise
]);
return result;
} catch (error) {
if (error.message === "Query timeout") {
return {
content: [{
type: "text",
text: "Query vượt quá thời gian cho phép (30 giây). Hãy thử query đơn giản hơn hoặc thêm điều kiện WHERE để giảm phạm vi dữ liệu."
}]
};
}
throw error;
}
}
Xử lý kết nối database mất
// Kiểm tra kết nối trước khi query
pool.on("error", (err) => {
console.error("Database pool error:", err.message);
});
async function healthCheck() {
try {
await pool.query("SELECT 1");
return { status: "connected" };
} catch (error) {
return { status: "disconnected", error: error.message };
}
}
// Expose health check như một MCP tool
server.tool(
"check_connection",
"Kiểm tra kết nối database có hoạt động không",
{},
async () => {
const health = await healthCheck();
return {
content: [{
type: "text",
text: JSON.stringify(health)
}]
};
}
);
Mở rộng: Thêm tool phân tích dữ liệu
Ngoài query thuần, bạn có thể thêm các tool phân tích dữ liệu cao cấp hơn:
// Tool: Thống kê tổng quan bảng
server.tool(
"table_stats",
"Lấy thống kê tổng quan của một bảng: số dòng, min/max/avg các cột số",
{ table_name: z.string() },
async ({ table_name }) => {
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(table_name)) {
return { content: [{ type: "text", text: "Tên bảng không hợp lệ." }] };
}
const countResult = await pool.query(
`SELECT COUNT(*) as total_rows FROM ${table_name}`
);
// Lấy thống kê các cột số
const columnsResult = await pool.query(`
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_name = $1
AND data_type IN ('integer', 'bigint', 'numeric', 'real', 'double precision')
`, [table_name]);
const stats = { total_rows: countResult.rows[0].total_rows };
for (const col of columnsResult.rows) {
const statResult = await pool.query(`
SELECT MIN(${col.column_name}) as min_val,
MAX(${col.column_name}) as max_val,
AVG(${col.column_name})::numeric(10,2) as avg_val
FROM ${table_name}
`);
stats[col.column_name] = statResult.rows[0];
}
return {
content: [{ type: "text", text: JSON.stringify(stats, null, 2) }]
};
}
);
Các trường hợp sử dụng phổ biến
- Data analyst: Hỏi Claude phân tích dữ liệu bán hàng, khách hàng, marketing mà không cần biết SQL
- Customer support: Tra cứu nhanh thông tin đơn hàng, khách hàng qua ngôn ngữ tự nhiên
- Product manager: Lấy số liệu sản phẩm, tỷ lệ chuyển đổi, retention mà không cần nhờ dev
- CEO/Manager: Xem báo cáo doanh thu, tăng trưởng trực tiếp từ database theo thời gian thực
Bước tiếp theo
Bạn đã biết cách thiết lập MCP Server cho PostgreSQL và MySQL với các biện pháp bảo mật cần thiết. Tiếp theo, hãy tìm hiểu cách bảo mật MCP Server toàn diện hơn cho môi trường production hoặc khám phá các MCP Server khác cho file system, API và dịch vụ web. Xem thêm tại Thư viện Ứng dụng Claude.
Bai viet co huu ich khong?
Bản quyền thuộc về tác giả. Vui lòng dẫn nguồn khi chia sẻ.






