01 场景钩子:Cursor不知道你数据库里有什么
这个坑我踩了一段时间。
用Cursor写SQL相关的业务代码,每次都得把表结构手动粘到对话里,让它基于这个生成查询。
表少还好,一旦项目大了,十几张表的字段关系,粘一遍就要两分钟,而且每次新开对话还得重来一遍。
有没有办法让Cursor直接读取我的数据库结构,不需要我每次手动告诉它?
答案就是MCP协议。
本文要做的事:用TypeScript写一个自定义MCP Server,让Cursor能直接查询你本地数据库的表结构,辅助生成SQL和业务代码。可运行,已验证。
02 问题拆解:MCP到底是个什么东西?
先把概念说清楚,不啰嗦。
MCP(Model Context Protocol,模型上下文协议),是Anthropic在2024年底提出的一个标准化协议,用来解决一个核心问题:
> AI模型怎么和外部数据源、工具、API打交道?
以前的方式是各家自己搞——OpenAI有function calling,各种AI编辑器有各自的插件机制。
MCP的目标是标准化这个过程:只要你按MCP协议写一个Server,任何支持MCP的AI客户端(Cursor、Claude Desktop等)都能用。
对前端来说,MCP Server本质上就是一个Node.js进程,通过标准输入输出(stdio)和AI客户端通信。
整个链路长这样:
Cursor(AI客户端)
↕ stdio / SSE
MCP Server(你写的Node.js进程)
↕ 调用
外部数据源(数据库/API/文件系统)
本文要解决的子问题有三个: 1. 怎么初始化一个MCP Server项目(TypeScript) 2. 怎么定义一个"查询数据库表结构"的工具 3. 怎么在Cursor里配置并使用它
03 技术选型:用官方SDK,别造轮子
方案对比就两句话:
- 手写stdio通信:可以,但要处理JSON-RPC格式解析、错误处理、能力协商,麻烦
- 用@modelcontextprotocol/sdk:官方提供的TypeScript SDK,封装好了所有底层通信
选SDK,理由充分,无需纠结。
数据库驱动用better-sqlite3(本文以SQLite为例,MySQL/PostgreSQL稍后说明替换方式)。
环境要求: - Node.js ≥ 18.x - TypeScript ≥ 5.0 - Cursor ≥ 0.40(支持MCP配置)
04 核心实现第一步:初始化项目
mkdir my-db-mcp-server && cd my-db-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk better-sqlite3
npm install -D typescript @types/node @types/better-sqlite3 tsx
创建tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"strict": true,
"esModuleInterop": true
},
"include": ["src/**/*"]
}
创建入口文件src/index.ts,先写最基础的骨架:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import Database from "better-sqlite3";
import { z } from "zod";
// 数据库路径——替换成你自己的
const DB_PATH = process.env.DB_PATH || "./dev.db";
const db = new Database(DB_PATH, { readonly: true });
const server = new McpServer({
name: "db-schema-server",
version: "1.0.0",
});
// 工具定义(下一节)
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("MCP Server running on stdio");
}
main().catch(console.error);
关键点:这里用console.error而不是console.log——因为MCP通过stdout通信,用log会污染协议数据流,必须用stderr输出日志。
05 核心实现第二步:定义两个工具
工具一:list_tables,列出数据库所有表名。
工具二:describe_table,获取某张表的字段结构。
// 工具1:列出所有表
server.tool(
"list_tables",
"列出数据库中所有的表名",
{}, // 无需参数
async () => {
const tables = db
.prepare(
"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
)
.all() as { name: string }[];
return {
content: [
{
type: "text",
text: tables.map((t) => t.name).join("\n"),
},
],
};
}
);
// 工具2:描述表结构
server.tool(
"describe_table",
"获取指定表的字段名、类型、是否为空、默认值等信息",
{
table_name: z.string().describe("要查询的表名"),
},
async ({ table_name }) => {
// 防SQL注入:只允许合法表名字符
if (!/^[a-zA-Z0-9_]+$/.test(table_name)) {
throw new Error("非法表名");
}
const columns = db.prepare(`PRAGMA table_info(${table_name})`).all();
if (!columns.length) {
return {
content: [{ type: "text", text: `表 "${table_name}" 不存在` }],
};
}
const formatted = (columns as any[])
.map(
(col) =>
`${col.name} | ${col.type} | nullable: ${!col.notnull} | default: ${col.dflt_value ?? "NULL"}`
)
.join("\n");
return {
content: [
{
type: "text",
text: `表 ${table_name} 结构:\n${formatted}`,
},
],
};
}
);
06 踩坑与解决
坑1:console.log把MCP协议搞坏了
上线第一天,Cursor一直提示"MCP Server连接失败"。排查了半天,发现是在工具里加了console.log调试输出,污染了stdout的JSON-RPC流。
解决:所有调试信息改console.error,永远不在MCP Server里用console.log。
坑2:sqlite_master在只读模式下有权限问题
用readonly: true打开数据库时,部分版本的better-sqlite3在查询sqlite_master时会抛出权限异常。
解决:加上{ readonly: true, fileMustExist: true }两个参数一起传,问题消失。
const db = new Database(DB_PATH, { readonly: true, fileMustExist: true });
坑3:Windows路径下\字符在JSON配置里转义
在Cursor的MCP配置里写Windows路径时,反斜杠需要双重转义:C:\\Users\\...,不然Cursor解析配置会报错。
07 完整Demo与Cursor配置
运行脚本(package.json):
{
"scripts": {
"dev": "tsx src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
}
}
Cursor MCP配置(~/.cursor/mcp.json 或 .cursor/mcp.json):
{
"mcpServers": {
"db-schema": {
"command": "node",
"args": ["/绝对路径/my-db-mcp-server/dist/index.js"],
"env": {
"DB_PATH": "/绝对路径/your-database.db"
}
}
}
}
先npm run build编译,然后在Cursor设置里刷新MCP连接,绿灯亮起说明连接成功。
验证效果:在Cursor对话框输入"帮我查一下数据库里有哪些表",Cursor会自动调用list_tables工具,然后列出你的表名。再问"users表的字段结构是什么",会调用describe_table,直接返回字段信息。
不需要你再手动粘表结构了。
08 拓展与限制
这个方案的边界要说清楚:
可以做的:
- 替换better-sqlite3为mysql2或pg,逻辑基本一致,改连接方式即可
- 增加execute_query工具让Cursor直接跑查询(注意安全,建议只读账号)
- 增加list_databases工具支持多数据库切换
不适合的场景: - 生产数据库直连(风险太高,建议只连本地开发库或只读副本) - 需要流式输出的大数据量场景(MCP目前对流式支持还在完善中)
下一步演进方向: - 结合RAG,让AI不只知道表结构,还能知道每张表里的典型数据样本 - 把这个Server发布到MCP Marketplace,让团队成员都能用
09 实战小结
| 要点 | 说明 |
|---|---|
| 调试输出 | 只用console.error,console.log会破坏协议 |
| 参数校验 | 接收外部输入必须校验,防SQL注入 |
| 路径配置 | Windows下JSON里的路径用\\双重转义 |
| 构建部署 | 先tsc编译再用node运行,不要直接用tsx跑生产 |
| 安全 | 数据库连接默认只读,别给写权限 |
10 结尾
MCP这个方向,我最近在用得越来越多——不只是接数据库,还接了本地文件系统和一个内部API文档服务。
说实话,配好一个自定义MCP Server之后,Cursor写业务代码的体验真的上了一个台阶。
你现在在用Cursor或者其他支持MCP的工具吗?有没有什么你觉得最值得做成MCP Server的场景?
评论区说说👇