本教程带你从零用官方 TypeScript SDK 构建一个只读 SQL 探针 MCP 服务器,让 Claude Code 能直接对你本地的 SQLite 数据库做"提问即查表"——查表结构、跑只读 SELECT、拿业务洞察。学完你将掌握 MCP 的三大原语(Tools / Resources / 传输层)在真实工程里的落地方式、只读安全的工程化护栏,以及如何用 .mcp.json 把自建服务器接入 Claude Code 并用 Inspector 调试。适合已写过一个 Skill 或跑通过 claude 命令、想往"给 AI 接外部数据源"方向进阶的开发者。
npm(或 pnpm/yarn)claude --version 能输出版本号)为什么选 SQLite?因为它就是一个
.sqlite文件,无需起服务、无需账号密码,最适合教学闭环。本教程的护栏代码同样适用于 PostgreSQL/MySQL,文末会给出迁移要点。
MCP(Model Context Protocol)是一套开放协议,定义了"AI 客户端"与"外部能力"之间的标准通信方式。Claude Code 就是客户端,你写的服务器就是"能力提供方"。一个 MCP 服务器对外暴露三类东西:
| 原语 | 作用 | 本项目对应 |
|---|---|---|
| Tools(工具) | 模型可以调用的函数,有入参 schema | query 执行只读 SQL |
| Resources(资源) | 模型可以读取的数据,有 URI | schema://tables 暴露表结构 |
| Prompts(提示) | 预制模板(本教程略) | — |
传输方式:本教程用 stdio——Claude Code 把你的服务器当子进程拉起,通过标准输入输出收发 JSON-RPC 消息。这是本地开发最简单、最常用的方式。
我们的目标:Claude Code 里直接问"上月销量前三的商品是谁、各自卖了多少",AI 会自动调 schema 看懂表,再调 query 跑出结果。
flowchart LR
U[用户提问<br/>上月销冠是谁] --> CC[Claude Code<br/>MCP 客户端]
CC -->|tools/call query| S[你的 MCP 服务器<br/>sql-probe]
CC -->|resources/read schema://tables| S
S -->|PRAGMA query_only| DB[(SQLite<br/>shop.sqlite)]
S -->|JSON 结果| CC
CC --> U
mkdir sql-probe-mcp && cd sql-probe-mcp
npm init -y
# 核心:官方 MCP TS SDK + better-sqlite3(同步、快、零配置)
npm install @modelcontextprotocol/sdk better-sqlite3 zod
npm install -D typescript @types/node @types/better-sqlite3 tsx
# 初始化 TS 配置
npx tsc --init
把 package.json 的 type 设为 "module",并加两条脚本:
{
"type": "module",
"scripts": {
"dev": "tsx src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
}
}
tsconfig.json 关键项:
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "Bundler",
"outDir": "dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src"]
}
预期结果:node_modules 里出现 @modelcontextprotocol/sdk;npm run dev 暂时还跑空,下一步才填逻辑。
常见问题:better-sqlite3 安装时报 node-gyp 编译错误——多数是 Node 版本太老或缺编译工具链,Linux 装 build-essential python3、macOS 装 Xcode Command Line Tools 即可。若仍失败,可改用纯 JS 的 sql.js,但读写性能差。
先造一份有真实结构的电商数据,方便后面验证。新建 seed.ts:
import Database from "better-sqlite3";
const db = new Database("shop.sqlite");
db.exec(`
CREATE TABLE IF NOT EXISTS products (
id INTEGER PRIMARY KEY, name TEXT, price REAL, stock INTEGER
);
CREATE TABLE IF NOT EXISTS orders (
id INTEGER PRIMARY KEY, product_id INTEGER, qty INTEGER,
amount REAL, created_at TEXT
);
`);
db.prepare(
"INSERT OR IGNORE INTO products VALUES (1,'蓝牙耳机',299,50),(2,'机械键盘',459,30),(3,'4K显示器',1899,12);"
).run();
db.prepare(`
INSERT OR IGNORE INTO orders VALUES
(1,1,2,598,'2026-06-03'),
(2,3,1,1899,'2026-06-12'),
(3,1,5,1495,'2026-06-20'),
(4,2,3,1377,'2026-06-25');
`).run();
db.close();
console.log("seed done");
跑一次 npx tsx seed.ts,目录下生成 shop.sqlite。
新建 src/index.ts。先搭最小骨架,确认传输通路:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import Database from "better-sqlite3";
// 1) 以只读方式打开数据库(第一道防线)
const db = new Database("shop.sqlite", { readonly: true, fileMustExist: true });
// 2) 创建服务器实例
const server = new McpServer({
name: "sql-probe",
version: "1.0.0",
});
// 3) 接 stdio 传输并启动
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("[sql-probe] running on stdio"); // 注意用 stderr
关键点:console.log 会污染 stdout——而 stdio 传输用 stdout 收发 JSON-RPC!所以调试信息一律走 console.error。这是新手最常踩的坑。
让模型"看懂"数据库,是它能写出正确 SQL 的前提。把 schema 暴露成一个可读资源:
server.resource(
"schema",
"schema://tables",
{ description: "所有表与列的结构定义", mimeType: "application/json" },
async () => {
const tables = db
.prepare("SELECT name FROM sqlite_master WHERE type='table'")
.all() as { name: string }[];
const result = tables.map((t) => {
const columns = db.prepare(`PRAGMA table_info(${t.name})`).all();
return { table: t.name, columns };
});
return {
contents: [
{
uri: "schema://tables",
mimeType: "application/json",
text: JSON.stringify(result),
},
],
};
}
);
模型一旦需要了解库结构,Claude Code 就会 resources/read schema://tables,拿到 JSON 后再决定怎么查。
这是整套服务器的灵魂。query 工具接收一段 SQL,但必须只读、必须限流、必须防注入。用 zod 定义入参 schema,SDK 会自动做参数校验并把 schema 上报给客户端:
import { z } from "zod";
// 第三道防线:SQL 白名单校验
function assertReadOnly(sql: string) {
const normalized = sql.trim().toLowerCase();
const forbidden =
/\b(insert|update|delete|drop|alter|create|truncate|attach|detach|pragma\s+(?!query_only)|replace|vacuum)\b/;
if (forbidden.test(normalized)) {
throw new Error("仅允许只读语句(SELECT / WITH / EXPLAIN)");
}
if (!/^(select|with|explain)\b/.test(normalized)) {
throw new Error("语句必须以 SELECT / WITH / EXPLAIN 开头");
}
if (normalized.includes(";") && normalized.replace(/;$/, "").includes(";")) {
throw new Error("禁止多语句,仅允许单条");
}
}
server.tool(
"query",
"对 shop.sqlite 执行只读 SQL 查询,返回 JSON 行",
{
sql: z.string().min(1).describe("一条只读 SQL,例如 SELECT * FROM products LIMIT 5"),
limit: z.number().int().min(1).max(500).default(100).describe("最大返回行数"),
},
async ({ sql, limit }) => {
try {
assertReadOnly(sql);
// 第二道防线:强制只读 + 超时
db.pragma("query_only = ON");
db.pragma("busy_timeout = 3000");
const rows = db.prepare(sql).all().slice(0, limit);
const truncated = rows.length === limit ? "(已达上限,可能被截断)" : "";
return {
content: [
{
type: "text",
text: JSON.stringify({ rowCount: rows.length, rows, truncated }, null, 2),
},
],
};
} catch (err) {
// 把错误以 isError 形式回传,模型能看到并自我纠正
return {
isError: true,
content: [{ type: "text", text: `查询失败:${(err as Error).message}` }],
];
}
}
);
三道护栏的设计哲学:
readonly: true 让 SQLite 本身拒绝写操作。PRAGMA query_only = ON 双保险,连临时表都建不了。assertReadOnly 白名单 + 单语句约束,拦截多语句注入与危险关键字。isError: true 很重要——它告诉模型"这次调用失败了",模型会读错误信息并自己改写 SQL 重试,而不是把错误当成功结果继续编故事。
npm run build # 产出 dist/index.js
在项目根目录新建 .mcp.json(项目级,可提交 git 与团队共享):
{
"mcpServers": {
"sql-probe": {
"command": "node",
"args": ["${workspaceFolder}/dist/index.js"]
}
}
}
${workspaceFolder}会被 Claude Code 展开为当前项目根目录。若你用旧版不支持该变量,写绝对路径即可。
也可以用 CLI 注册(用户级,跨项目可用):
claude mcp add sql-probe --scope user -- node /绝对路径/dist/index.js
claude mcp list # 验证已注册
claude mcp get sql-probe # 查看详情
作用域三级:
| 作用域 | 位置 | 适用 |
|---|---|---|
local |
~/.claude.json 内当前项目段 |
仅自己、仅本项目(默认) |
project |
项目根 .mcp.json |
团队共享,提交 git |
user |
~/.claude.json 全局 |
跨所有项目个人使用 |
启动 Claude Code,第一次会提示授权连接 sql-probe,确认即可。在会话里输入:
> 上个月(2026-06)销量最高的商品是谁?卖了多少钱?
预期 Claude Code 的行为链:先 resources/read schema://tables 读懂 orders/products 结构 → 自动写 JOIN 聚合 SQL → 调 query 工具 → 回答"蓝牙耳机,售出 7 件,金额 2093 元"。整个过程你无需手写一条 SQL。
服务器不工作?别盲猜,用官方 Inspector 逐条测工具:
npx @modelcontextprotocol/inspector node dist/index.js
它会起一个本地 Web 界面,你能手动 List Tools、填参数、Call Tool、List Resources,看完整 JSON-RPC 往返。调试 MCP 服务器的第一铁律:先在 Inspector 跑通,再接 Claude Code。
常见问题速查:
| 现象 | 原因 | 解法 |
|---|---|---|
| Claude Code 提示 server failed to start | 路径错 / 编译失败 | node dist/index.js 手动跑,看报错 |
| 工具能列出来但调用超时 | SQL 卡死或锁库 | 加 busy_timeout,检查是否误连了写库 |
| 模型不主动用资源 | 工具描述没引导 | 在 query 描述里写"如不确定表结构请先读 schema://tables" |
| stdout 出现乱码导致协议崩 | 误用 console.log |
全部改 console.error |
把上面的库换成你真实业务的 SQLite(或导出一份只读副本),加一个"自然语言经营日报"的玩法。在 Claude Code 里:
> 帮我生成 2026 年 6 月经营简报:总营收、销量 TOP3 商品、客单价。
数据用 sql-probe 查,结果整理成表格。
Claude 会连续发起 3~4 次 query,自己拼出 SUM(amount)、GROUP BY product_id ORDER BY ... LIMIT 3、SUM(amount)/COUNT(*) 等查询,最后产出一张 Markdown 表。你等于零代码拥有了一个"懂你库的临时分析师"。
进阶:再加一个 explain 工具返回查询计划(EXPLAIN QUERY PLAN),让 Claude 在慢查询时自我优化——这是把 MCP 从"查询通道"升级为"性能顾问"的关键一步。
sequenceDiagram
participant U as 用户
participant C as Claude Code
participant S as sql-probe MCP
participant D as SQLite
U->>C: 上月销冠是谁?
C->>S: resources/read schema://tables
S->>D: PRAGMA table_info
D-->>S: 列定义
S-->>C: 表结构 JSON
C->>S: tools/call query {sql: "...JOIN..."}
S->>S: assertReadOnly ✅
S->>D: SELECT (query_only=ON)
D-->>S: 4 行
S-->>C: rowCount+rows
C-->>U: 蓝牙耳机,7件,2093元
只需替换驱动与三道防线:
pg / mysql2,用连接串而非文件路径GRANT SELECT ON ... TO reader)default_transaction_read_only = onassertReadOnly 原样复用护栏思路通用,差异只在数据库侧的权限模型。
explain 工具;② 改造为远程 HTTP 传输(流式 SSE);③ 给资源加上 prompts 模板,固化"经营日报"提问方式McpServer 高层 API 与 stdio 传输示例.mcp.json 三级作用域与 claude mcp add 命令server.tool() 注册、zod 入参校验、isError 回传机制详解[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。
