Claude Code 安全接入 GaussDB:只读 MCP 服务设计与实现
发布时间:2026/9/28 17:56:51来源:尧图网络
1. 为什么我要给 Claude Code 配一个只读的 GaussDB 通道让 AI 编程助手直接连生产数据库这件事我一开始是拒绝的。原因很简单Claude Code 这类工具在排查问题时最喜欢干的事情就是“顺手查一下”——查表结构、查索引、查几条样本数据甚至有时候会尝试执行 EXPLAIN 看执行计划。这些操作本身没问题问题在于它连的是生产库。我踩过的坑很具体有一次让助手帮忙分析一个慢查询它很“贴心”地补了一句SELECT * FROM orders LIMIT 10结果那张表有 2 亿行虽然加了 LIMIT但生产库当时的连接池已经吃紧这一下直接把一个只读副本的 CPU 拉到了 90% 以上。从那以后我就定了个规矩AI 可以看但绝对不能碰写权限而且连接必须走我控制的通道。这就是我做这个 GaussDB 只读 MCP 服务的起因。MCP 是 Model Context Protocol简单说就是一套让 AI 助手调用外部工具的协议标准。Claude Code 支持通过 MCP 接入自定义工具我只要写一个 MCP Server把“查表结构”“执行只读 SQL”“看执行计划”这几个能力暴露出去同时在服务端做死权限校验就能让助手在安全边界内干活。选 Go 来写理由也很直接单二进制、无运行时依赖、并发模型适合做这种轻量代理服务部署到内网跳板机上一条命令就能跑起来。GaussDB 这边它兼容 PostgreSQL 协议所以底层的驱动和连接池方案可以大量复用 PG 生态的成熟做法但要注意 GaussDB 在权限模型和部分系统表上有自己的实现不能完全照搬 PG 的经验。这篇文章我会把整个服务的设计思路、权限收口方式、MCP 协议对接细节、GaussDB 连接配置、以及实际跑起来之后遇到的各种坑全部摊开讲。如果你也在用 Claude Code、Cursor 这类支持 MCP 的助手并且手上有 GaussDB 或者类似的国产数据库这套方案可以直接抄作业。2. 整体架构设计与关键技术选型2.1 为什么是 MCP 而不是直接给个数据库账号最省事的做法当然是建一个只读账号把连接串丢给 Claude Code让它自己连。但我没这么干原因有三个。第一只读账号不等于安全。GaussDB 里只读账号依然可以查pg_stat_activity、可以看其他会话的 SQL、可以查系统表里的一些敏感信息。如果助手被诱导去查这些信息就泄露了。第二连接串一旦写进配置文件就等于把凭据散落在开发机上谁都能翻出来。第三我需要审计——助手到底查了什么、查了多少次、有没有异常的大查询这些必须留痕。MCP 这一层正好卡在中间助手只能调用我定义好的工具工具内部再去连数据库。凭据只存在于 MCP 服务的环境变量里助手看不到每次调用我都能记日志SQL 进来先过一遍白名单校验不合规的直接拒绝。提示MCP 的核心价值不是“让 AI 能连数据库”而是“让 AI 只能通过你允许的方式连数据库”。这个定位想清楚了后面的设计就顺了。2.2 Go 语言在这个场景下的具体优势用 Go 写 MCP Server我主要看中这几点。编译产物是单个静态二进制扔到内网机器上不需要装任何东西scp过去chmod x就能跑。标准库的database/sql配合pgx驱动连接池管理非常成熟设置最大连接数、空闲超时、健康检查都是一行配置的事。goroutine 处理并发请求多个工具调用同时进来不会互相阻塞这对助手那种“一次问好几个问题”的使用模式很友好。还有一个容易被忽略的点Go 的context包天然适合做超时控制。我在每个工具调用上都挂了context.WithTimeoutSQL 执行超过 5 秒直接取消防止助手发起一个全表扫描把连接占死。2.3 GaussDB 连接层的特殊考量GaussDB 虽然兼容 PostgreSQL 协议但实际对接时有几个地方要注意。驱动选择上标准的lib/pq能用但我更推荐pgx因为它在类型映射和批量查询上更完善GaussDB 的一些扩展类型比如numeric的大精度场景处理得更稳。连接参数上GaussDB 对sslmode的要求比 PG 严格生产环境一般强制require或verify-full这个在连接串里必须显式写清楚。权限层面我建议单独建一个账号只授予CONNECT和特定 schema 的USAGE再加上目标表的SELECT。不要图省事用现成的只读角色因为很多环境里的“只读角色”其实带了一堆系统表的查询权限。-- 建专用账号只给最小权限 CREATE USER mcp_reader WITH 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; -- 关键撤销默认的 public 权限 REVOKE ALL ON SCHEMA public FROM PUBLIC;这段 SQL 里最后一句REVOKE是重点。很多环境建完账号就完事了结果PUBLIC角色还留着默认权限等于白建。3. MCP 服务核心实现细节拆解3.1 MCP 协议对接工具定义怎么写MCP 协议里服务端要做的就是声明自己提供哪些工具tools每个工具包含名称、描述、参数 schema。Claude Code 启动时会拉取这个列表然后根据对话内容决定调用哪个。我定义了三个工具刻意保持精简工具名用途参数风险等级list_tables列出指定 schema 下的表schema 名低describe_table查看表结构、索引、约束表名低run_readonly_query执行只读 SQLSQL 语句中工具描述我写得比较“克制”没有写“可以执行任意查询”这种话而是明确写“仅支持 SELECT 语句且单次返回不超过 100 行”。这样助手在决定调用时行为会更保守。参数 schema 用 JSON Schema 定义比如run_readonly_query的sql参数我加了maxLength: 2000防止助手拼一个超长 SQL 进来。3.2 只读校验三层防线怎么设这是整个服务最核心的部分。我设了三层校验任何一层不过就直接拒绝。第一层是语句类型白名单。SQL 进来先做词法分析只允许SELECT、WITH、EXPLAIN、SHOW这几种开头。这里不能用简单的字符串HasPrefix因为/* comment */ SELECT这种能绕过。我用了一个轻量的 SQL parser 把注释和空白剥掉再判断。第二层是危险关键字黑名单。即使语句以 SELECT 开头里面也可能藏东西。比如SELECT ... INTO OUTFILE、SELECT ... FOR UPDATE、WITH x AS (DELETE ... RETURNING *) SELECT * FROM x。我把INSERT、UPDATE、DELETE、DROP、TRUNCATE、ALTER、GRANT、COPY、INTO OUTFILE、FOR UPDATE这些全部列进黑名单出现即拒。第三层是数据库侧的兜底。就算前两层都被绕过账号本身只有 SELECT 权限写操作也执行不了。这层是最后的安全网不能省。func validateSQL(sql string) error { cleaned : stripComments(sql) upper : strings.ToUpper(strings.TrimSpace(cleaned)) allowed : []string{SELECT, WITH, EXPLAIN, SHOW} ok : false for _, prefix : range allowed { if strings.HasPrefix(upper, prefix) { ok true break } } if !ok { return fmt.Errorf(仅允许只读语句) } forbidden : []string{ INSERT, UPDATE, DELETE, DROP, TRUNCATE, ALTER, GRANT, REVOKE, COPY, INTO OUTFILE, FOR UPDATE, FOR SHARE, LOCK TABLE, } for _, kw : range forbidden { if strings.Contains(upper, kw) { return fmt.Errorf(检测到禁止关键字: %s, kw) } } return nil }注意黑名单永远有漏网之鱼所以第三层的数据库权限才是真正的底线。前两层的作用是“尽早拦截、减少无效连接”不是“绝对安全”。3.3 结果集限制防止助手把库拖垮助手有个特点它不知道表有多大。你让它查数据它可能就来个SELECT * FROM big_table。所以我在服务端强制加了两道限制。行数限制不管 SQL 怎么写我在执行时外面套一层LIMIT。具体做法是解析原 SQL如果没有 LIMIT 就补一个LIMIT 100如果有但超过 100 就改成 100。这样即使助手写LIMIT 100000实际也只返回 100 行。超时限制每个查询挂 5 秒超时用context.WithTimeout实现。超时后pgx会取消查询连接归还池子。这里要注意 GaussDB 的查询取消机制pgx的Cancel在 GaussDB 上实测是生效的但偶尔会有延迟所以超时时间不要设得太极限5 秒是个比较稳的值。返回体积限制有些字段是 TEXT 或者 JSONB单行可能几 MB。我在扫描结果时对每个字段做了截断超过 4096 字符的部分用...[truncated]替代。这样即使查到大字段返回给助手的上下文也不会爆炸。4. 完整实操从零把服务跑起来4.1 环境准备与依赖安装先确认 Go 版本我用的 1.221.21 以上都行。go version # go version go1.22.0 linux/amd64建项目目录初始化模块mkdir gaussdb-readonly-mcp cd gaussdb-readonly-mcp go mod init github.com/yourname/gaussdb-readonly-mcp拉依赖主要是 MCP 的 Go SDK 和 pgx 驱动go get github.com/mark3labs/mcp-go go get github.com/jackc/pgx/v5 go get github.com/jackc/pgx/v5/pgxpoolmark3labs/mcp-go这个库我用下来比较顺手它把 MCP 的握手、工具注册、请求路由都封装好了我只需要关注业务逻辑。4.2 数据库连接池配置连接池参数不能拍脑袋定我按实际并发量算过。Claude Code 一般同时发起的工具调用不超过 3 个加上一些重试峰值按 10 算。每个查询平均 200ms那么理论上 2-3 个连接就够。但为了应对突发我设成config, err : pgxpool.ParseConfig(connString) if err ! nil { log.Fatal(err) } config.MaxConns 10 // 最大连接数 config.MinConns 2 // 保持的最小空闲连接 config.MaxConnLifetime 30 * time.Minute config.MaxConnIdleTime 5 * time.Minute config.HealthCheckPeriod 1 * time.MinuteMaxConns设 10 是留了余量MinConns设 2 保证冷启动时不用等建连。MaxConnLifetime30 分钟是为了避开 GaussDB 侧可能存在的连接老化策略具体值要看你环境的tcp_keepalives和中间件配置。连接串从环境变量读不写死在代码里export GAUSSDB_DSNpostgres://mcp_reader:password10.0.0.5:8000/your_db?sslmoderequireconnect_timeout5提示GaussDB 默认端口不一定是 5432很多部署用的是 8000 或者自定义端口这个要跟 DBA 确认清楚。sslmode生产环境务必用require以上。4.3 工具注册与请求处理MCP 服务的入口逻辑很清晰创建 server、注册工具、启动 stdio 或 SSE 监听。我用的是 stdio 模式因为 Claude Code 本地调用最方便。s : server.NewMCPServer(gaussdb-readonly, 1.0.0) s.AddTool(mcp.NewTool(list_tables, mcp.WithDescription(列出指定 schema 下的所有表名仅返回表名列表), mcp.WithString(schema, mcp.Required(), mcp.Description(schema 名称默认 public)), ), handleListTables) s.AddTool(mcp.NewTool(describe_table, mcp.WithDescription(查看表结构包括字段、类型、索引和约束), mcp.WithString(table, mcp.Required(), mcp.Description(表名)), ), handleDescribeTable) s.AddTool(mcp.NewTool(run_readonly_query, mcp.WithDescription(执行只读 SELECT 查询单次最多返回 100 行超时 5 秒), mcp.WithString(sql, mcp.Required(), mcp.Description(SELECT 语句)), ), handleQuery) if err : server.ServeStdio(s); err ! nil { log.Fatal(err) }每个 handler 的结构都差不多解析参数、校验、查库、格式化返回。以handleQuery为例func handleQuery(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { sql, err : req.RequireString(sql) if err ! nil { return mcp.NewToolResultError(缺少 sql 参数), nil } if err : validateSQL(sql); err ! nil { return mcp.NewToolResultError(err.Error()), nil } sql enforceLimit(sql, 100) ctx, cancel : context.WithTimeout(ctx, 5*time.Second) defer cancel() rows, err : pool.Query(ctx, sql) if err ! nil { return mcp.NewToolResultError(查询失败: err.Error()), nil } defer rows.Close() result : formatRows(rows, 100) return mcp.NewToolResultText(result), nil }enforceLimit的实现要注意不能简单地在末尾拼LIMIT 100因为原 SQL 可能已经有 LIMIT 或者以分号结尾。我的做法是先用正则匹配已有的 LIMIT 子句有就替换没有就在去掉末尾分号后追加。4.4 在 Claude Code 里配置接入服务编译好后在 Claude Code 的配置文件里加一段 MCP server 定义。配置文件位置根据系统不同Linux 下一般在~/.config/claude-code/config.json或者项目根目录的.claude/config.json。{ mcpServers: { gaussdb-readonly: { command: /usr/local/bin/gaussdb-readonly-mcp, env: { GAUSSDB_DSN: postgres://mcp_reader:password10.0.0.5:8000/your_db?sslmoderequire } } } }配好之后重启 Claude Code用/mcp命令能看到服务状态。如果显示 connected就说明握手成功了。这时候你可以直接问助手“帮我看看 orders 表的结构”它会自动调用describe_table。注意环境变量里的密码在配置文件里是明文所以这个文件的权限要设成 600而且不要提交到 git。更稳妥的做法是用系统的密钥管理工具但那是另一个话题了。5. 实际运行中踩过的坑与排查记录5.1 GaussDB 特有的报错与处理问题一unsupported frontend protocol。这个报错通常是因为连接串里带了 PG 特有的参数GaussDB 不认。我遇到过一次是因为加了?prefer_simple_protocoltrue去掉就好了。GaussDB 对扩展查询协议的支持和 PG 有差异遇到协议类报错先检查连接参数。问题二permission denied for schema pg_catalog。助手有时候会想查系统表比如SELECT * FROM pg_catalog.pg_class。虽然账号有 public schema 的权限但 pg_catalog 默认是不给的。我的处理方式是在validateSQL里加一条规则禁止查询pg_catalog、information_schema这些系统 schema从源头挡掉。问题三中文乱码。GaussDB 的字符集配置和客户端编码要匹配。连接串里加client_encodingUTF8能解决大部分问题。如果还有乱码检查数据库的server_encoding是不是 UTF8。5.2 助手行为异常的应对助手有时候会“自作聪明”。比如你问“这个表有多少行”它可能不调run_readonly_query而是先调describe_table看结构然后自己估算。这种行为本身无害但如果它反复调用同一个工具就会浪费连接。我的应对是在服务端加了简单的调用频率限制同一个工具在 10 秒内被调用超过 20 次就返回一个提示“调用过于频繁请稍后再试”。这个阈值是拍脑袋定的实测下来够用因为正常的排查场景不会这么密集。还有一种情况是助手拼的 SQL 语法不对GaussDB 返回一堆错误信息。这些错误信息里可能包含表名、字段名等元数据。我的处理是把错误信息做了一层过滤只返回错误码和简短的描述详细的堆栈只记到服务端日志里。5.3 常见问题速查表现象可能原因排查方向解决方式MCP 服务连不上二进制路径错/权限不足手动执行二进制看报错检查路径和 chmod x工具列表为空握手失败看 Claude Code 日志确认 MCP 协议版本兼容查询超时SQL 太重/连接池满看服务端日志的慢查询加索引或调大超时返回结果被截断触发了行数/字段限制看返回里的 truncated 标记缩小查询范围权限报错账号权限不足用账号手动连库测试补 GRANT 或调整 schema5.4 几个我踩过的具体坑坑一连接池泄漏。早期版本我在 handler 里忘了defer rows.Close()跑了一天之后连接池耗尽所有查询都卡住。这个错误很隐蔽因为单次测试看不出来。后来我加了连接池监控定期打印pool.Stat()能看到AcquiredConns的实时数量一旦接近MaxConns就告警。坑二超时后连接没释放。context超时后pgx会尝试取消查询但如果 GaussDB 侧取消得慢连接会短暂处于“正在取消”状态。如果这时候并发上来连接池会被占满。我的解决是把MaxConns从 5 调到 10留出缓冲同时在超时时间上从 3 秒放宽到 5 秒减少取消操作的频率。坑三助手把 EXPLAIN 当查询跑。EXPLAIN ANALYZE会真正执行 SQL如果助手对一个写操作做EXPLAIN ANALYZE那就出大事了。虽然我的白名单允许EXPLAIN但我在校验里加了一条EXPLAIN ANALYZE后面如果跟的不是 SELECT直接拒绝。这个细节很容易漏但后果很严重。6. 这套方案还能怎么扩展跑通之后我又做了几个小扩展成本很低但很实用。加了一个get_table_stats工具专门查表的行数估算和索引使用情况。数据来源是pg_stat_user_tables这个视图在 GaussDB 上也有但字段名和 PG 略有差异需要实测确认。有了这个工具助手在决定要不要查某张表之前能先知道表有多大避免盲目查询。日志接入了结构化输出每条记录包含时间戳、工具名、SQL 摘要、执行耗时、返回行数。这些日志我导到了一个本地的时序库里偶尔看看哪些查询最频繁能反过来优化我的工具设计。比如发现describe_table被调用得特别多我就给它加了一层内存缓存同样的表 5 分钟内只查一次库。支持了多数据源。通过环境变量配置多个 DSN工具参数里加一个datasource字段来切换。这样一套服务可以同时服务开发库和测试库助手问的时候明确指定就行。生产库我依然只开一个只读通道不跟其他环境混在一起。最后分享一个我在权限设计上的体会只读不是一种权限而是一种架构约束。你不能指望通过一个账号的权限设置来保证安全因为账号权限可以被改、可以被绕过。真正可靠的做法是在应用层把能力收窄到最小让“写”这个动作在架构上就不存在。我这个 MCP 服务里写操作的代码路径压根就没写这才是最踏实的。
网站建设高端定制企业官网