把发票台账接进 Codex:KingbaseES MCP 的一次只读风险排查实践

月末对账时,最容易被低估的一类工作,是"帮忙查一下这批发票"。

问题通常不难:查某个月已入账的记录、找重复票号、比较 OCR 识别金额和台账金额、列出没有附件的发票。难的是这些问题出现得很临时,字段又散在不同表里。开发人员需要先确认业务口径,再写 SQL、改条件、导出结果,最后把数据库里的几列字段翻译成财务同事能直接复核的清单。查一次不算什么,连续几天被不同的人打断,时间就都耗在重复沟通上。

这次把一个脱敏的发票台账放进 KingbaseES,使用 Kingbase-MCP 连接到 Codex。目标不是让模型替代数据库管理员,而是把"临时写 SQL"变成"用自然语言提出问题,再让数据库返回可复核的结果"。权限设计从一开始就限定为只读:MCP 使用 restricted 模式,KES 连接账号只拥有演示表的 SELECT 权限。即使客户端看不到写入工具,数据库层仍然保留第二道限制。

先把场景缩小到一张台账

演示数据放在 app_dbapp_schema 下,保留两张表:t_mcp_invoice_ledger 存发票台账,t_mcp_supplier 存供应商编码和状态。台账字段包括发票号、供应商编码、开票日期、含税金额、税额、OCR 金额、附件状态、入账状态和来源渠道。所有数据都是实验数据,票号使用 DEMO-INV- 前缀,不对应真实业务。

部署环境是 KingbaseES V009R001C010,数据库监听在服务器本机的 127.0.0.1:54321,MCP 也部署在同一台 todoitbo 服务器上。MCP 的 HTTP 服务只绑定 127.0.0.1:8000,Codex 在 Mac 上通过 SSH 隧道访问它。这样数据库端口和 MCP 端口都没有直接暴露给公网。

先固定 Kingbase-MCP 的源码提交,再创建 Python 3.12 虚拟环境。服务器自带的 Python 是 3.6.8,项目要求 Python 3.12,因此没有替换系统 Python,而是使用 uv 安装并调用并行的 3.12.13。

bash 复制代码
mkdir -p /opt/kingbase-mcp
cd /opt/kingbase-mcp
git clone https://gitee.com/king-db/kingbase-mcp.git
cd kingbase-mcp
git checkout cf68c1d686cbfed8e5fda1a72d4d160b7e7af0af

uv venv --python 3.12 .venv
source .venv/bin/activate
uv pip install .

虚拟环境创建完成后,安装项目依赖,分别导入 ksycopg2mcp,最后执行命令入口的帮助信息。这里留下帮助输出,是为了在服务启动前确认驱动、MCP SDK 和命令入口都能被当前解释器加载。

ksycopg2 的版本信息中已经带出 Kingbase V9 适配信息。驱动和命令入口先跑通,连接失败时就能先排除"解释器错了"或"驱动没有装上",不必一开始就在数据库网络和账号配置上反复试。

数据库权限先于自然语言查询

KES 中单独创建 mcp_readonly 账号,只授予 app_schema.t_mcp_invoice_ledgerapp_schema.t_mcp_supplier 两张表的 SELECT 权限。连接串放在服务器的 /etc/kingbase-mcp.env 中,文件只允许 root 读取,MCP 进程通过环境变量加载它。

这条约束不能由 MCP 的工具列表替代。今天的程序可能只暴露 execute_sql,以后升级后工具数量、SQL 解析逻辑或客户端能力都可能变化;数据库账号的权限则直接由 KES 执行。即使有人误把 UPDATE 语句交给 MCP,数据库仍会拒绝写入。

使用只读账号查询台账行数,然后故意提交一条 UPDATE ... WHERE 1 = 0。条件不会命中任何数据,但 KES 仍然先检查写权限,因此返回 permission denied。这比只看"影响行数为 0"更有意义:失败原因来自权限,而不是因为条件碰巧没有匹配记录。

MCP 的职责是连接和约束

手工启动 MCP 时显式指定三个参数:streamable-http 作为传输方式,服务绑定 127.0.0.1:8000,访问模式设置为 restricted。启动日志同时给出数据库连接池初始化成功和 HTTP 服务启动完成,说明它不只是把端口占住,而是已经完成了 KES 连接。

