·

PostgreSQL MCP với Cursor

Cài đặt PostgreSQL MCP trong Cursor để AI agent có thể truy vấn, kiểm tra và quản lý database ngay trong trình soạn thảo.

Cursor là IDE fork từ VS Code, tích hợp sẵn AI agent mode có thể tự đọc nhiều file, chạy command, và gọi MCP tool trong một luồng làm việc liên tục — khác với việc bạn chat qua lại từng câu một. Khi gắn Postgres MCP vào Cursor, agent mode có thể vừa đọc schema thật, vừa đọc code hiện có trong repo (model, migration, test), rồi sinh code mới khớp hoàn toàn với cả hai nguồn thông tin đó. Bài này hướng dẫn kết nối MCP vào Cursor Agent Mode, sinh ORM model/type definition từ schema sống, viết test assert đúng trạng thái database thật, và các hạn chế/rủi ro cần lưu ý khi dùng trong IDE.

Kết nối Postgres MCP vào Cursor Agent Mode

Cursor hỗ trợ MCP qua file cấu hình .cursor/mcp.json trong project (khuyến nghị, để version control cùng repo) hoặc cấu hình global trong Settings > MCP. Cấu trúc file giống chuẩn MCP phổ biến:

{
  "mcpServers": {
    "postgres-dev": {
      "command": "npx",
      "args": [
        "-y",
        "@modelcontextprotocol/server-postgres",
        "postgres://mcp_readonly:StrongPass123@localhost:5432/app_dev"
      ]
    }
  }
}

Sau khi lưu file, mở Cursor Settings (⌘/Ctrl + Shift + J) > mục MCP, bạn sẽ thấy server postgres-dev xuất hiện với một chấm trạng thái xanh nếu connect thành công. Cursor còn cho phép bật/tắt từng tool riêng lẻ trong một MCP server — hữu ích nếu bạn chỉ muốn agent có quyền list_tables, query (chỉ SELECT) mà không muốn expose tool nào có khả năng chạy DDL, dù server gốc có hỗ trợ.

Để dùng MCP trong Agent Mode, mở chat panel, chuyển sang mode "Agent" (không phải "Ask" — mode Ask thường bị giới hạn không gọi tool), rồi gõ:

@postgres-dev liệt kê các bảng trong schema public

Ký hiệu @ cho phép bạn chỉ định rõ MCP server muốn agent dùng trong prompt, tránh nhầm lẫn khi có nhiều server cùng cấu hình (ví dụ vừa có postgres-dev vừa có postgres-staging).

Mẹo: Trong Cursor Settings > MCP, tắt hẳn (disable) các tool có khả năng viết dữ liệu nếu server MCP bạn dùng có expose chúng, ngay cả khi role database đã là read-only. Đây là lớp phòng thủ kép (defense in depth) — không dựa vào một điểm kiểm soát duy nhất, phòng trường hợp ai đó vô tình đổi connection string sang một role có quyền cao hơn.

Sinh ORM model và type definition từ schema sống

Đây là use case Cursor làm rất tốt vì agent mode có thể đọc cả database thật lẫn cấu trúc code hiện có trong repo (convention đặt tên, style file, ORM đang dùng) cùng lúc, rồi sinh code khớp với cả hai.

Ví dụ với Prisma, prompt:

Kết nối postgres-dev, đọc cấu trúc đầy đủ của bảng "orders" và "order_items"
(column, type, nullable, foreign key). Dựa vào đó, cập nhật file prisma/schema.prisma
hiện có (đọc file này trước để giữ đúng style/convention đang dùng trong project),
thêm model Order và OrderItem khớp chính xác với database thật, bao gồm relation
đúng chiều (@relation) và mapping tên cột nếu cột dùng snake_case còn field Prisma
dùng camelCase.

Vì agent đọc trực tiếp schema thật thay vì đoán theo tên bảng, nó tránh được lỗi rất phổ biến khi người viết tay: quên một cột nullable, sai kiểu numeric thành Float (dẫn đến sai số học với tiền tệ — nên map đúng sang Decimal trong Prisma), hoặc thiếu index unique dẫn đến thiếu constraint tương ứng ở tầng ORM.

Kết quả agent sinh ra thường có dạng:

