Skip to main content
Glama
Hone125

mcp-guarded-toolkit

by Hone125

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.py

python 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

做什么

list_tables

db:read

列出所有表和视图

get_schema

db:read

看某张表的列与类型

run_sql

db:query

执行一条只读查询(过三层护栏)

ask_database

db:query

自然语言提问 → 模型写 SQL → 护栏 → 执行 → 失败则重写

search_passages

kb:read

在公版文本库里检索段落,返回原文片段与偏移量

get_passage

kb:read

按偏移量取回原文

调用工具要带一个 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

/tools/<工具名>

调一个工具;上表那 6 个名字各有一条路由

GET

/tools

工具名 + 所需 scope 的清单

GET

/healthz

存活探针

GET

/docs

OpenAPI 交互文档(FastAPI 自带,/openapi.json 是机器可读的那份)

状态码不是一刀切:拒绝 → 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。

目录结构

mcp_server/     服务端与全部纯逻辑(可 import,因此可单测)
  auth.py          JWT 签发/校验/guard(),工具级 scope 表
  guardrails.py    三层 SQL 护栏
  db_tools.py      列表 / 结构 / 查询
  kb_tools.py      检索 / 取原文
  t2sql_core.py    自然语言转 SQL 的状态机(有界重写)
  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   并发下的账本对账(需要模型凭据)
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 确定性重建

怎么证明它是对的

四层,从细到粗:

谁

管什么

python -m pytest

某个函数对不对

reports/verify.md

每个入口脚本的实际退出码对不对得上声明

reports/deliverable_check.md

文档里写的和仓库里对不对得上

reports/final_selfcheck.md

发布前 9 项红线,逐项报命中数

三条原则写在 docs/design.md 里,这里只列结论:

  • 每一项检查都要能红。 只证明「会绿」的检查,一个永远返回 PASS 的假实现也能通过。

  • 「未完成」必须交证据。 需要模型凭据的脚本以退出码 5 结束,同时留下写清卡在哪一步的 「未完成」说明和 BLOCKERS.md 里对应的最小动作。三件缺一,判失败 —— 没有落盘、没有解法的「未完成」,和「忘了跑」长得一模一样。

  • 报告不能成为它自己报告的污染。 自检报告要写出「在哪里命中了什么」, 而写出来的那一刻,报告自己就带上了那个词。处置方式见 docs/design.md 第 4.4 节。

当前的完成状态

部分

状态

数据构建、鉴权、三层护栏、MCP 握手、知识库检索

已完成,有实测报告

自然语言转 SQL(ask_database)

已跑通一次(2026-09-18):配好凭据后四条链路全部跑完并落盘,数字见 RESULTS.md 第二节。凭据不在仓库里,所以 clone 后要自配一份才能重跑(见 BLOCKERS.md 卡点 1)

噪声带与配对统计

同上那次运行里跑完:尺子有正负控,数字见 RESULTS.md 第二节

HTTP 入口(FastAPI)

已完成:6 条工具路由 + /tools + /healthz + OpenAPI 文档;真实子进程冒烟 20 项全过,见 reports/http_smoke.md

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。

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    D
    maintenance
    A 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.
    10
    10 npm
    63
    MIT
  • F
    license
    Not graded
    quality
    D
    maintenance
    A 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.
    -
  • A
    license
    B
    quality
    B
    maintenance
    A 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.
    100
    16 npm
    82
    MIT