首页 › tutorials › 造只读SQL探针MCP:Claude秒查数据库

概述

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

前置条件

为什么选 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。

第四步:写服务器骨架(stdio 传输)

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

第五步:用 Resources 暴露表结构

让模型"看懂"数据库,是它能写出正确 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 后再决定怎么查。

第六步:用 Tools 实现只读查询(核心)

这是整套服务器的灵魂。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}` }],
      ];
    }
  }
);

三道护栏的设计哲学:

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

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

第七步:编译并接入 Claude Code

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。

第八步:用 MCP Inspector 调试

服务器不工作?别盲猜,用官方 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

实战案例:让 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 从"查询通道"升级为"性能顾问"的关键一步。

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

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

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

训练营适用性评估

参考来源

  1. MCP TypeScript SDK — 官方 TS SDK 仓库,含 McpServer 高层 API 与 stdio 传输示例
  2. Connect Claude Code to tools via MCP — Claude Code 官方文档,讲 .mcp.json 三级作用域与 claude mcp add 命令
  3. Tool Registration and Execution — server.tool() 注册、zod 入参校验、isError 回传机制详解
  4. MSSQL Read-Only MCP Server — 工业级只读 SQL MCP 参考实现,护栏设计借鉴来源
  5. Build a Custom 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学通信知识星球海报-扫码加入