bash 复制代码
cd /opt/kingbase-mcp/kingbase-mcp
source .venv/bin/activate
set -a
source /etc/kingbase-mcp.env
set +a

kingbase-mcp \
  --transport streamable-http \
  --streamable-http-host 127.0.0.1 \
  --streamable-http-port 8000 \
  --access-mode restricted

手工验证通过后,再交给 systemd 管理。服务启动后自动重启,日志集中写入 journal,机器重启也不会丢掉启动配置。最终的服务命令仍然保持 restricted 和本机回环监听。

服务化之后,前台终端不再承担进程存活责任。排查时使用 systemctl status 看进程状态,使用 journalctl -u kingbase-mcp 看应用日志;两处信息结合起来,既能识别入口执行失败,也能区分"进程已运行"和"连接池已经连上 KES"。

部署过程中出现过一次 status=203/EXEC。入口脚本本身有执行权限,但虚拟环境里的 Python 软链接指向 /root/.local/share/uv/...,切换到另一个 Linux 账号执行时无法穿过 /root 目录,最终表现为 Permission denied。这个错误发生在 systemd 执行入口之前,和 KES 连接、SQL 权限都还没有关系。

这次演示环境最终保留 root 运行方式,原因是当前 uv 的 Python 安装位置和虚拟环境都由 root 管理,先保证部署路径可复现。生产环境仍应把解释器、项目目录和环境文件迁移到专用的非 root 服务账号下;无论进程用什么 Linux 账号,KES 的 mcp_readonly 都不应改成有写权限的业务账号。

修正入口后重新加载 systemd,服务状态为 active (running),日志可以同时看到 RESTRICTED mode、连接池初始化成功以及 127.0.0.1:8000。这几个信息需要一起看,单独的绿色 active 状态不能证明数据库真的可用。

服务注册后,排查不靠猜。先在服务器上检查监听地址,再查看服务日志;需要确认数据库权限时,直接用 mcp_readonly 登录 KES 执行一条不会命中数据的 UPDATE。三条命令对应三个不同层面,输出也不会混在一起:

bash 复制代码
ss -lnt | awk '$4 ~ /127\.0\.0\.1:8000$/'
journalctl -u kingbase-mcp -n 80 -l --no-pager
ksql -h 127.0.0.1 -p 54321 -U mcp_readonly -d app_db

第一条应返回 127.0.0.1:8000,而不是 0.0.0.0:8000;第二条需要同时出现 RESTRICTED modeSuccessfully connected to database;第三条执行 UPDATE 时应得到权限拒绝。端口正常只能说明 MCP 在运行,连接成功才说明环境变量中的连接串可用,权限拒绝才说明 KES 没有把台账写权限交给 MCP。

当前客户端没有写入接口,仍然保留只读数据库账号。工具清单会随版本变化,SQL 校验也会随着支持的语法扩大;SELECT 权限由 KES 在执行时检查,不依赖客户端是否恰好隐藏了某个按钮或工具。出现误调用时,数据库会停住请求,台账不会被改动。

MCP 放在本机还是服务器,先看 KES 能否直达

Kingbase-MCP 并不要求一定和 KES 部署在同一台机器上。常见环境可以分成两种。

第一种是运维终端能够直接访问 KES。企业内部通常通过办公网、VPN 或专线访问数据库私网地址,少数受控环境也会使用来源 IP 白名单限制的数据库地址。在这种网络条件下,MCP 可以直接运行在运维人员本机,通过 stdio 与 Codex 通信,配置中填入 KES 连接串即可:

toml 复制代码
[mcp_servers.kingbase-mcp]
command = "uv"
args = [
  "--directory", "/Users/user/CodeDir/mcp/kingbase-mcp",
  "run", "kingbase-mcp",
  "--access-mode", "restricted"
]

[mcp_servers.kingbase-mcp.env]
DATABASE_URI = "kingbase://mcp_readonly:<URL编码后的密码>@<KES私网地址>:54321/app_db"

这种方式不需要在服务器上长期运行 MCP 服务,适合已经具备数据库私网访问条件的个人开发和日常查询。KES 即使提供了可达地址,也不等于应把 54321 对所有公网来源开放;地址可达范围、来源白名单、传输加密和数据库最小权限仍然需要单独控制。

