1. 为什么我要给 Claude Code 配一个只读的 GaussDB 通道先说结论我写了一个用 Go 实现的 MCP 服务把 GaussDB 的查询能力以只读方式暴露给 Claude Code。它解决的核心问题是——我想让 AI 帮我查数据、写 SQL、分析表结构但绝对不能让它在生产库上执行任何写操作。这个服务适合所有正在用 Claude Code 做数据相关开发、又对生产库安全有硬性要求的后端和 DBA 同学参考。事情的起因很朴素。我日常要维护一套跑在 GaussDB 上的业务系统经常需要确认某张表的结构、某个字段的分布、某条 SQL 的执行计划。以前的做法是打开客户端、连上库、手敲 SQL来回切换窗口效率很低。后来用上 Claude Code发现它写 SQL 的能力相当靠谱我就想能不能让它直接连到库里查但紧接着一个念头把我按住了——生产库万一它哪次理解偏了给我来个 UPDATE 或者 DELETE那可不是闹着玩的。MCP 协议正好是干这个的。MCP 全称 Model Context Protocol本质上是给 AI 客户端比如 Claude Code和外部工具之间定的一套通信规范。你可以把它理解成AI 世界的 USB 接口只要你的服务按这个协议说话Claude Code 就能把它当成一个可调用的工具。我需要的不是让 AI 拥有数据库的全部权限而是给它开一扇只能看、不能改的窗。这就是只读 MCP 服务的由来。为什么用 Go 写几个现实原因。第一Go 编译出来是单个静态二进制扔到服务器上就能跑不用装运行时部署成本极低。第二Go 的并发模型处理数据库连接池很顺手多个查询请求进来不会互相阻塞。第三GaussDB 官方提供了 Go 的驱动兼容性有保障。第四我自己 Go 写得比 Python 熟出问题好排查。这几点加起来Go 就是最省心的选择。这篇文章我会把整个服务的思路、关键代码、踩过的坑、以及怎么和 Claude Code 对接全部摊开讲。不管你是刚接触 MCP 的新手还是已经在用 Claude Code 的老手都能照着复现一套属于自己的只读数据通道。2. 整体设计只读这件事必须从架构层面锁死2.1 只读不能靠约定要靠机制很多人第一反应是我在提示词里告诉 AI只准查不准改不就行了我劝你千万别这么想。提示词是软的AI 的理解是概率性的你没法保证它 100% 遵守。真正靠谱的只读必须在服务端用机制强制实现让写操作在物理上就没有执行的可能。我的设计里只读是通过三层来保证的。第一层是数据库账号权限给这个服务单独建一个数据库用户只授予 SELECT 权限从根上堵死写操作。第二层是 SQL 语句校验服务在把 SQL 发给数据库之前先做一次解析只放行 SELECT 和 EXPLAIN 这类只读语句其他一律拒绝。第三层是连接参数在连接串里显式设置只读模式让数据库驱动层面也帮忙兜底。三层叠加任何一层被绕过还有另外两层挡着。提示数据库账号权限是最硬的一道墙。哪怕你的代码有 bug只要账号没有写权限数据库自己就会拒绝。所以这一步绝对不能省也不能图省事直接用业务账号。2.2 为什么选 MCP 而不是直接写个 HTTP 接口有人会问我直接写个 REST API 给 Claude Code 调不行吗行但麻烦。Claude Code 原生支持 MCP你按 MCP 协议实现它就能自动发现你的工具、自动读取工具描述、自动把参数传进来。你不用去教 AI这个接口怎么调、参数叫什么MCP 的 schema 会把这些信息结构化地告诉它。这就像你给 AI 递了一本说明书而不是让它猜。MCP 服务支持两种传输方式stdio 和 SSE。stdio 是通过标准输入输出通信适合本地进程SSE 是通过 HTTP 长连接适合远程服务。我选的是 stdio因为我的场景是本地开发机连生产库通过跳板或专线服务就跑在本地Claude Code 直接拉起进程简单直接不用管端口和网络。2.3 工具粒度怎么切MCP 服务对外暴露的是一个个工具tool。工具切得太粗AI 不好用切得太细AI 要调很多次。我最后定了四个工具list_tables列出当前库或指定 schema 下的所有表describe_table查看某张表的字段、类型、注释、索引run_query执行一条只读 SQL 并返回结果explain_query对一条 SQL 生成执行计划这四个覆盖了我 90% 的日常需求。list_tables和describe_table让 AI 先了解数据结构run_query用来取数explain_query用来分析性能。工具描述我写得比较详细把每个参数的用途、格式、限制都写清楚了这样 AI 调用时不容易传错参数。3. 核心实现从连接池到 SQL 校验的完整拆解3.1 环境准备与依赖选型先说环境。我用的 Go 版本是 1.21GaussDB 驱动用的是官方提供的gaussdb驱动基于 PostgreSQL 协议因为 GaussDB 兼容 PG 协议。MCP 的 Go SDK 我用的是社区维护的mcp-go库它把协议细节封装好了我只需要关注工具的实现。依赖清单如下go mod init gaussdb-mcp go get github.com/mark3labs/mcp-go go get github.com/HuaweiCloudDeveloper/gaussdb-go这里有个坑要提前说GaussDB 的驱动和 PostgreSQL 的lib/pq在连接串格式上略有差异尤其是 SSL 相关的参数。我一开始直接套用 PG 的连接串结果一直连不上后来查了官方文档才发现 GaussDB 对sslmode的取值有额外要求。这个后面在排查章节会细讲。3.2 数据库连接池的配置连接池这块我用的是database/sql标准库配合驱动。关键参数有三个最大连接数、最大空闲连接数、连接最大存活时间。生产库的连接资源是有限的不能让你一个 MCP 服务把连接占满。db, err : sql.Open(gaussdb, connStr) if err ! nil { log.Fatalf(open db failed: %v, err) } db.SetMaxOpenConns(5) db.SetMaxIdleConns(2) db.SetConnMaxLifetime(30 * time.Minute)为什么最大连接数设 5因为 MCP 服务的查询请求是串行偏多的AI 一次通常只发一两个查询5 个连接足够应对突发。设太大反而会挤占业务连接。最大空闲连接设 2保证有请求时不用现建连接又不会长期占着资源。存活时间设 30 分钟是为了避免连接被数据库端或中间网络设备悄悄断开后客户端还在用一个死连接。注意连接池参数没有万能值要根据你的库的max_connections和业务并发来调。我的经验是MCP 服务占用的连接数不要超过总连接数的 5%。3.3 只读账号的创建与权限收敛这一步在数据库侧做。我建了一个叫mcp_reader的用户只给 SELECT 权限CREATE USER mcp_reader WITH PASSWORD your_strong_password; GRANT CONNECT ON DATABASE your_db TO mcp_reader; GRANT USAGE ON SCHEMA public TO mcp_reader; GRANT SELECT ON ALL TABLES IN SCHEMA public TO mcp_reader; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO mcp_reader;最后那句ALTER DEFAULT PRIVILEGES很关键。它保证以后新建的表mcp_reader也自动有 SELECT 权限不用每次建表后手动授权。我一开始漏了这句结果新加的表 AI 查不到排查了半天才发现是权限问题。密码这块我强烈建议不要硬编码在代码里用环境变量传。我在服务启动时从GAUSSDB_MCP_PASSWORD环境变量读取代码里只留一个占位。3.4 SQL 只读校验的实现这是整个服务最核心的一段逻辑。我的做法是拿到 SQL 后先做词法层面的检查提取出语句的第一个关键字只允许SELECT、EXPLAIN、SHOW、WITH这几种。为什么把WITH也放进来因为 CTE公共表表达式在很多分析场景里很有用而WITH ... SELECT本身是只读的。但要注意WITH后面如果跟的是INSERT或UPDATE那就危险了所以还得进一步检查。func isReadOnly(sqlStr string) bool { normalized : strings.TrimSpace(strings.ToUpper(sqlStr)) // 去掉开头的注释 normalized stripLeadingComments(normalized) allowedPrefixes : []string{SELECT, EXPLAIN, SHOW, WITH} for _, p : range allowedPrefixes { if strings.HasPrefix(normalized, p) { // 对 WITH 做额外检查防止 CTE 里藏写操作 if p WITH { return !containsWriteKeyword(normalized) } return true } } return false }containsWriteKeyword会扫描整条语句如果发现INSERT、UPDATE、DELETE、DROP、TRUNCATE、ALTER、CREATE这些词就直接拒绝。这里有个细节不能简单地用字符串包含来判断因为字段名或字符串常量里可能恰好含有这些词。我的做法是配合简单的词法分析把这些关键字从字符串常量和注释里排除掉再判断。提示SQL 校验永远做不到 100% 完美因为 SQL 语法太灵活了。所以它只是第二道防线真正的底线是数据库账号权限。两者配合安全性才有保障。3.5 MCP 工具的注册与描述MCP 服务的核心是注册工具。每个工具需要提供名称、描述、参数 schema 和处理函数。描述写得越清楚AI 用得越准。我以run_query为例tool : mcp.NewTool(run_query, mcp.WithDescription(执行一条只读 SQL 查询并返回结果。仅支持 SELECT/EXPLAIN/SHOW/WITH 语句。返回结果最多 1000 行。), mcp.WithString(sql, mcp.Required(), mcp.Description(要执行的只读 SQL 语句)), mcp.WithNumber(limit, mcp.Description(返回行数上限默认 100最大 1000)), )注意我在描述里明确写了仅支持只读语句和最多 1000 行。这两个约束很重要前者让 AI 知道边界后者防止一条SELECT *把几十万行数据灌进上下文既慢又浪费 token。处理函数里我会强制加上LIMIT即使 AI 没传 limit 参数。4. 实操过程从零跑通一个查询4.1 编译与本地测试代码写完后先编译go build -o gaussdb-mcp .得到一个二进制文件。测试时我建议先不接 Claude Code直接用命令行手动喂 JSON-RPC 消息确认服务能正常响应。MCP 的 stdio 模式就是一行一个 JSON 消息你可以用 echo 管道测试echo {jsonrpc:2.0,id:1,method:tools/list} | ./gaussdb-mcp如果返回了工具列表说明服务基本正常。这一步能帮你把协议层的问题和数据库层的问题分开排查起来快很多。4.2 配置 Claude Code 接入Claude Code 的 MCP 配置在一个 JSON 文件里。我把它配成这样{ mcpServers: { gaussdb-readonly: { command: /path/to/gaussdb-mcp, env: { GAUSSDB_MCP_HOST: your-db-host, GAUSSDB_MCP_PORT: 8000, GAUSSDB_MCP_USER: mcp_reader, GAUSSDB_MCP_PASSWORD: your_password, GAUSSDB_MCP_DBNAME: your_db } } } }配好后重启 Claude Code它启动时会自动拉起这个进程。你可以在对话里问它现在有哪些数据库工具可用它会列出我注册的四个工具。然后就可以让它查表了比如帮我看看 orders 表的结构它会自动调describe_table。4.3 一次完整的查询实录我实际用的时候会这样跟 Claude Code 对话我帮我查一下最近 7 天每天的订单量按日期排序。Claude Code 会先调describe_table看 orders 表有哪些字段确认时间字段叫created_at然后生成 SQL 调run_querySELECT DATE(created_at) AS day, COUNT(*) AS cnt FROM orders WHERE created_at NOW() - INTERVAL 7 days GROUP BY DATE(created_at) ORDER BY day;服务收到后先过只读校验通过再补上LIMIT 1000发给 GaussDB拿到结果返回。整个过程我只需要说一句话剩下的它自己搞定。这就是 MCP 的价值——把查数据这件事的交互成本降到了最低。4.4 结果集大小的控制这里单独说一下结果集控制因为它直接影响使用体验。我踩过的坑是有一次让 AI 查一张日志表它生成了SELECT * FROM logs结果表里有上百万行。虽然我加了LIMIT 1000但返回的 1000 行数据里有很多长文本字段塞进上下文后直接把对话撑爆了响应变得极慢。后来我做了两个改进。第一在run_query的返回里如果某一行某个字段超过 500 字符就截断并标注...[truncated]。第二在工具描述里明确告诉 AI查询大表时请先用 COUNT 确认行数再取样本数据。这两招之后再没出现过上下文被撑爆的情况。5. 常见问题与排查技巧实录5.1 连接不上数据库的几种典型情况连接问题是最常见的。我整理了一个排查表现象可能原因排查方法报 SSL 相关错误sslmode 参数不匹配尝试 disable/require/verify-full 逐个测试连接超时网络不通或端口错telnet 测试端口连通性认证失败用户名密码错或权限不足用同账号在客户端手动连一次连接被拒数据库 max_connections 满查 pg_stat_activity 看连接数我遇到最多的是 SSL 问题。GaussDB 对sslmode的处理和标准 PG 有差异有些版本只认disable和require你写prefer它会报错。解决办法就是明确指定别用默认值。5.2 SQL 校验误杀怎么办只读校验有时候会误杀合法查询。比如字段名里带update这个词或者字符串常量里有delete。我的处理方式是先看被拒绝的 SQL 是不是真的只读如果是就调整校验逻辑把字符串常量和注释排除掉再判断。但我的原则是宁可误杀不可放过——如果一条 SQL 的只读性有疑问我宁愿让 AI 换个写法也不冒险放行。提示如果你发现某类合法查询频繁被误杀可以在校验函数里加白名单但一定要谨慎白名单越少越好。5.3 AI 生成的 SQL 性能问题AI 写的 SQL 语法通常没问题但性能不一定好。我遇到过它生成的查询没走索引全表扫描把库拖慢的情况。解决办法是在工具描述里加一句生成查询前请先查看表的索引信息并且提供explain_query工具让 AI 可以自己检查执行计划。另外我在数据库侧给mcp_reader账号设了statement_timeout超过 30 秒的查询自动终止防止慢查询拖垮库。ALTER USER mcp_reader SET statement_timeout 30s;这一句非常有用相当于给 AI 的查询加了个保险丝。5.4 中文乱码与字符集问题GaussDB 的字符集如果和客户端不一致返回的中文可能乱码。我在连接串里显式指定了client_encodingUTF8并且在 Go 侧确认返回的字符串按 UTF-8 处理。如果还是乱码检查数据库的server_encoding是不是 UTF8。这个坑不常遇到但一旦遇到很影响体验。5.5 服务进程被意外拉起多次Claude Code 在某些情况下可能会重复拉起 MCP 进程导致多个实例同时连库。我的做法是在服务启动时检查一个锁文件如果已有实例在跑就直接退出。这样能避免连接数被无谓占用。6. 几个让服务更好用的进阶技巧6.1 给工具加上下文提示MCP 工具的描述里可以塞一些使用建议。比如我在run_query的描述里加了查询前建议先用 describe_table 了解表结构。这看起来是小事但能显著减少 AI 的无效调用。AI 看到这句话会先了解结构再查生成的 SQL 准确率高很多。6.2 返回结果的结构化run_query返回的不只是数据我还把列名、列类型、行数一起返回。这样 AI 拿到结果后能更准确地理解数据含义。比如它看到某列类型是timestamp就知道可以做时间运算。这个细节让后续对话的连贯性好了不少。6.3 日志与审计虽然是只读服务但审计还是要有。我把每次查询的 SQL、执行时间、返回行数都记到日志里。一方面方便排查问题另一方面也能看到 AI 到底在查什么心里有数。日志我按天切分保留 7 天避免占满磁盘。6.4 敏感字段的脱敏生产库里难免有手机号、身份证这类敏感字段。我在服务里加了一个可配置的脱敏规则如果查询结果里出现配置的敏感列名就自动打码。比如手机号只显示前三位和后四位。这个功能不是必须的但如果你的库里有敏感数据强烈建议加上。7. 我踩过的坑和最后的几句实在话回过头看这个项目最大的价值不是代码本身而是它让我重新思考了AI 和数据的边界。以前我总觉得要么全给权限、要么完全不给现在发现中间地带是存在的——通过 MCP 这样的协议通过只读账号和 SQL 校验这样的机制我可以精确地控制 AI 能做什么、不能做什么。踩过的坑里最值得说的是权限那一次。我一开始图省事直接用了一个有写权限的账号想着反正有 SQL 校验兜着。结果有一次校验逻辑有个边界没覆盖到AI 生成的一条语句差点就执行了写操作幸好数据库账号权限拦住了。从那以后我就明白了安全设计里永远不要依赖单一防线每一层都要能独立兜底。还有一个体会是关于工具描述的。我一开始描述写得很简略结果 AI 经常传错参数或者用错工具。后来我把每个参数的格式、取值范围、默认值都写清楚调用准确率立刻上来了。这让我意识到MCP 服务的用户体验很大程度上取决于你给 AI 的说明书写得好不好。最后分享一个小技巧如果你也想做类似的服务建议先从最小的工具集开始比如只做一个run_query跑通了再逐步加工具。我一开始想一口气做七八个工具结果每个都半成品反而不好用。聚焦核心需求把一两个工具做扎实比铺一堆半吊子工具强得多。这个服务现在每天帮我省下大量切窗口、敲 SQL 的时间而且用得安心——因为我知道它只能看不能改。
阅读完成 · 觉得有帮助?