上周有个朋友问我你用Claude Code写项目每次查数据库都要手动粘SQL结果给它吗不用。我写了一个MCP ServerClaude Code能直接连我的SQLite数据库查表结构、跑查询、甚至帮我写迁移脚本。整个过程不到一小时。MCPModel 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 20node -v检查一下 - 一个SQLite数据库文件没有的话我们自己建一个 - Claude Code或者任何支持MCP的客户端先建项目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-sqlite3tsconfig.json{ compilerOptions: { target: ES2022, module: Node16, moduleResolution: Node16, outDir: ./dist, rootDir: ./src, strict: true, esModuleInterop: true }, include: [src/**/*] }package.json里加一行{ type: module, scripts: { build: tsc, start: node dist/index.js } }写Server主体创建src/index.tsimport { 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。每个工具有名字、描述、参数定义和执行逻辑。我们注册三个最常用的// 工具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工具server.tool( execute, 执行写入SQLINSERT/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在文件末尾加上启动逻辑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解析错误查了半天才发现日志混进了协议流里。编译运行看看npm run build node dist/index.js ./test.db # 用任意一个SQLite文件测试如果看到stderr输出sqlite-mcp 已启动Server就跑起来了。按CtrlC退出。接入Claude Code在项目根目录创建.mcp.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_transaction、commit、rollback也做成工具。做数据迁移的时候能回滚比什么都重要。多数据库切换。如果你有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 → 注册工具 → 定义参数 → 处理请求 → 返回结果。
从零写一个MCP Server:让Claude Code直接操作你的数据库
发布时间:2026/6/1 9:54:09
上周有个朋友问我你用Claude Code写项目每次查数据库都要手动粘SQL结果给它吗不用。我写了一个MCP ServerClaude Code能直接连我的SQLite数据库查表结构、跑查询、甚至帮我写迁移脚本。整个过程不到一小时。MCPModel 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 20node -v检查一下 - 一个SQLite数据库文件没有的话我们自己建一个 - Claude Code或者任何支持MCP的客户端先建项目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-sqlite3tsconfig.json{ compilerOptions: { target: ES2022, module: Node16, moduleResolution: Node16, outDir: ./dist, rootDir: ./src, strict: true, esModuleInterop: true }, include: [src/**/*] }package.json里加一行{ type: module, scripts: { build: tsc, start: node dist/index.js } }写Server主体创建src/index.tsimport { 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。每个工具有名字、描述、参数定义和执行逻辑。我们注册三个最常用的// 工具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工具server.tool( execute, 执行写入SQLINSERT/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在文件末尾加上启动逻辑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解析错误查了半天才发现日志混进了协议流里。编译运行看看npm run build node dist/index.js ./test.db # 用任意一个SQLite文件测试如果看到stderr输出sqlite-mcp 已启动Server就跑起来了。按CtrlC退出。接入Claude Code在项目根目录创建.mcp.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_transaction、commit、rollback也做成工具。做数据迁移的时候能回滚比什么都重要。多数据库切换。如果你有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 → 注册工具 → 定义参数 → 处理请求 → 返回结果。