Tutorial MCP: Hubungkan Database PostgreSQL ke AI Agent Lokal
Poin Kunci Artikel Ini:
- Node.js v18.0.0 atau versi lebih baru
- Instance PostgreSQL berjalan (lokal atau cloud)
- Package manager (npm, pnpm, atau yarn)
1. Arsitektur Model Context Protocol (MCP) untuk Database
Large Language Model (LLM) memiliki batasan context window dan knowledge cutoff. Mengirim data database secara manual via prompt tidak efisien dan berisiko membocorkan konteks sensitif. Model Context Protocol (MCP) memecahkan masalah ini melalui standar arsitektur client-server terbuka buatan Anthropic.
MCP mendefinisikan protokol interaksi terstandarisasi. AI Agent bertindak sebagai MCP Client. Aplikasi pengelola koneksi database bertindak sebagai MCP Server. Komunikasi antar proses terjadi melalui transmisi Standard Input/Output (stdio) atau Server-Sent Events (SSE).
- Akses Data Real-Time: AI Client membaca state database terbaru tanpa skenario ETL (Extract, Transform, Load) eksternal.
- Isolasi Keamanan: AI Client tidak memegang kredensial koneksi DB langsung. MCP Server membatasi akses melalui antarmuka tool tervalidasi.
- Efisiensi Konteks: AI Agent mengambil skema dan baris data relevan secara dinamis sesuai kebutuhan query user.
2. Persiapan Environment dan Dependensi
Prasyarat sistem sebelum implementasi:
- Node.js v18.0.0 atau versi lebih baru
- Instance PostgreSQL berjalan (lokal atau cloud)
- Package manager (npm, pnpm, atau yarn)
- Claude Desktop App atau MCP Inspector untuk pengujian
Eksekusi urutan perintah terminal berikut untuk menginisialisasi direktori proyek TypeScript:
mkdir mcp-postgres-server
cd mcp-postgres-server
npm init -y
npm install @modelcontextprotocol/sdk pg zod dotenv
npm install --save-dev typescript @types/node @types/pg tsx
npx tsc --initKonfigurasi file tsconfig.json untuk mendukung modul NodeNext dan target ES2022:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"]
}3. Konfigurasi Keamanan PostgreSQL (Principle of Least Privilege)
Keamanan database mewajibkan pemisahan hak akses. Jangan pernah menggunakan akun superuser (seperti user postgres) dalam integrasi MCP Agent.
Jalankan skrip SQL berikut pada instance PostgreSQL untuk membuat role khusus berkemampuan read-only:
-- Membuat role khusus AI Agent
CREATE ROLE ai_readonly WITH LOGIN PASSWORD 'PasswordAmanAI2025!';
-- Memberikan hak akses koneksi ke database target
GRANT CONNECT ON DATABASE db_bisnis TO ai_readonly;
-- Pindah ke database target lalu berikan hak penggunaan skema public
\c db_bisnis;
GRANT USAGE ON SCHEMA public TO ai_readonly;
-- Memberikan hak akses SELECT pada seluruh tabel saat ini dan masa depan
GRANT SELECT ON ALL TABLES IN SCHEMA public TO ai_readonly;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON ALL TABLES TO ai_readonly;Buat file .env di akar proyek untuk menyimpan variabel lingkungan koneksi database:
DATABASE_URL=postgres://ai_readonly:PasswordAmanAI2025!@localhost:5432/db_bisnis4. Membangun MCP Server TypeScript
Buat berkas src/index.ts. Kode berikut mendefinisikan dua tool utama: get_database_schema untuk inspeksi struktur tabel dan execute_read_query untuk eksekusi query SELECT terisolasi dengan validasi ketat.
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool
} from "@modelcontextprotocol/sdk/types.js";
import pkg from "pg";
const { Pool } = pkg;
import dotenv from "dotenv";
dotenv.config();
const connectionString = process.env.DATABASE_URL;
if (!connectionString) {
console.error("FATAL: DATABASE_URL tidak ditemukan di environment variable.");
process.exit(1);
}
const dbPool = new Pool({
connectionString,
max: 5,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 5000,
});
const READ_SCHEMA_TOOL: Tool = {
name: "get_database_schema",
description: "Mengambil daftar tabel, nama kolom, dan tipe data dari database PostgreSQL.",
inputSchema: {
type: "object",
properties: {},
required: []
}
};
const EXECUTE_QUERY_TOOL: Tool = {
name: "execute_read_query",
description: "Mengeksekusi query SQL SELECT yang aman untuk membaca data. Hanya menerima statement SELECT.",
inputSchema: {
type: "object",
properties: {
sql: {
type: "string",
description: "Query SQL SELECT yang valid."
}
},
required: ["sql"]
}
};
const server = new Server(
{
name: "mcp-postgres-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [READ_SCHEMA_TOOL, EXECUTE_QUERY_TOOL],
};
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
if (name === "get_database_schema") {
try {
const queryText = `
SELECT
table_name,
column_name,
data_type
FROM
information_schema.columns
WHERE
table_schema = 'public'
ORDER BY
table_name, ordinal_position;
`;
const result = await dbPool.query(queryText);
return {
content: [
{
type: "text",
text: JSON.stringify(result.rows, null, 2),
},
],
};
} catch (error: any) {
return {
isError: true,
content: [
{
type: "text",
text: `Gagal membaca skema: ${error.message}`,
},
],
};
}
}
if (name === "execute_read_query") {
const sql = String(args?.sql || "").trim();
const normalizedSql = sql.toUpperCase();
if (!normalizedSql.startsWith("SELECT")) {
return {
isError: true,
content: [
{
type: "text",
text: "Akses ditolak: Hanya query SELECT yang diizinkan.",
},
],
};
}
const forbiddenKeywords = ["INSERT", "UPDATE", "DELETE", "DROP", "ALTER", "TRUNCATE", "GRANT", "REVOKE"];
const hasForbidden = forbiddenKeywords.some(keyword => normalizedSql.includes(keyword));
if (hasForbidden) {
return {
isError: true,
content: [
{
type: "text",
text: "Akses ditolak: Query mengandung kata kunci modifikasi data terlarang.",
},
],
};
}
try {
const result = await dbPool.query(sql);
return {
content: [
{
type: "text",
text: JSON.stringify(result.rows, null, 2),
},
],
};
} catch (error: any) {
return {
isError: true,
content: [
{
type: "text",
text: `Gagal mengeksekusi query: ${error.message}`,
},
],
};
}
}
return {
isError: true,
content: [
{
type: "text",
text: `Tool '${name}' tidak ditemukan.`,
},
],
};
});
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("MCP Postgres Server berjalan via stdio.");
}
main().catch((error) => {
console.error("Fatal error pada process utama:", error);
process.exit(1);
});5. Integrasi MCP Server dengan Claude Desktop Client
Daftarkan MCP Server yang telah selesai dibangun ke dalam file konfigurasi Client MCP (misalnya Claude Desktop App).
Lokasi file konfigurasi sistem:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
Tambahkan entri registrasi server pada objek mcpServers:
{
"mcpServers": {
"postgres-local": {
"command": "npx",
"args": [
"tsx",
"/path/absolut/ke/mcp-postgres-server/src/index.ts"
],
"env": {
"DATABASE_URL": "postgres://ai_readonly:PasswordAmanAI2025!@localhost:5432/db_bisnis"
}
}
}
}Ganti /path/absolut/ke/mcp-postgres-server/src/index.ts sesuai lokasi absolut direktori di sistem target. Simpan file lalu restart aplikasi Claude Desktop.
6. Verification dan Debugging Menggunakan MCP Inspector
Pengujian fungsi MCP Server dapat dilakukan independen tanpa Claude Desktop menggunakan alat resmi MCP Inspector.
Jalankan perintah berikut pada terminal proyek:
npx @modelcontextprotocol/inspector npx tsx src/index.tsPerintah tersebut akan me-launch antarmuka web debugging lokal. Buka tautan yang muncul pada browser (biasanya http://localhost:5173) lalu jalankan langkah verifikasi berikut:
- Klik List Tools. Pastikan tool
get_database_schemadanexecute_read_queryterdeteksi secara otomatis. - Pilih
get_database_schema, klik Run Tool. Verifikasi bahwa struktur skema PostgreSQL dikembalikan dalam format JSON valid. - Pilih
execute_read_query, masukkan input JSON{"sql": "SELECT * FROM information_schema.tables LIMIT 5;"}, lalu jalankan. Verifikasi hasil eksekusi data baris. - Uji skenario kegagalan: Masukkan query mutasi data seperti
DELETE FROM users;. Pastikan server merespons dengan pesan penolakan akses.
7. Praktik Terbaik Pengamanan dan Optimasi Kinerja
Implementasi MCP Server pada lingkungan produksi memerlukan beberapa pengamanan tambahan:
- Pembatasan Baris Data Auto-Limit: Tambahkan pembatas kata kunci
LIMITsecara otomatis di tingkat server jika query dari AI Agent tidak menyertakan batasan jumlah baris data. Hal ini mencegah konsumsi RAM berlebih (Out-Of-Memory) pada Node.js. - Query Statement Timeout: Tetapkan nilai
statement_timeoutpada koneksi pg pool (misal 5000 ms) agar query yang menggantung tidak membebani Resource PostgreSQL. - Parsing SQL via AST: Jangan mengandalkan pengecekan berbasis RegEx sederhana. Gunakan parser SQL berbasis Abstract Syntax Tree (seperti
pgsql-parser) untuk memverifikasi AST query secara akurat sebelum dieksekusi. - Penggunaan Database Views: Buat Database View khusus yang mengeksklusi kolom data sensitif (misalnya hash password, kredensial, atau data PII) lalu batasi akses role
ai_readonlyhanya pada View tersebut.


