Implementing a Custom MCP Server to Connect Cursor to Local SQLite Databases
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.
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!