model Order {
  id         String      @id @default(uuid()) @db.Uuid
  customerId String      @map("customer_id") @db.Uuid
  status     String
  createdAt  DateTime    @map("created_at") @default(now())
  customer   Customer    @relation(fields: [customerId], references: [id])
  items      OrderItem[]

  @@map("orders")
}

model OrderItem {
  id        String  @id @default(uuid()) @db.Uuid
  orderId   String  @map("order_id") @db.Uuid
  productId String  @map("product_id") @db.Uuid
  quantity  Int
  unitPrice Decimal @map("unit_price") @db.Decimal(10, 2)
  order     Order   @relation(fields: [orderId], references: [id])

  @@map("order_items")
}

Với TypeScript thuần (không ORM, dùng query builder như pg hoặc postgres.js), bạn có thể yêu cầu agent sinh type definition tương ứng:

Dựa trên schema bảng orders và order_items vừa đọc, sinh type TypeScript
tương ứng (interface Order, OrderItem), dùng kiểu string cho uuid, number
cho numeric (ghi rõ comment cảnh báo về precision nếu dùng cho tính tiền),
và Date cho timestamp.

Sau khi có model/type, luôn yêu cầu agent chạy lại một câu query thật để xác nhận field mapping đúng — ví dụ query 1 dòng và so field trả về với type vừa định nghĩa, tránh trường hợp model đúng về mặt cấu trúc nhưng agent hiểu sai enum value thực tế đang lưu trong cột status.

Mẹo: Khi sinh model cho cột tiền tệ (money, numeric), luôn nhắc rõ agent không dùng kiểu Float/number của JavaScript cho phép tính toán — chỉ dùng để hiển thị. Dùng Decimal (Prisma) hoặc thư viện decimal riêng để tránh lỗi sai số học dấu phẩy động khi cộng trừ tiền.

Viết test assert đúng trạng thái database thật qua Cursor

Một điểm mạnh khác của Postgres MCP trong IDE: agent có thể viết integration test rồi tự chạy migration/seed, chạy code cần test, rồi query lại database để assert đúng trạng thái thực tế — thay vì chỉ mock data và assert logic thuần.

Prompt ví dụ cho một test integration:

Viết integration test (dùng framework test hiện có trong project — đọc package.json
để biết là Jest hay Vitest) cho function createOrder(customerId, items) trong
src/services/orderService.ts. Test cần:
1. Seed một customer test vào database test (đọc cấu hình DATABASE_URL cho môi trường test)
2. Gọi function createOrder
3. Dùng MCP postgres-dev (đã trỏ vào database test) để query lại bảng orders và
   order_items, assert đúng số dòng, đúng tổng tiền được tính, và đúng foreign key
4. Cleanup dữ liệu test sau khi chạy xong (trong afterEach hoặc transaction rollback)

Agent thường ưu tiên pattern bọc mỗi test trong transaction rồi rollback ở cuối (nếu ORM/driver hỗ trợ), để tránh dữ liệu test tích tụ dần trong database test qua nhiều lần chạy CI:

import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { pool } from "../src/db/pool";
import { createOrder } from "../src/services/orderService";

describe("createOrder", () => {
  let client;

  beforeEach(async () => {
    client = await pool.connect();
    await client.query("BEGIN");
  });

  afterEach(async () => {
    await client.query("ROLLBACK");
    client.release();
  });

  it("creates order with correct total from order items", async () => {
    const { rows: [customer] } = await client.query(
      `INSERT INTO customers (name, email) VALUES ($1, $2) RETURNING id`,
      ["Test Customer", "test@example.com"]
    );

    const order = await createOrder(customer.id, [
      { productId: "prod-1", quantity: 2, unitPrice: 15.5 },
    ]);

    const { rows } = await client.query(
      `SELECT o.id, oi.quantity, oi.unit_price
       FROM orders o JOIN order_items oi ON oi.order_id = o.id
       WHERE o.id = $1`,
      [order.id]
    );

    expect(rows).toHaveLength(1);
    expect(Number(rows[0].unit_price)).toBe(15.5);
    expect(rows[0].quantity).toBe(2);
  });
});

Điều quan trọng cần nhấn mạnh với agent: MCP dùng để agent tự khám phá schema và soạn test đúng cấu trúc, nhưng file test thật (.test.ts) nên tự chạy bằng driver database trong code test suite (như ví dụ trên dùng pool trực tiếp), không nên phụ thuộc vào MCP server tại thời điểm CI chạy — vì MCP là công cụ hỗ trợ agent viết code, không phải một phần runtime của ứng dụng hay test suite.

