mcp-guarded-toolkit
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@mcp-guarded-toolkitlist all tables in the sample database"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
mcp-gatekeeper
一个基于 MCP(Model Context Protocol) 的本地工具服务:把一份示例数据库和一份公版文本库 开放成 6 个工具。其中「让模型自己写 SQL 查数据库」这条链路必须真的安全 —— 所以它配了三层 SQL 护栏、工具级 scope 鉴权、有界重写状态机和可复核的数字口径。
它的用途是技术验证:证明这套工程约束是可迁移的,而不是绑在某一份数据上的特例。
只用公开、可再分发的数据:示例库 + 公版中文文本(逐篇出处见 NOTICE.md)
不联网、不需要模型凭据就能跑完全部测试与自检(自检里靠词表的那几项会判
SKIP, 见「已知边界」——SKIP是「没查」,不是通过)报告里每个数字都能指出「脚本名 + 口径」,没有估计值
需要模型凭据的那条链路已经跑通(2026-09-18 的评测;2026-09-22 又在并发下重跑了一遍对账), 数字落盘在 RESULTS.md 第二节与 reports/ledger_under_concurrency.md
本仓库不放凭据(
.env也不放,理由见 BLOCKERS.md 卡点 1),所以直接重跑 需要凭据的脚本会一律报「未完成」(退出码 5 + 一份说明),不编数字。 本轮真跑通的那几次,凭据是以进程环境变量注入的,没落仓库盘
快速开始
# 1) 单元测试(零网络、零模型调用)
python -m pytest -q
# 2) 构建数据:示例库(.sql → .db)与文本库(切词 → 全文索引)
python experiments/01_build_db.py
python experiments/02_build_kb.py
# 3) 安全层与协议层:鉴权矩阵、护栏负例矩阵、真实 stdio 握手
python experiments/10_auth_test.py
python experiments/11_guardrail_test.py
python experiments/08_mcp_smoke.py
# 4) HTTP 入口与并发:真起服务、真 TCP 冒烟;以及并发度的实测读数
python experiments/30_http_smoke.py
python experiments/29_concurrency_bench.py
# 5) 一条命令回答「现在能不能交出去」
python tools/check.pypython tools/check.py 会依次跑单元测试、台账式自检、交付物核验、收尾自检,
退出码 = 卡在第几步(0 = 全部通过)。只想看它要跑什么:python tools/check.py --list。
起服务端
python -m mcp_server.server --check # 只做启动自检:注册的工具与 scope 表是否一致
python -m mcp_server.server # 真正拉起 stdio 服务端
python -m mcp_server.http_api --check # HTTP 侧的自检:路由名集合 == TOOL_SCOPES 键集合
python -m mcp_server.http_api # 拉起 HTTP 服务(默认 127.0.0.1:8000)
python -m mcp_server.http_api --port 0 # 由系统分配端口,启动行会打印真实端口stdio 服务端的 stdout 是协议通道,所以人话一律走 stderr —— 打印一行启动信息到
stdout 会让握手直接报 Invalid JSON(这个 bug 只有真实握手能抓到,见 PROGRESS.md 阶段 3)。
HTTP 侧沿用同一条规矩:人话走 stderr,端口也打在那儿([http] 监听 127.0.0.1:<port>)。
服务端自己先 bind 好 socket 再交给 uvicorn,所以端口是确定的,不存在「先探测空闲端口、
启动时被抢走」的竞态。
起好之后,/docs 是 OpenAPI 自动生成的交互文档,/openapi.json 是机器可读的那份。
Related MCP server: any-db-mcp
工具集
6 个工具、3 个 scope。scope 挂在工具上而不是「用户」上,所以换一个客户端、 换一次会话,权限判定都一样。
工具 | scope | 做什么 |
|
| 列出所有表和视图 |
|
| 看某张表的列与类型 |
|
| 执行一条只读查询(过三层护栏) |
|
| 自然语言提问 → 模型写 SQL → 护栏 → 执行 → 失败则重写 |
|
| 在公版文本库里检索段落,返回原文片段与偏移量 |
|
| 按偏移量取回原文 |
调用工具要带一个 JWT,令牌里带着 scope 列表:
from mcp_server import auth
token = auth.make_token("demo-user", scopes=["db:read", "kb:read"])ask_database 是唯一需要模型凭据的工具;其余 5 个不需要。
同一批工具的 HTTP 入口
mcp_server/http_api.py 把上面这 6 个工具挂成 HTTP 路由。路由是照着 auth.TOOL_SCOPES
现生成的(不是第二份工具清单),请求体就是那个工具的 kwargs(含 token),
响应体就是工具原本的返回值 —— 同一份业务逻辑,两个传输层。
方法 | 路径 | 说明 |
POST |
| 调一个工具;上表那 6 个名字各有一条路由 |
GET |
| 工具名 + 所需 scope 的清单 |
GET |
| 存活探针 |
GET |
| OpenAPI 交互文档(FastAPI 自带, |
状态码不是一刀切:拒绝 → 403(code 原样带回)、未知工具 → 404、参数绑不上 → 400,
被 SQL 护栏拦下的查询是 200 —— 那是业务结论(请求被正确执行并给出了结论),
报 4xx 会让「护栏工作正常」看起来像「调用失败」。完整映射见
mcp_server/http_api.py 的 status_for(),逐条断言在 tests/test_http_api.py。
并发调用走 mcp_server/concurrency.py:同步阻塞的工具函数一律 asyncio.to_thread
挪出事件循环,Semaphore 限并发,wait_for 超时降级成一条结果记录而不是拖垮整批。
为什么必须挪、以及不挪会怎样(实测并发度退化成 1),见 reports/concurrency.md。
Text2SQL 状态机:手写版与 LangGraph 版
同一个「有界重写」状态机有两套实现,并列保留,不是替换:
手写版 | LangGraph 版 | |
控制流 | 一个 | 4 个节点 + 2 个路由函数 + 1 条回边 |
谁在用 | MCP 的 | 测试(与手写版跑同一批断言);评测脚本的 |
依赖 | 无框架 |
|
两版共用提示词、取 SQL、失败批注、预算常量(最多 3 次模型调用)、护栏执行和返回体,
只有控制流不同。tests/test_t2sql_state_machine.py 里所有调用 run() 的用例对两版各跑一遍;
另有一组对照用例,要求同一剧本下两版发给模型的每一轮消息、返回体逐字相同。
接上真模型的对照(2026-10-23,deepseek-chat,提示词 v3,同一天两版各跑一遍 36 题):
python experiments/09_text2sql_eval.py --versions v3 --impl hand --tag 20261023
python experiments/09_text2sql_eval.py --versions v3 --impl graph --tag 20261023
python experiments/33_impl_compare.py --tag 20261023两版都是可答题 31/32、不可答题拒答 4/4、每题平均 1.00 次调用,逐题判定翻转 0 道; 有 3 道题两版写出的 SQL 文本不同但都判对,那是模型输出自己的抖动。明细见 reports/impl_compare.20261023.md。 这组数和 RESULTS.md 第二节 2026-09-18 那一轮不是同一个模型、同一个端点,不能拿来直接比。
下图由 GRAPH.get_graph().draw_mermaid() 直接导出:实线是固定的边,虚线是路由函数决定的条件边。
测试会核对这张图和代码现画出来的是否一致。
---
config:
flowchart:
curve: linear
---
graph TD;
__start__([<p>__start__</p>]):::first
prepare(prepare)
generate(generate)
execute(execute)
repair(repair)
__end__([<p>__end__</p>]):::last
__start__ --> prepare;
execute -.-> __end__;
execute -.-> repair;
generate -.-> __end__;
generate -.-> execute;
generate -.-> repair;
prepare --> generate;
repair --> generate;
classDef default fill:#f2f0ff,line-height:1.2
classDef first fill-opacity:0
classDef last fill:#bfb6fc为什么先手写、再迁移
先手写,是为了先把行为钉死。 这个状态机只有一条循环、三种失败、一个上限, 手写一遍最容易看清每一步发生了什么;再用假模型把控制流测透(重写几次、批注怎么写、 什么时候停)。这批测试就是行为规格。
再迁移,迁移才有可判定的标准。 规格在手,迁移就是「换控制流、规格不变」—— 同一批测试两版都要过,而不是「看起来能跑」。
反过来先上框架,框架用法和业务逻辑同时在变,测试红了分不清是框架用错了还是逻辑写错了。
两种实现的取舍
LangGraph 带来的能力,以及本仓库实际用上了哪些:
能力 | 是什么 | 本仓库 |
持久化(checkpointer) | 每走一步把 State 存档;同一个会话( | 接了,只用于多轮追问 |
中断恢复(interrupt) | 在某个节点前后停下,等人确认或修改 State 再继续 | 未用 |
可视化 | 从代码直接导出流程图 | 用了:上面那张图 |
流式输出(stream) | 每个节点跑完就产出一次更新,不必等整张图跑完 | 未对外提供 |
LangGraph 的代价:
多一个依赖:
langgraph本身之外还带进 21 个传递依赖,其中 8 个是编译扩展 (明细见requirements.txt文件头);以后升级版本要跟着回归。多一层抽象:原来一个函数从头读到尾;现在要对照 State 定义、节点、路由、组图四处, 还要先懂 reducer、context、
recursion_limit这些框架概念。出问题要隔着框架调试:异常栈里多出框架自己的调用层;框架参数的口径要实测才知道 —— 例如走满预算的 9 个节点,
recursion_limit设 9 会报错、设 10 才够(见t2sql_graph.py的注释), 而这个版本的默认值是 10007,不显式设就几乎等于没有上限。
手写版反过来:零依赖、一个函数读到底、单步调试直接;代价是状态藏在循环的位置里, 存档、恢复、流式这些都要自己写。
所以两版的定位是:手写版是行为基准,LangGraph 版是同一份行为在框架上的表达。
目录结构
mcp_server/ 服务端与全部纯逻辑(可 import,因此可单测)
auth.py JWT 签发/校验/guard(),工具级 scope 表
guardrails.py 三层 SQL 护栏
db_tools.py 列表 / 结构 / 查询
kb_tools.py 检索 / 取原文
t2sql_core.py 自然语言转 SQL 的状态机(有界重写)
t2sql_graph.py 同一个状态机的 LangGraph 版(并列实现,手写版保留)
cost.py 用量账本(JSONL 只追加,金额允许为空)
dispatch.py 名字 → 工具函数的派发器(HTTP 层的唯一入口)
http_api.py FastAPI 入口:路由由 TOOL_SCOPES 现生成
concurrency.py asyncio 并发层:限流 + 超时降级 + 阻塞调用挪出事件循环
selfcheck.py 仓库卫生扫描口径(词表、范围、掩码)
verify_kit.py 退出码台账与判定
deliverable_kit.py 交付物核验 G1~G8
experiments/ 可重跑的入口脚本(全部幂等)
29_concurrency_bench.py 三种跑法的实测并发度对照
30_http_smoke.py HTTP 入口冒烟(真进程、真端口)
31_ledger_under_concurrency.py 并发下的账本对账(需要模型凭据)
33_impl_compare.py 手写版 vs LangGraph 版的真模型逐题对照(不调模型,读 09 的产物)
tests/ 单元测试:零网络、零模型调用
tools/check.py 守门链
data/ 公开数据(示例库 .sql + 公版文本 + 出处清单)
reports/ 全部实测产物落盘
docs/ 设计说明数据与许可
示例数据库:Chinook(MIT),随仓库分发
.sql源文件,由脚本构建成数据库文本库:40 篇公版中文文本,逐篇的标题、作者、原文地址、许可与 sha256 见 NOTICE.md
构建产物(数据库文件、全文索引)不入库,由
experiments/01_build_db.py与experiments/02_build_kb.py确定性重建
怎么证明它是对的
四层,从细到粗:
谁 | 管什么 |
| 某个函数对不对 |
每个入口脚本的实际退出码对不对得上声明 | |
文档里写的和仓库里对不对得上 | |
发布前 9 项红线,逐项报命中数 |
三条原则写在 docs/design.md 里,这里只列结论:
每一项检查都要能红。 只证明「会绿」的检查,一个永远返回
PASS的假实现也能通过。「未完成」必须交证据。 需要模型凭据的脚本以退出码 5 结束,同时留下写清卡在哪一步的 「未完成」说明和
BLOCKERS.md里对应的最小动作。三件缺一,判失败 —— 没有落盘、没有解法的「未完成」,和「忘了跑」长得一模一样。报告不能成为它自己报告的污染。 自检报告要写出「在哪里命中了什么」, 而写出来的那一刻,报告自己就带上了那个词。处置方式见
docs/design.md第 4.4 节。
当前的完成状态
部分 | 状态 |
数据构建、鉴权、三层护栏、MCP 握手、知识库检索 | 已完成,有实测报告 |
自然语言转 SQL( | 已跑通一次(2026-09-18):配好凭据后四条链路全部跑完并落盘,数字见 RESULTS.md 第二节。凭据不在仓库里,所以 clone 后要自配一份才能重跑(见 BLOCKERS.md 卡点 1) |
噪声带与配对统计 | 同上那次运行里跑完:尺子有正负控,数字见 RESULTS.md 第二节 |
HTTP 入口(FastAPI) | 已完成:6 条工具路由 + |
asyncio 并发层 | 已完成:限流、超时降级、阻塞调用挪出事件循环。实测并发度(不是设定值):串行 1 / 天真 gather 1 / 正确并发 6,见 reports/concurrency.md |
并发下的账本一致性 | 已完成(2026-09-22,凭据以环境变量注入):并发批与串行批各 12 题,账本新增行数都等于实际调用次数 12,见 reports/ledger_under_concurrency.md |
真实跑出来的数字见 RESULTS.md;每个数字都注明了脚本与口径。
已知边界
卫生扫描用的词表按设计不在仓库里:那张「不许出现的名字 / 数字 / 术语」表 放在仓库外面(
MCP_TOOLKIT_WORDLIST指向的.py,没设就找仓库同级目录的mcp-guarded-toolkit-wordlist.py)。把它放进仓库等于在仓库里再造一份那些字面量 —— 那正是这个仓库修掉的那座「明文桥」。代价写清楚:clone 下来跑自检时, 靠词表的那几项判SKIP(没查,不是通过),所以python tools/check.py在没有词表的机器上停在「未完成」,不会全绿。这不是失败,但也不是通过。第一次跑之前要构建数据:
data/下的示例库与全文索引是构建产物、不入库, 所以 clone 下来直接跑闸门会停在「示例库还没构建」,先跑「快速开始」第 2 步 那两条命令(不需要联网)。不联网:数据随仓库分发;只有「重新抓语料」这一个脚本需要网络,且它是可选的。
护栏不是形式化证明:三层护栏(文本校验 / 只读连接 / 引擎级 authorizer) 挡的是「写不进数据库」,不是「SQL 一定正确」。
experiments/11_guardrail_test.py列了 41 条负例,那 41 条之外没测过的写法是未知的。交付物核验 G4 的强度有限:它查「文档里的
--flag在源码里出现过」, 不保证argparse真的注册了那个参数。本机现在没有模型凭据,所以那四条需要模型的命令不能重跑 —— 它们会以退出码 5 结束, 并写出「未完成」声明。已经跑通的那一次(2026-09-18)数字在 RESULTS.md 第二节, 是实测,不是 0,也不是任何估计值。(本机能拿到凭据与「工作区里没有
.env」这条发布自检 互斥,所以两者不可能同时成立,见 BLOCKERS.md 卡点 1。)
设计取舍与代价逐条记在 DECISIONS.md,整体思路见 docs/design.md。
This server cannot be deployed
Maintenance
Related MCP Connectors
Text-to-SQL MCP server: read-only queries on PostgreSQL, MySQL and SQL Server with your real schema
The Remote MCP server acts as a standardized bridge between LLM applications (like Claude, ChatGPT, and Cursor) and external services, enabling AI agents to access external tools and resources. Its primary capability is providing a centralized search tool to discover other MCP servers and their respective tools. Unlike local implementations, it runs remotely with OAuth authentication and permission controls for security.
MCP server for your apps' tools and custom tools, plus hosted AI agents and approval-gated workflows
- gatewayOAuthai.sealgate
MCP gateway with runtime security policy, tool-call-level control, and audit of agent actions.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceA production-grade MCP server designed for multi-tenant, authenticated, and observable AI agent systems, enabling secure tool execution across heterogeneous data sources.67MIT
- AlicenseAqualityDmaintenanceA multi-database MCP server that enables LLMs to safely interact with MySQL, PostgreSQL, SQLite, and others through a unified tool interface, with permission modes and schema resources.102 npm51MIT
- FlicenseNot gradedqualityDmaintenanceA custom MCP server with 6 utility tools (file search, file reading, math calculation, JSON formatting, time query, system info) that demonstrates MCP protocol workflow and integrates with LangChain agents.-
- AlicenseBqualityBmaintenanceA universal MCP server enabling AI assistants to query and manage six database engines (Postgres, Redis, Elasticsearch, MySQL, MongoDB, LDAP) through 113+ tools with read-only safety and fault tolerance.10012 npm80MIT