<!-- title: 造只读SQL探针MCP：Claude秒查数据库 -->

## 概述

本教程带你从零用官方 TypeScript SDK 构建一个**只读 SQL 探针 MCP 服务器**，让 Claude Code 能直接对你本地的 SQLite 数据库做"提问即查表"——查表结构、跑只读 SELECT、拿业务洞察。学完你将掌握 MCP 的三大原语（Tools / Resources / 传输层）在真实工程里的落地方式、只读安全的工程化护栏，以及如何用 `.mcp.json` 把自建服务器接入 Claude Code 并用 Inspector 调试。适合已写过一个 Skill 或跑通过 `claude` 命令、想往"给 AI 接外部数据源"方向进阶的开发者。

## 前置条件

- 已安装 Node.js 18+ 与 `npm`（或 `pnpm`/`yarn`）
- 已装好 Claude Code（`claude --version` 能输出版本号）
- 会基本的 TypeScript / 命令行操作
- 全程不需要装数据库服务——SQLite 是单文件、零配置的

> 为什么选 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` 跑出结果。

```mermaid
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
```

## 第二步：初始化项目并安装依赖

```bash
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"`，并加两条脚本：

```json
{
  "type": "module",
  "scripts": {
    "dev": "tsx src/index.ts",
    "build": "tsc",
    "start": "node dist/index.js"
  }
}
```

`tsconfig.json` 关键项：

```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`：

```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`。

## 第四步：写服务器骨架（stdio 传输）

新建 `src/index.ts`。先搭最小骨架，确认传输通路：

```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`。这是新手最常踩的坑。

## 第五步：用 Resources 暴露表结构

让模型"看懂"数据库，是它能写出正确 SQL 的前提。把 schema 暴露成一个可读资源：

```ts
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 后再决定怎么查。

## 第六步：用 Tools 实现只读查询（核心）

这是整套服务器的灵魂。`query` 工具接收一段 SQL，但**必须只读、必须限流、必须防注入**。用 `zod` 定义入参 schema，SDK 会自动做参数校验并把 schema 上报给客户端：

```ts
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}` }],
      ];
    }
  }
);
```

**三道护栏的设计哲学**：

1. **连接层**：`readonly: true` 让 SQLite 本身拒绝写操作。
2. **会话层**：`PRAGMA query_only = ON` 双保险，连临时表都建不了。
3. **应用层**：`assertReadOnly` 白名单 + 单语句约束，拦截多语句注入与危险关键字。

`isError: true` 很重要——它告诉模型"这次调用失败了"，模型会读错误信息并自己改写 SQL 重试，而不是把错误当成功结果继续编故事。

## 第七步：编译并接入 Claude Code

```bash
npm run build   # 产出 dist/index.js
```

在项目根目录新建 `.mcp.json`（项目级，可提交 git 与团队共享）：

```json
{
  "mcpServers": {
    "sql-probe": {
      "command": "node",
      "args": ["${workspaceFolder}/dist/index.js"]
    }
  }
}
```

> `${workspaceFolder}` 会被 Claude Code 展开为当前项目根目录。若你用旧版不支持该变量，写绝对路径即可。

也可以用 CLI 注册（用户级，跨项目可用）：

```bash
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。

## 第八步：用 MCP Inspector 调试

服务器不工作？别盲猜，用官方 Inspector 逐条测工具：

```bash
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` |

---

## 实战案例：让 Claude 当你的临时数据分析师

把上面的库换成你真实业务的 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 从"查询通道"升级为"性能顾问"的关键一步。

```mermaid
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元
```

---

## 迁移到 PostgreSQL / MySQL

只需替换驱动与三道防线：

- 驱动换 `pg` / `mysql2`，用连接串而非文件路径
- 连接层防线：用一个**只有 SELECT 权限的只读账号**（`GRANT SELECT ON ... TO reader`）
- 会话层：Postgres 设 `default_transaction_read_only = on`
- 应用层 `assertReadOnly` 原样复用

护栏思路通用，差异只在数据库侧的权限模型。

## 训练营适用性评估

- **适合类型**：后端开发训练营、AI 应用工程训练营、数据分析入门营
- **教学时长**：约 3~4 课时（1 课时讲 MCP 协议与三原语，2 课时动手写 + 调试，1 课时做迁移与拓展）
- **难度**：★★★☆☆（中等，门槛在 TS 异步与护栏设计，而非协议本身）
- **可拓展作业**：① 加 `explain` 工具；② 改造为远程 HTTP 传输（流式 SSE）；③ 给资源加上 `prompts` 模板，固化"经营日报"提问方式

## 参考来源

1. [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk) — 官方 TS SDK 仓库，含 `McpServer` 高层 API 与 stdio 传输示例
2. [Connect Claude Code to tools via MCP](https://code.claude.com/docs/en/mcp) — Claude Code 官方文档，讲 `.mcp.json` 三级作用域与 `claude mcp add` 命令
3. [Tool Registration and Execution](https://deepwiki.com/modelcontextprotocol/typescript-sdk/3.2-tool-registration-and-execution) — `server.tool()` 注册、zod 入参校验、`isError` 回传机制详解
4. [MSSQL Read-Only MCP Server](https://github.com/trainerroad/mcp-sqlserver) — 工业级只读 SQL MCP 参考实现，护栏设计借鉴来源
5. [Build a Custom MCP Server for Claude Code](https://apidog.com/blog/how-to-quickly-build-a-mcp-server-for-claude-code/) — 从零构建到接入 Claude Code 的完整图文指南

> [!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 等场景全覆盖。

![51学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