第二种是运维终端无法直接访问 KES。当前环境就属于这种情况:KES 只监听服务器本机的 127.0.0.1:54321,没有向公网开放数据库端口。因此把 Kingbase-MCP 部署到 KES 所在服务器,由它在本机回环地址上连接数据库;MCP 的 8000 端口也只监听 127.0.0.1,Mac 端通过 SSH 本地转发访问。

本机 MCP 配合数据库端口隧道同样可以实现连接,但每台运维终端都要安装 Python、驱动和 Kingbase-MCP,并保存数据库连接凭据。当前方案把运行环境、源码版本和只读数据库连接串集中留在服务器,Mac 端只保存一个本地 MCP 地址,更适合这次长期保留的只读查询入口。

SSH 隧道也不要求运维人员进入服务器交互终端。-N 表示只建立转发,不执行远程命令;隧道建立后,日常查询和结果核对仍然在本机 Codex 中完成。这里把本机 127.0.0.1:18000 转到服务器的 127.0.0.1:8000,再注册 MCP:

bash 复制代码
ssh -N -L 18000:127.0.0.1:8000 root@todoitbo

codex mcp add kingbase-mcp --url http://127.0.0.1:18000/mcp
codex mcp get kingbase-mcp

Codex 使用的是 http://127.0.0.1:18000/mcp,服务器侧实际服务地址仍是 http://127.0.0.1:8000/mcp。KES 的 54321 端口不需要开放给 Mac,数据库账号和连接串也不会进入 Codex 配置。对运维人员而言,服务器登录从日常查询动作变成了一条只负责转发的 SSH 连接;对数据库而言,外部仍然没有直接访问入口。

这种隧道方式适合个人开发、远程排查和 DBA 日常查询。需要给内网多名使用者提供服务时,应保留 MCP 到 KES 的本机连接,在 MCP 前放置公司内网网关或反向代理,启用 HTTPS、身份认证、访问日志和访问控制,再由网关提供受控地址。当前 8000 端口没有直接绑定到 0.0.0.0,也不作为公网服务使用。

接入后的第一个问题很简单:列出 app_schema 下名称以 t_mcp_ 开头的表。Codex 实际调用 kingbase-mcp.execute_sql 查询 information_schema.tables,返回了两张演示表。先验证对象发现,再进入业务查询,能避免模型猜错 schema 或表名。

表名确认后,查询才进入业务口径。这里没有让 Codex 直接猜"已入账"的含义,而是先在台账中确认 ledger_status 的实际取值,再把月份范围、排序字段和需要返回的列写进人工 SQL。对象发现解决的是"查哪张表",参照查询解决的是"什么结果才算正确"。

先用人工 SQL 留下参照结果

自然语言查询之前,先用 mcp_readonly 执行确定的 SQL,留下可以逐项核对的参照结果。2026 年 7 月已入账记录共有 7 行,其中 DEMO-INV-202607-004 出现两次,金额合计 6400.00。

这组结果先回答了最基础的问题:哪些记录属于本月已入账范围,哪些字段可以直接交给业务人员核对。两条相同的 ...004 并不是查询重复返回,而是表中确实存在两行相同票号记录;金额合计 6400.00 也因此成为后面风险清单里的复核线索。人工结果留在终端中,后面每次让 Codex 查询都用同一组条件对照。

金额核对采用"OCR 金额 - 台账金额"的定义。DEMO-INV-202607-006 的台账金额为 4999.00,OCR 金额为 4500.00,差异为 -499.00。负号说明 OCR 结果小于台账金额,是否属于录入问题还需要回到原始票据复核,不能直接下结论。

同一批数据中,DEMO-INV-202607-007 的附件状态为 missing,金额为 2600.00。它是资料完整性风险,不等于金额一定有问题。

人工 SQL 还把后续解释需要的字段和口径固定了下来:重复票号按 invoice_no 分组,只有 count(*) > 1 才进入清单;金额差异明确采用 ocr_amount - amount,因此 OCR 少识别 499.00 时显示为 -499.00;附件筛选同时关注 missing 和空值。条件先写清楚,后面的自然语言查询才有可比性。否则同一句"查异常",可能有人把全部状态都算进去,也可能有人把金额差异取绝对值,最终看起来都像有结果,实际口径已经变了。

把"查数据"交给自然语言

