Trung cấpHướng dẫnClaude DesktopNguồn: Anthropic

MCP Server cho PostgreSQL/MySQL — Claude truy vấn database trực tiếp

Nghe bài viết
00:00

Điểm nổi bật

Nhấn để đến mục tương ứng

  1. 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. 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. 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. 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. 5 "Khách hàng nào đã mua hơn 5 lần trong quý này?" 4.
white printer paper on brown wooden table

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:

  1. 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"
  2. Claude phân tích câu hỏi, xem schema database (thông qua MCP), viết câu SQL phù hợp
  3. Claude gửi câu SQL đến MCP Server để thực thi
  4. MCP Server kiểm tra quyền, chạy query trên database và trả kết quả
  5. 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:

  1. Claude gọi "list_tables" để biết database có những bảng nào
  2. Claude gọi "describe_table" cho các bảng liên quan đến câu hỏi
  3. Dựa trên schema, Claude viết câu SQL phù hợp
  4. 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.

Tính năng liên quan:MCP ServerDatabase QuerySchema IntrospectionNatural Language SQL

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ẻ.

Bình luận (0)
Ảnh đại diện
Đăng nhập để bình luận...
Đăng nhập để bình luận
  • Đang tải bình luận...

Đăng ký nhận bản tin

Nhận bài viết hay nhất về sản phẩm và vận hành, gửi thẳng vào hộp thư của bạn.

Bảo mật thông tin. Hủy đăng ký bất cứ lúc nào. Chính sách bảo mật.