MCP协议实战:用TypeScript给Cursor写一个自定义MCP Server,10分钟接入你的数据库

2 阅读6分钟

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-sqlite3mysql2pg,逻辑基本一致,改连接方式即可 - 增加execute_query工具让Cursor直接跑查询(注意安全,建议只读账号) - 增加list_databases工具支持多数据库切换

不适合的场景: - 生产数据库直连(风险太高,建议只连本地开发库或只读副本) - 需要流式输出的大数据量场景(MCP目前对流式支持还在完善中)

下一步演进方向: - 结合RAG,让AI不只知道表结构,还能知道每张表里的典型数据样本 - 把这个Server发布到MCP Marketplace,让团队成员都能用

09 实战小结

要点说明
调试输出只用console.errorconsole.log会破坏协议
参数校验接收外部输入必须校验,防SQL注入
路径配置Windows下JSON里的路径用\\双重转义
构建部署tsc编译再用node运行,不要直接用tsx跑生产
安全数据库连接默认只读,别给写权限

10 结尾

MCP这个方向,我最近在用得越来越多——不只是接数据库,还接了本地文件系统和一个内部API文档服务。

说实话,配好一个自定义MCP Server之后,Cursor写业务代码的体验真的上了一个台阶。

你现在在用Cursor或者其他支持MCP的工具吗?有没有什么你觉得最值得做成MCP Server的场景?

评论区说说👇