Problem: Cursor Can't Access Your Database
You want Cursor AI to help query your local database, but it has no way to see your schema or run queries. Copy-pasting table structures is tedious and error-prone.
You'll learn:
- Build a custom MCP server that exposes database tools
- Configure Cursor to use your MCP server
- Implement safe read-only database access with query validation
Time: 30 min | Level: Intermediate
Why This Matters
The Model Context Protocol (MCP) lets AI assistants like Cursor access external tools safely. By creating a database MCP server, you get:
Benefits:
- Natural language queries without leaving your editor
- Automatic schema awareness for better suggestions
- Read-only safety by default
- Works with PostgreSQL, MySQL, SQLite
Common use cases:
- Debugging production data issues
- Writing complex JOINs with AI assistance
- Exploring unfamiliar database schemas
Prerequisites
# Verify you have Node.js 20+ and npm
node --version # Should show v20.0.0 or higher
npm --version
# Install database (if you don't have one)
# PostgreSQL example:
brew install postgresql@16 # macOS
# OR
sudo apt install postgresql-16 # Ubuntu
You'll need:
- Node.js 20+ (MCP SDK requirement)
- A local database (PostgreSQL, MySQL, or SQLite)
- Cursor editor installed
- Database connection credentials
Solution
Step 1: Initialize MCP Server Project
# Create project directory
mkdir cursor-db-mcp
cd cursor-db-mcp
# Initialize with TypeScript
npm init -y
npm install @modelcontextprotocol/sdk pg dotenv
npm install -D @types/node @types/pg typescript tsx
# Create tsconfig
npx tsc --init --target ES2022 --module Node16 --moduleResolution Node16
Expected: A new TypeScript project with MCP SDK installed.
Step 2: Create Database Schema Tool
Create src/index.ts:
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { Pool } from "pg";
import * as dotenv from "dotenv";
dotenv.config();
// Read-only connection pool
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
// Force read-only transactions
max: 3,
idleTimeoutMillis: 30000,
});
const server = new Server(
{
name: "cursor-db-mcp",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
// Tool 1: Get database schema
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "get_schema",
description: "Get database schema including tables, columns, and relationships",
inputSchema: {
type: "object",
properties: {
table: {
type: "string",
description: "Optional: specific table name to get schema for",
},
},
},
},
{
name: "query_database",
description: "Execute read-only SQL query (SELECT only)",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "SQL SELECT query to execute",
},
limit: {
type: "number",
description: "Maximum rows to return (default: 100, max: 1000)",
default: 100,
},
},
required: ["query"],
},
},
],
}));
// Tool implementation
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
if (name === "get_schema") {
const tableName = args?.table as string | undefined;
// Query information_schema for table structure
const query = tableName
? `
SELECT
table_name,
column_name,
data_type,
is_nullable,
column_default
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = $1
ORDER BY ordinal_position;
`
: `
SELECT
table_name,
column_name,
data_type,
is_nullable
FROM information_schema.columns
WHERE table_schema = 'public'
ORDER BY table_name, ordinal_position;
`;
const result = await pool.query(
query,
tableName ? [tableName] : []
);
return {
content: [
{
type: "text",
text: JSON.stringify(result.rows, null, 2),
},
],
};
}
if (name === "query_database") {
const query = args?.query as string;
const limit = Math.min((args?.limit as number) || 100, 1000);
// Security: Only allow SELECT statements
if (!query.trim().toUpperCase().startsWith("SELECT")) {
throw new Error("Only SELECT queries are allowed");
}
// Execute with row limit
const result = await pool.query(`${query} LIMIT ${limit}`);
return {
content: [
{
type: "text",
text: JSON.stringify({
rows: result.rows,
rowCount: result.rowCount,
fields: result.fields.map(f => f.name),
}, null, 2),
},
],
};
}
throw new Error(`Unknown tool: ${name}`);
} catch (error) {
return {
content: [
{
type: "text",
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
},
],
isError: true,
};
}
});
// Start server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Cursor DB MCP Server running on stdio");
}
main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});
Why this works: The MCP SDK handles communication with Cursor, while pg handles safe database access. Read-only validation prevents accidental data modification.
Step 3: Configure Environment
Create .env:
# PostgreSQL example
DATABASE_URL=postgresql://username:password@localhost:5432/your_database
# MySQL example (requires mysql2 package instead of pg)
# DATABASE_URL=mysql://username:password@localhost:3306/your_database
# SQLite example (requires better-sqlite3)
# DATABASE_URL=sqlite:./dev.db
Security note: Never commit .env to version control. Add it to .gitignore.
If using MySQL: Replace pg with mysql2 and update the Pool import:
npm uninstall pg @types/pg
npm install mysql2
Step 4: Make Server Executable
Update package.json:
{
"name": "cursor-db-mcp",
"version": "1.0.0",
"type": "module",
"bin": {
"cursor-db-mcp": "./dist/index.js"
},
"scripts": {
"build": "tsc",
"dev": "tsx src/index.ts",
"prepare": "npm run build"
}
}
Build and test:
# Compile TypeScript
npm run build
# Test server starts
npm run dev
# Should output: "Cursor DB MCP Server running on stdio"
# Press Ctrl+C to stop
Expected: Successful compilation with no errors.
Step 5: Configure Cursor to Use MCP Server
Open Cursor settings (Cmd/Ctrl + Shift + P → "Cursor: Open Settings (JSON)"):
Add to your settings.json:
{
"mcp.servers": {
"database": {
"command": "node",
"args": ["/absolute/path/to/cursor-db-mcp/dist/index.js"],
"env": {
"DATABASE_URL": "postgresql://username:password@localhost:5432/dbname"
}
}
}
}
Critical: Use absolute path, not relative. Get it with:
pwd # Copy this output and append /dist/index.js
Alternative: Install globally to avoid path issues:
npm link
# Then use:
"command": "cursor-db-mcp"
Step 6: Restart Cursor
# Completely quit Cursor (not just close window)
# Then reopen your project
Verify it's working:
- Open Cursor composer (Cmd/Ctrl + I)
- Type: "What tables are in my database?"
- Cursor should use the
get_schematool automatically
Verification
Test the schema tool:
You: "Show me the schema for the users table"
Cursor should respond with column names, types, and constraints.
Test querying:
You: "How many active users are there?"
Cursor should translate this to SQL and execute via your MCP server.
Expected output: Cursor will show it's using your "database" MCP server and display query results.
If it fails:
- "MCP server not found": Check absolute path in settings.json
- "Connection refused": Verify DATABASE_URL is correct
- "Only SELECT queries allowed": Working as intended - server is read-only
Advanced: Add Query Validation
For production use, add query sanitization:
// Add to src/index.ts before executing query
import { parse } from "pgsql-ast-parser";
function validateQuery(sql: string): void {
try {
const ast = parse(sql);
// Block dangerous operations
const dangerous = ["DROP", "DELETE", "UPDATE", "INSERT", "TRUNCATE"];
const sqlUpper = sql.toUpperCase();
for (const keyword of dangerous) {
if (sqlUpper.includes(keyword)) {
throw new Error(`${keyword} operations are not allowed`);
}
}
} catch (error) {
throw new Error(`Invalid SQL: ${error.message}`);
}
}
// Use before pool.query
validateQuery(query);
Install parser:
npm install pgsql-ast-parser
What You Learned
- MCP servers expose tools to AI assistants via stdio protocol
- Read-only database access requires query validation at multiple levels
- Cursor automatically discovers and uses MCP tools based on context
- Environment variables keep credentials out of code
Limitations:
- Only works with local databases (no remote RDS without VPN)
- Read-only by design - use Cursor's Terminal for schema changes
- Requires Cursor restart when updating MCP server code
Security considerations:
- Never expose write operations without authentication
- Limit query result sizes to prevent memory issues
- Consider adding rate limiting for production use
Troubleshooting
Cursor doesn't see the MCP server
Check:
# Verify the server runs standalone
cd cursor-db-mcp
npm run dev
# You should see "Cursor DB MCP Server running on stdio"
Fix: Ensure settings.json path is absolute and points to compiled JS (not TypeScript):
"args": ["/Users/you/cursor-db-mcp/dist/index.js"] // ✅ Correct
"args": ["./dist/index.js"] // ⌠Relative path won't work
"args": ["/Users/you/cursor-db-mcp/src/index.ts"] // ⌠Points to TS not JS
Database connection errors
Symptoms: "Connection refused" or "authentication failed"
Fix: Test connection separately:
# PostgreSQL
psql $DATABASE_URL -c "SELECT 1"
# MySQL
mysql -u username -p your_database -e "SELECT 1"
Ensure your DATABASE_URL format matches:
- PostgreSQL:
postgresql://user:pass@localhost:5432/dbname - MySQL:
mysql://user:pass@localhost:3306/dbname - SQLite:
sqlite:./path/to/db.sqlite
TypeScript compilation errors
Error: "Cannot find module '@modelcontextprotocol/sdk'"
Fix: Ensure moduleResolution is correct in tsconfig.json:
{
"compilerOptions": {
"module": "Node16",
"moduleResolution": "Node16",
"target": "ES2022"
}
}
Tested on Cursor 0.43+, Node.js 20.11, PostgreSQL 16, macOS Sonoma & Ubuntu 24.04