参照结果准备好后,让 Codex 查询 2026 年 7 月已入账发票,并要求返回票号、供应商编码、金额、附件状态和实际执行的 SQL。Codex 先读取字段定义和状态值,再执行日期范围查询,最终返回 7 行记录。查询结果与人工 SQL 的 7 行保持一致,日期条件使用左闭右开范围,避免把 8 月 1 日的数据混进来。

查询条件、返回字段和排序方式都被保留下来,开发人员可以快速判断它有没有漏掉重复记录,也能让业务人员拿着结果回到原始台账逐行核对。对于临时查询,能够看到实际 SQL 比只得到一段摘要更可靠:日期范围写错、状态值写错、把 ocr_amount 当成台账金额,这些问题在 SQL 中很快就能定位。

接着把三个风险条件一次交给 Codex。它分别执行重复票号聚合、OCR 与台账金额比较、附件状态筛选,返回的票号与人工 SQL 参照结果一致:重复票号是 ...004,金额差异是 ...006,附件缺失是 ...007

这里要求返回 SQL,是因为"发现异常"和"解释异常"是两件事。重复次数、金额差值、附件状态都能回到具体字段和条件;如果结果不一致,可以直接比较 SQL,而不是凭自然语言重新猜测模型的判断过程。三条查询还分别覆盖了聚合、字段比较和状态筛选,既能验证 MCP 的基本执行能力,也能暴露模型对业务状态值、金额方向和空值处理的理解是否准确。风险清单因此不是一段脱离数据的总结,而是每一行都能回到一个查询条件。

没有写入工具,也要验证写入边界

最后尝试把 DEMO-INV-202607-007 的附件状态改成 complete。当前 MCP 只暴露只读 execute_sql 工具,没有可执行写入的工具,因此 Codex 没有提交 UPDATE,而是先查询当前值并明确说明无法完成修改。复核结果仍然是 missing

这个结果不能替代数据库权限验证。工具层没有写入口,只能证明当前客户端路径不会执行修改;前面的 KES 权限测试才证明数据库账号本身没有写权限。两层限制同时存在,才不会因为某次客户端配置变化、工具扩展或 SQL 校验缺陷而直接写入台账。

这套做法解决了什么

落地前,财务同事提出一个问题,通常需要开发人员介入:确认表、确认状态值、写 SQL、解释结果,再把异常记录整理成清单。接入 MCP 后,Codex 可以处理查询入口和结果整理,开发人员把时间放在口径确认、权限设计和异常复核上。

它没有把数据库交给模型,也没有把"风险"自动变成结论。查询仍然发生在 KingbaseES 中,账号仍然由 KES 控制,MCP 只在受限模式下转发允许的只读 SQL。重复票号只表示同一票号在台账中出现多条记录,OCR 差异只表示两个金额字段不一致,附件缺失只表示资料状态未完成;是否构成财务问题,仍需人工回到原始凭证和业务流程。

实际部署中最容易漏掉的不是自然语言提示,而是权限边界:MCP 没有编辑工具时,数据库账号也不能因此使用普通业务账号;服务能启动时,日志还要确认连接池初始化;Codex 能列出表时,风险结果仍要和人工 SQL 对照。把这三处验证保留下来,发票查询才从一次演示变成可以放进工作流程的只读辅助工具。

相关推荐
Navicat中国1 小时前
使用 Navicat 轻松生成数据库测试数据
数据库·数据
zyplayer-doc1 小时前
核心制度不该被随手修改:用zyplayer-doc锁定文档和全部子文档
大数据·数据库·人工智能·笔记·pdf·ocr
一路向北North2 小时前
Spring AI(11) :ChatPDF-向量数据库、PDF处理、向量写入和向量搜索
数据库·人工智能·spring
暗暗别做白日梦2 小时前
CompletableFuture 并发统计
网络·数据库·oracle
VIP_CQCRE2 小时前
用 Ace Data Cloud 接入 Codex:让 AI 编程工具配置更简单
openai·ai编程·开发工具·codex·acedatacloud
ClouGence2 小时前
告别 Navicat!CloudDM:开源免费一站式数据库开发管理工具
数据库·sql·开源·数据库开发
灵析表格2 小时前
灵析表格功能函数深度分析报告
前端·数据库·microsoft
jaboo122 小时前
postgresql从入门到精通
数据库·postgresql·langchain
晴天¥2 小时前
记一次Oracle集群归档日志爆满之后如何处理(涉及ASM磁盘的增添。有坑必踩)
数据库·oracle