Implementing a Custom MCP Server to Connect Cursor to Local SQLite Databases

JohnInShanghai Intermediate 4/30/2026 78 views 13 likes 3 min read

Cursor's native indexing is great for codebase navigation, but it's blind to the actual data sitting in your local SQLite databases. I spent the weekend building a custom Model Context Protocol (MCP) server to bridge this gap, and it completely changes how I debug data-driven features. Instead of manually running queries in a DB browser and pasting the results into the chat, I can now ask Cursor, "Why is the user with ID 402 getting a validation error?" and it queries the DB directly to find the offending row.

The magic here is that MCP allows the LLM to call "tools" (functions) on your local machine. For a SQLite integration, you only need a few endpoints: one to list tables and one to execute read-only queries.

Here is the minimal TypeScript implementation using the @modelcontextprotocol/sdk. I kept it read-only to avoid the AI accidentally dropping a table during a hallucination.

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 Database from "better-sqlite3";

const db = new Database("local_data.db");

const server = new Server({
  name: "sqlite-explorer",
  version: "1.0.0",
}, {
  capabilities: { tools: {} },
});

server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [
    {
      name: "query_db",
      description: "Execute a read-only SQL query on the local SQLite database",
      inputSchema: {
        type: "object",
        properties: {
          sql: { type: "string", description: "The SQL query to run" },
        },
        required: ["sql"],
      },
    },
  ],
}));

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  if (request.params.name === "query_db") {
    const sql = request.params.arguments?.sql as string;
    try {
      // Force read-only by checking for keywords or using a read-only connection
      if (!sql.toLowerCase().startsWith("select")) {
        throw new Error("Only SELECT queries are allowed for safety");
      }
      const results = db.prepare(sql).all();
      return { content: [{ type: "text", text: JSON.stringify(results) }] };
    } catch (e: any) {
      return { content: [{ type: "text", text: `Error: ${e.message}` }], isError: true };
    }
  }
  throw new Error("Tool not found");
});

const transport = new StdioServerTransport();
await server.connect(transport);

To get this running in Cursor, you have to build the project and point Cursor to the executable. In Settings > Features > MCP, add a new server:

Name: SQLite-Local
Type: command
Command: node /absolute/path/to/your/build/index.js

One major gotcha I encountered: Cursor's context window can get flooded if a query returns 1,000 rows. I had to implement a hard LIMIT 100 in my prompt instructions (via .cursorrules) to stop the LLM from eating all my tokens on a single SELECT * call.

Productivity gains I've noticed:

  • Schema Discovery: I no longer need to keep a schema.sql file open. I just ask "What are the columns in the orders table?" and it figures it out.
  • Instant Bug Triangulation: When a frontend bug appears, I can ask "Check if the API response matches the DB record for this UUID," and it does the cross-reference in seconds.
  • Seed Data Generation: I can ask it to "Analyze the existing entries in the categories table and write a migration script to add missing tags," and it uses real data to write the code.
Implementing a Custom MCP Server to Connect Cursor to Local SQLite Databases

If you're doing this, avoid using sqlite3 (the async lib) and go with better-sqlite3. The synchronous nature of the latter makes the MCP server logic much cleaner since the SDK handles the transport layer asynchronously anyway.

All Replies (0)

No replies yet — be the first!

Write a Reply

Markdown supported