Mẹo: Luôn yêu cầu agent dùng transaction rollback (hoặc database riêng được reset mỗi lần CI chạy) cho test tương tác database thật — tuyệt đối không để test suite ghi dữ liệu thật vào database dev/staging chung mà không dọn dẹp, vì sẽ làm sai lệch dữ liệu cho các test khác hoặc cho chính bạn khi debug thủ công sau đó.

Hạn chế và lưu ý an toàn khi dùng Postgres MCP trong Cursor

Vài rủi ro và hạn chế thực tế cần cân nhắc trước khi để cả team dùng Postgres MCP trong Cursor như một phần workflow chính thức:

  • Agent mode có thể tự động chạy tool liên tiếp nhiều bước nếu bạn không giới hạn quyền — với connection có quyền viết, một chuỗi hành động sai (ví dụ agent hiểu nhầm yêu cầu "xoá test data" thành "xoá luôn dữ liệu trong bảng chính") có thể xảy ra nhanh hơn bạn kịp phản ứng. Luôn để MCP mặc định dùng role read-only, chỉ bật quyền viết tạm thời khi cần và tắt lại ngay sau.
  • File .cursor/mcp.json nếu commit kèm connection string thật (có password) sẽ lộ credential cho bất kỳ ai có quyền đọc repo — luôn dùng biến môi trường (${DATABASE_URL}) thay vì hardcode, và thêm file vào .gitignore nếu buộc phải để giá trị thật cho môi trường local của từng người.
  • Cursor cache context của file đã đọc, nhưng không cache kết quả MCP tool call giữa các session — mỗi khi mở lại project, agent phải introspect schema lại từ đầu nếu bạn không có file tóm tắt lưu sẵn trong repo, tốn thời gian và token với schema lớn.
  • Không có cơ chế review diff cho thay đổi database như cách Cursor hiển thị diff cho thay đổi code — khi agent chạy một câu UPDATE/INSERT qua MCP, bạn không có "diff view" trực quan để review trước khi confirm, khác hẳn với việc review code thay đổi. Điều này khiến việc có guardrail ở tầng database (role, REVOKE quyền) quan trọng hơn nhiều so với chỉ dựa vào UI approval.
  • Một số phiên bản Cursor giới hạn số lượng tool call MCP liên tiếp trong một lượt agent để tránh loop vô hạn — với task cần introspect rất nhiều bảng, đôi khi agent bị cắt giữa việc và bạn cần nhắc "tiếp tục" thủ công.

Mẹo: Trước khi để Cursor Agent Mode tự động chạy nhiều bước liên tiếp có đụng tới MCP Postgres, kiểm tra kỹ role đang dùng chỉ có quyền SELECT. Đây là cách rẻ nhất và chắc chắn nhất để agent "có tự động cao" vẫn không thể gây hại thật, bất kể agent hiểu sai ý bạn tới đâu.

Tips

  • Luôn version-control .cursor/mcp.json nhưng inject giá trị connection string thật qua biến môi trường, không hardcode password trong file commit lên git.
  • Tận dụng khả năng tắt/mở từng tool riêng trong MCP settings của Cursor để giới hạn agent chỉ dùng đúng tool cần thiết cho từng loại task.
  • Với sinh ORM model, luôn yêu cầu agent đọc file schema hiện có trước khi thêm mới, để giữ đúng convention đặt tên và tránh conflict với model đã tồn tại.
  • Với test tương tác database thật, luôn dùng transaction rollback hoặc database test riêng biệt, tuyệt đối không chạy trên database dev/staging chung không có cơ chế dọn dẹp.
  • Định kỳ review lại danh sách MCP server trong Cursor Settings của toàn team, đảm bảo không ai vô tình để lại một server có quyền viết trỏ vào production.

Mẹo: Thêm một mục checklist review trong quy trình onboarding thành viên mới của team: "kiểm tra .cursor/mcp.json không chứa credential thật" và "xác nhận role MCP Postgres đang dùng là read-only" — những lỗi cấu hình an toàn này rất dễ lặp lại ở mỗi máy mới nếu không có checklist rõ ràng.