Build a Custom MCP Server for Cursor Database Access in 30 Minutes

Create a Model Context Protocol server that lets Cursor AI safely query your local PostgreSQL or MySQL database with natural language.

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:

  1. Open Cursor composer (Cmd/Ctrl + I)
  2. Type: "What tables are in my database?"
  3. Cursor should use the get_schema tool 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