从零写一个MCP Server:让Claude Code直接操作你的数据库

上周有个朋友问我:"你用Claude Code写项目,每次查数据库都要手动粘SQL结果给它吗?"

不用。我写了一个MCP Server,Claude Code能直接连我的SQLite数据库,查表结构、跑查询、甚至帮我写迁移脚本。整个过程不到一小时。

MCP(Model Context Protocol)你可能听过很多次了,但大多数教程停留在"用别人写好的MCP Server"。今天这篇不一样------我们从零写一个,你跟着做完就能理解MCP到底怎么跑的,以后自己改、扩展都不成问题。

先搞清楚MCP在干什么

MCP的核心很简单:它是AI模型和外部工具之间的一层协议。Claude Code本身不能连数据库、不能调API、不能读你电脑上的文件------但通过MCP Server,它可以。

数据流是这样的:

复制代码
Claude Code  ──(MCP协议)──>  MCP Server  ──(SQL)──>  SQLite数据库
                                  |
                           返回查询结果

MCP Server就是一个中间人。Claude Code通过标准化的JSON-RPC消息告诉Server"我要调哪个工具、传什么参数",Server执行完把结果返回去。

准备工作

你需要: - Node.js 20+(node -v 检查一下) - 一个SQLite数据库文件(没有的话我们自己建一个) - Claude Code(或者任何支持MCP的客户端)

先建项目:

bash 复制代码
mkdir my-sqlite-mcp && cd my-sqlite-mcp
npm init -y
npm install @modelcontextprotocol/sdk better-sqlite3
npm install -D typescript @types/node @types/better-sqlite3

tsconfig.json:

json 复制代码
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true
  },
  "include": ["src/**/*"]
}

package.json里加一行:

json 复制代码
{
  "type": "module",
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js"
  }
}

写Server主体

创建 src/index.ts

typescript 复制代码
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";
import path from "path";

// 从命令行参数拿数据库路径
const dbPath = process.argv[2];
if (!dbPath) {
  console.error("用法: node dist/index.js <数据库路径>");
  process.exit(1);
}

const db = new Database(path.resolve(dbPath), { readonly: false });
db.pragma("journal_mode = WAL");  // 并发性能好一些

const server = new McpServer({
  name: "sqlite-mcp",
  version: "1.0.0",
});

这段代码做了三件事:导入依赖、打开数据库连接、初始化MCP Server实例。better-sqlite3是同步的SQLite驱动,MCP Server本身就是单线程处理请求,用同步驱动反而更省事。

注册工具

MCP Server的核心是"工具"(Tools)。每个工具有名字、描述、参数定义和执行逻辑。我们注册三个最常用的:

typescript 复制代码
// 工具1:列出所有表
server.tool(
  "list_tables",
  "列出数据库中的所有表和视图",
  {},
  async () => {
    const tables = db.prepare(
      "SELECT name, type FROM sqlite_master WHERE type IN ('table','view') ORDER BY name"
    ).all();
    return {
      content: [{
        type: "text",
        text: JSON.stringify(tables, null, 2)
      }]
    };
  }
);

// 工具2:查看表结构
server.tool(
  "describe_table",
  "查看指定表的字段信息",
  { table_name: z.string().describe("表名") },
  async ({ table_name }) => {
    // 防SQL注入:表名只允许字母、数字、下划线
    if (!/^[\w]+$/.test(table_name)) {
      return {
        content: [{ type: "text", text: "错误:表名包含非法字符" }],
        isError: true
      };
    }
    const columns = db.prepare(
      `PRAGMA table_info("${table_name}")`
    ).all();
    if (columns.length === 0) {
      return {
        content: [{ type: "text", text: `表 ${table_name} 不存在` }],
        isError: true
      };
    }
    return {
      content: [{
        type: "text",
        text: JSON.stringify(columns, null, 2)
      }]
    };
  }
);

// 工具3:执行SQL查询
server.tool(
  "query",
  "执行SQL查询(SELECT语句)",
  {
    sql: z.string().describe("要执行的SQL语句"),
    limit: z.number().optional().default(100).describe("最大返回行数")
  },
  async ({ sql, limit }) => {
    const trimmed = sql.trim().toUpperCase();
    // 只允许SELECT和WITH开头的查询
    if (!trimmed.startsWith("SELECT") && !trimmed.startsWith("WITH")) {
      return {
        content: [{ type: "text", text: "只允许SELECT查询。写入操作请用execute工具。" }],
        isError: true
      };
    }
    try {
      const stmt = db.prepare(sql);
      const rows = stmt.all().slice(0, limit);
      return {
        content: [{
          type: "text",
          text: `查询返回 ${rows.length} 行:\n${JSON.stringify(rows, null, 2)}`
        }]
      };
    } catch (e: any) {
      return {
        content: [{ type: "text", text: `SQL错误: ${e.message}` }],
        isError: true
      };
    }
  }
);

注意几个细节:

参数校验用zod。 MCP SDK内置了zod支持,参数定义和校验一步到位。Claude Code会根据这些定义自动生成工具调用参数。

表名防注入。 describe_table里用正则过滤了表名。虽然这是本地工具,但养成习惯没坏处。

查询结果限制行数。 默认最多返回100行。数据库可能有几十万行数据,全返回给Claude Code没有意义,还会撑爆上下文窗口。

加一个写入工具(可选)

如果你信任Claude Code在你的数据库上做写入操作,可以加一个execute工具:

