从零写一个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 → 注册工具 → 定义参数 → 处理请求 → 返回结果。

相关推荐
doiito8 分钟前
【Agent Harness】为什么我把 JSON‑LD “编译成 DAG” 后,整个 Agent 平台立刻聪明了
ai·rust·架构设计·系统设计·ai agent
ServBay2 小时前
Laravel Herd MCP 的替代,多语言与跨平台的 AI 本地开发选择
后端·ai编程·mcp
码哥字节3 小时前
我把整个代码库喂给 Claude Code,工具超 50 个就静默丢失,这个坑太阴了
mcp·claude code·ai编程工具
Flynt4 小时前
装上TypeScript 7.0 RC之后,最让我意外不是10倍提速
typescript·visual studio code
疯狂SQL4 小时前
手写高性能在线 JSON 工具|Web Worker 工程化打包 + 语法自动修复 + 多语言代码生成实战
typescript·json·next.js·web worker·前端性能优化·esbuild·源码实战
xiezhr5 小时前
折腾半小时,终于让AI 能直接帮我写飞书文档了
ai·飞书·ai agent·飞书cli·飞书文档
ServBay4 天前
打通 AI 编程本地运维边界,利用 MCP 协议简化环境与服务管理
后端·ai编程·mcp
Momo__4 天前
TypeScript NoInfer<T>——精准控制泛型推断的工具类型
前端·typescript
Super Scraper5 天前
如何批量抓取 TikTok 数据而不被封锁?完整指南
爬虫·ai·自动化·抖音·tiktok·ai agent