typescript 复制代码
server.tool(
  "execute",
  "执行写入SQL(INSERT/UPDATE/DELETE/CREATE等)",
  { sql: z.string().describe("要执行的SQL语句") },
  async ({ sql }) => {
    try {
      const result = db.prepare(sql).run();
      return {
        content: [{
          type: "text",
          text: `执行完成。影响行数: ${result.changes}`
        }]
      };
    } catch (e: any) {
      return {
        content: [{ type: "text", text: `SQL错误: ${e.message}` }],
        isError: true
      };
    }
  }
);

我自己的做法是:开发库开写入,生产库只开查询。这个通过启动参数控制就行,不用改代码。

启动Server

在文件末尾加上启动逻辑:

typescript 复制代码
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("sqlite-mcp 已启动,数据库:", dbPath);
}

main().catch((e) => {
  console.error("启动失败:", e);
  process.exit(1);
});

注意console.error不是console.log。MCP通过stdin/stdout通信,stdout被协议消息占了,日志必须走stderr。这个坑我踩过一次------用了console.log之后Claude Code就一直报JSON解析错误,查了半天才发现日志混进了协议流里。

编译运行看看:

bash 复制代码
npm run build
node dist/index.js ./test.db  # 用任意一个SQLite文件测试

如果看到stderr输出"sqlite-mcp 已启动",Server就跑起来了。按Ctrl+C退出。

接入Claude Code

在项目根目录创建.mcp.json

json 复制代码
{
  "mcpServers": {
    "sqlite": {
      "command": "node",
      "args": [
        "/你的路径/my-sqlite-mcp/dist/index.js",
        "/你的路径/database.db"
      ]
    }
  }
}

重启Claude Code,输入/mcp检查一下,应该能看到sqlite server和它注册的工具。

接下来你可以直接跟Claude Code说"帮我查一下users表有多少条数据",它会自动调用query工具执行SELECT COUNT(*) FROM users

实际用起来什么感觉

我用这个MCP Server一个多月了,说几个真实的使用场景:

场景1:排查数据问题。 之前有个bug,用户反馈"订单状态不对"。我跟Claude Code说了一下症状,它自己查了orders表、order_items表、payment_records表,十几条SQL下来直接定位到是一条支付回调写了两次。整个过程我没写一行SQL。

场景2:写数据迁移脚本。 要给一个老表加字段、迁移数据。Claude Code先用describe_table看了现有结构,然后生成了迁移SQL,用execute跑了一遍,最后再query验证数据对不对。中间有一条UPDATE语句WHERE条件漏了,它看到影响行数不对主动改了。

场景3:生成报表。 产品经理要一个"最近7天各渠道注册用户数"的数据。我把需求原文发给Claude Code,它自己写SQL、查数据、整理成表格返回。3分钟搞定。

两个踩坑记录

坑1:数据库文件路径用绝对路径。 .mcp.json里的数据库路径如果用相对路径,Claude Code可能在不同的工作目录下启动Server,导致找不到文件。用绝对路径稳。

坑2:大结果集会拖慢响应。 有一次我忘了加limit,查了一个50万行的表。MCP Server倒是正常返回了,但Claude Code收到巨长的JSON后思考了很久。后来我把默认limit设成100,需要更多数据的时候再手动指定。

扩展方向

这个MCP Server现在就四个工具,够用但不够好用。你可以继续加:

表数据统计。 注册一个table_stats工具,返回每个表的行数、大小、最近更新时间。Claude Code在分析问题时经常需要这些上下文。

事务支持。begin_transactioncommitrollback也做成工具。做数据迁移的时候,能回滚比什么都重要。

多数据库切换。 如果你有dev、staging、prod多个库,可以注册一个switch_db工具动态切换连接。

MCP协议本身还在演进。Anthropic 5月中旬更新了MCP规范,加了Streamable HTTP传输和OAuth认证。如果你要做远程MCP Server(比如团队共用一个数据库查询服务),HTTP传输比stdio方便得多。

代码仓库

完整代码大概150行TypeScript。你可以在上面的代码基础上直接用,也可以去npm搜@anthropic/mcp-server-sqlite,那是Anthropic官方的参考实现,功能更完整,但代码也更复杂。

自己写一遍的好处是你真正理解了MCP的工作原理。以后不管是接数据库、接Redis、接内部API,都是同一套模式:初始化Server → 注册工具 → 定义参数 → 处理请求 → 返回结果。

相关推荐
梦想的颜色1 小时前
TypeScript 完全指南(中):函数、接口、类与高级类型
前端·typescript
码哥字节4 小时前
升到 Spring Boot 4.1,虚拟线程开了,HikariCP 连接池却崩了
java·springboot·claude code
ggabb4 小时前
中英诗歌对比:各有千秋,中文诗词独具极致美学与思想高度
sqlite
cooldream20095 小时前
使用 uv 管理 Python 虚拟环境:现代 Python 开发的高效实践
python·uv·mcp
心之伊始5 小时前
Spring Boot 接入 MCP 实战:用 Spring AI 调用本地工具的最小闭环
java·spring boot·agent·spring ai·mcp
abcy0712137 小时前
【无标题】
数据库·sqlite
wgc2k7 小时前
Nest.js基础-5:关于Docker的简单概述
docker·typescript·node.js
Dxy12393102168 小时前
Django 模型查询中的数据库连接池配置指南
数据库·django·sqlite