mattdfuchs/extensible-mcp
GitHub: mattdfuchs/extensible-mcp
一个位于 LLM 与 MCP 服务器之间的安全代理,通过 RAG 按需检索工具、确定性过滤器管道执行访问控制,解决多 MCP 服务器环境下的 token 膨胀与工具调用安全问题。
Stars: 0 | Forks: 0
# extensible-mcp
extensible-mcp 是一个位于 LLM 和众多 MCP 服务器之间的代理,提供按需工具检索和确定性的访问控制执行点。工具定义无需存放在提示词中,敏感凭证无需保留在 LLM 的上下文中,且安全策略由代码而非模型进行评估。
## 为什么需要它
将 LLM 客户端连接到一组 MCP 服务器通常是一个启动时的决定:在配置中列出服务器,启动客户端,祈祷你猜对了。没有简洁的方法可以在对话中途添加服务器,或者让 LLM 自身去获取未预先配置的功能。
即使服务器连接成功,LLM 客户端也会接收到来自每个服务器的每一个工具的扁平列表,并被全量注入到上下文窗口中。随着服务器数量的增长,这会导致 token 膨胀、模型性能下降以及严重的上下文长度限制错误——即使大多数工具与当前轮次无关。
而且没有标准的控制平面。如果你想阻止危险操作、强制执行参数结构策略,或者限制 LLM 首先被允许连接哪些服务器,你必须将其单独内置到每个客户端或每个服务器中。
一种诱惑是将这些决定推给 LLM 本身——但 LLM 看到的任何内容既会在每一轮通过网络传输,又容易受到其读取的任何文档、工具结果或网页的提示注入攻击。机密信息必须远离模型的上下文,安全绝不能交由与任何类型外部系统通信的 LLM 处理。执行必须存在于模型和外部世界之间的某个确定性位置。
extensible-mcp 位于 LLM 和你的 MCP 服务器之间,并解决了这三个问题:
1. **动态服务器加载** — 在启动时从配置连接 MCP 服务器,或在运行时通过 URL 连接。LLM 可以按需从网络中引入全新的服务器及其功能,而无需重启客户端。
2. **基于 RAG 的工具搜索和检索** — 工具定义被嵌入到向量索引中。LLM 使用 `search_tools(query)` 进行语义搜索,并仅提取所需的匹配项,而不是让每个工具定义占用每个提示词的空间。
3. **可插拔的过滤器管道** — 每个操作(搜索、调用、服务器加载)都通过一个过滤器链。该代理强制执行一项结构性保证:LLM 只能调用其先前通过 `search_tools` 展示过的工具。除此之外,过滤逻辑由你决定:随附的参考过滤器涵盖访问控制、Rego 策略评估和服务器加载白名单;你可以引入用于参数验证、审计日志、签名声明验证或任何其他用途的过滤器。
```
LLM <--> extensible-mcp <--> MCP Server(s)
|
+-- search_tools(query) → vector search over indexed tools
+-- call_tool(name, args) → proxied to the right server
+-- load_mcp_server(name, url) → connect a new server at runtime
```
## 工作原理
该代理向 LLM 暴露了三个元工具:
- **`search_tools(query)`** — 用自然语言描述你想做什么。该代理使用 [all-MiniLM-L6-v2](https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2) 嵌入查询,对工具索引运行余弦相似度,并返回匹配的定义。
- **`call_tool(tool_name, arguments)`** — 通过工具的限定名称(例如 `github__create_issue`)调用工具。代理将调用路由到正确的下游服务器。
- **`load_mcp_server(server_name, url)`** — 在运行时连接到新的远程 MCP 服务器。其工具会被立即索引,并可供搜索和调用。
检索是由模型驱动的:LLM 决定何时搜索并构建自己的查询,因此在不需要工具的轮次中不会进行浪费的检索。
## 状态
代理的 v1 版本已经可以工作:动态服务器加载、基于 RAG 的工具检索、可扩展的过滤器管道和凭证处理目前均已提供。该管道强制执行一项结构性保证——LLM 只能调用其通过 `search_tools` 发现的工具——并随附了用于访问控制、Rego 策略评估和服务器加载白名单的参考过滤器,你可以直接使用、配置或用自己的替换。104 项测试通过;示例配置可针对官方 GitHub MCP 服务器运行。
该管道与策略引擎无关:Rego 目前已作为参考挂接到调用过滤器中,但该架构并不偏向任何单一引擎——你可以接入 OPA、Cedar、自定义 Python,或任何适合你技术栈的引擎。活跃的研究方向:
- 调用时的 **签名声明验证** — 推送批准、签名文档、可验证凭证。请参阅威胁模型部分以了解论证。
- **原生的 Policy-as-Type 集成** — 链接到 [Policy as Code, Policy as Type (Fuchs, 2025)](https://arxiv.org/abs/2506.01446) 中的框架,该框架将策略视为依赖类型。策略的属性可以被数学证明,而不仅仅是测试。
这两个方向都在不改变架构的前提下扩展现有的过滤器管道。
## 威胁模型
LLM 不能被信任来管理其自身的安全。它们容易受到其摄入的任何材料的提示注入攻击,可能以不明显的方式受其训练集中材料的影响(包括将数据视为指令),它们会产生幻觉,可能会忘记指令,并且传递给它们的任何信息都必须被视为已泄露。因此,任何强制执行规则的严肃尝试都必须存在于 LLM 之外,存在于不受所有这些弱点影响的代码中。这就是我们的前提。
该管道允许在 LLM 和外部世界之间的所有接触点进行控制:
- 在服务器加载时,我们可以过滤并阻止代理加载不受信任的服务器。不仅是工具,服务器和工具的描述也可能包含提示注入攻击。
- 在搜索时,我们同样可以隐藏危险或不受信任的工具。在当前版本中,我们包含了一个示例过滤器来隐藏任何包含“delete”的工具;这样的工具不仅不能被调用,甚至不能被找到。
- 在调用时,进一步的策略可以阻止对允许工具的非法使用。在示例代码中,我们阻止了关闭 issue,但允许同一工具的其他用途,例如更新 issue。
- LLM 不能调用它在搜索期间未找到的任何工具。这确保了 LLM 仅调用受保护集合中的工具,并且不易受试图在受保护范围之外进行调用的攻击。
- 我们不会将机密(特别是安全 token)传递给 LLM。要在 HTTP Authorization 头中使用的 token 保存在一个单独的文件中。LLM 可以提示用户在 token 似乎过期时更新它,但它永远不会看到 token 本身。
当然,我们只能在 LLM 自身的上下文中应用这些保护。我们无法防范:
- 用户配置中的安全漏洞,
- 下游服务器的行为(尽管限制为受信任的服务器可以缓解此问题),
- 信任未经证实的 LLM 声明(例如用户是否同意某项操作)的策略,
- 否则无效的策略(例如,我们简单的 Rego 脚本禁止了一项操作,但允许所有其他操作)。
使用必需的参数值作为扩展策略的方法很诱人,例如在删除继续之前要求 `confirmation: 'CONFIRM_DELETE'`。我们考虑了这一点并放弃了它:一个可能被提示注入去删除文件的 LLM,同样可能被提示注入去提供确认字符串。用户的同意是未经证实的。这种机制可以防止意外,但不能防止恶意攻击。我们将使用签名声明(其有效性取决于 LLM 无法影响的通道的证据)来解决这种模式。
随着代理与其他代理进行通信,这一点变得尤为突出。AP2 所依赖的 A2A 让接收代理通过 LLM 处理每条消息,使得每个交易对手消息都成为潜在的提示注入向量。LLM 对其谈判对手已同意内容的判断在结构上是不安全的;解决单代理授权问题的相同签名证据架构在多代理设置中更是必不可少。
通过增加对签名声明作为参数的支持,我们可以确保值来自有效来源(例如用户),并且不能被 LLM 伪造。这方面的示例包括 Duo 或 CIBA 推送批准、W3C 可验证凭证(用于 Google 的 AP2 及其扩展通用商务协议 Universal Commerce Protocol),或 DocuSign 级别的信封。
通过添加签名声明,我们可以分三部分注入这种级别的安全性:
- 首先,在将工具定义交给 LLM 之前,预过滤器会修改参数 schema,以指定哪些必须被签名。
- 这些要求迫使 LLM 从用户或其他方检索这些参数的有效声明。签名要求阻止了 LLM 进行欺骗。
- 最后,在工具调用时,策略在批准调用时验证签名的参数。
这解决了未经证实的声明问题,也可用于加强对 MCP 服务器被允许的保证。正如 Google 的 Universal Commerce Protocol 所示,经过验证的声明现在成为代理式商务的关键,但这种要求也将适用于许多非商业操作,例如删除文件。
我们目前附带了 Rego 作为挂接到调用过滤器的参考策略引擎,但管道并不依赖于它——任何策略引擎都可以通过自定义 `CallFilter` 插入。Rego 的优势在于广泛的 ABAC 表达能力;其弱点是对类型检查策略正确性的支持极少(输入结构可以用 JSON Schema 检查,但策略逻辑本身未经验证)。我们计划链接到 [Policy as Code, Policy as Type (Fuchs, 2025)](https://arxiv.org/abs/2506.01446) 中的框架,该框架将策略视为依赖类型,并允许对策略的属性进行数学证明,而不仅仅是测试。
## 设置
需要 Python 3.11+。
```
# Clone 和 install
git clone https://github.com/mattdfuchs/extensible-mcp.git
cd extensible-mcp
uv sync
# 创建 config 文件
cp config.example.json config.json
# 使用你的 MCP servers 编辑 config.json
```
`config.example.json` 刻意设计为一个极简的入门配置——请参阅下方的“配置”部分以获取完整的选项集(URL 服务器、`rego_policy`、`load_control` 等)。
如果你的配置引用了 `$VAR_NAME` 样式的值(例如 stdio 服务器的 `env` 块中的 `"GITHUB_PERSONAL_ACCESS_TOKEN": "$GITHUB_PERSONAL_ACCESS_TOKEN"`),请在加载的配置所在的相同目录下放置一个 `.env` 文件,或在 shell 中导出这些变量——该代理会先解析 dotenv,然后解析 `os.environ`。`.env` 的查找是按配置目录进行的,因此仓库根目录下的 `.env` 不会应用于从其他位置加载的配置。
## 配置
配置文件使用与 Claude Desktop 相同的 `mcpServers` 格式,外加一个可选的 `filters` 部分。服务器可以是本地的(通过 `command` 的 stdio)或远程的(通过 `url` 的 Streamable HTTP):
```
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
},
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": ""
}
},
"remote-tools": {
"url": "https://example.com/mcp"
}
},
"filters": {
"similarity_threshold": 0.3,
"access_control": {
"deny": ["github__delete_repo"],
"deny_patterns": ["*__drop_*", "*__delete_*"],
"allow_servers": ["filesystem", "github"]
},
"load_control": {
"deny_url_patterns": ["http://*"],
"allow_url_patterns": ["https://github.com/*", "https://internal.corp/*"]
}
}
}
```
### 身份验证
许多 MCP 服务器需要凭证——OAuth Bearer token、PAT、API 密钥。extensible-mcp 支持两种路径,具体取决于访问下游服务器的方式:
**Stdio 服务器**(通过 `command` 作为子进程启动)— 通过 `mcpServers` 中的 `env` 块传递凭证,与任何 MCP 服务器的方式相同。[`examples/`](examples/) 中的 GitHub 示例使用了这种模式,即从 `.env` 文件或代理的环境中解析 `$GITHUB_PERSONAL_ACCESS_TOKEN`。
**URL 服务器**(通过 `url` 的 Streamable HTTP)— 在你的配置旁边放置一个 `tokens` 文件:
```
# tokens — 默认被 gitignore
notion=secret_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
internal-api=eyJhbGciOiJIUzI1NiIs...
```
格式:每行一个 `server_name=value` 对,`#` 表示注释,值周围的外部引号会被去除。如果加载的配置同目录中存在 `tokens` 文件,代理会自动获取它。
**将 tokens 文件移到项目之外。** 如果你的设置包含一个文件系统 MCP 服务器(或任何其他工具),它可以读取项目目录内的路径,则默认的 `tokens` 位置可能会被代理访问到。为了使凭证无法被触及,请将 `EXTENSIBLE_MCP_TOKENS_FILE` 设置为代理无法看到的路径——例如 `~/.secrets/extensible-mcp-tokens`。该变量可以在代理的环境中设置,也可以在配置旁边的 `.env` 文件中设置;相对路径是相对于配置目录解析的,并且 `~` 会被展开。如果设置了该变量但文件不存在,代理将拒绝启动。如果没有设置该变量,行为保持不变:代理在配置旁边查找 `tokens`,如果不存在则在没有它的情况下运行。解析的路径会在启动时记录,以便你确认正在使用哪个文件。
Tokens 作为 `Authorization: Bearer ` 头发送。该文件在每次连接时都会重新读取,因此你可以在不重启代理的情况下轮换凭证——覆盖该行,保存,下一次请求就会获取新值。
extensible-mcp 本身 **不** 运行 OAuth 流程。如果服务器使用 OAuth,请在外部(CLI、浏览器流程、无头服务账户认证,或任何你有的方式)生成访问 token 并将其放入 `tokens` 文件中。刷新是你的责任。
**Token 过期。** 如果下游调用返回 401/403(或包含“unauthorized”/“forbidden”的错误消息),代理会将其转换为给 LLM 的明确错误,指明服务器名称并报告 token 未更改的时长。代理的系统指令告诉 LLM 要求用户更新 `tokens` 文件中的 token 然后重试——绝不在对话中请求 token。按照设计,token 会被排除在聊天记录之外。
### 过滤器管道
三个独立的管道——搜索、调用和服务器加载——每个都在操作运行之前通过一个有序的过滤器链传递请求。代理强制执行一项结构性保证:**LLM 只能调用其先前通过 `search_tools` 展示过的工具。** 该门控内置在调用管道中,无法绕过。
除了发现门控之外,过滤逻辑由你定义。下面描述的过滤器作为参考实现随附,并通过 JSON 配置进行配置;对于超出这些范围的需求,请编写你自己的——请参阅[编写自定义过滤器](#writing-a-custom-filter)。
**搜索过滤器** — 在 `search_tools` 结果返回给 LLM 应用于其上。
| 字段 | 描述 |
|---|---|
| `similarity_threshold` | 最小余弦相似度得分(默认:`0.3`) |
| `access_control.deny` | 要隐藏的确切限定工具名称(例如 `github__delete_repo`) |
| `access_control.deny_patterns` | 要隐藏的 Glob 匹配模式(例如 `*__delete_*`) |
| `access_control.allow_servers` | 如果非空,则结果中只显示来自这些服务器的工具 |
**调用过滤器** — 在 `call_tool` 调用被代理发送到下游之前应用于其上。
| 字段 | 描述 |
|---|---|
| `access_control.*` | 与搜索相同的拒绝/允许规则——即使 LLM 知道工具名称也会阻止调用 |
**Rego 策略** — 对于细粒度的调用时策略评估,你可以指向一个 `.rego` 文件:
```
{
"filters": {
"rego_policy": "policies/deny_dangerous.rego"
}
}
```
该策略在每次 `call_tool` 调用时接收以下输入:
```
{
"tool_name": "github__delete_repo",
"arguments": {"repo": "my-org/my-repo"},
"server_name": "github"
}
```
该策略必须定义 `allow`(布尔值)。可选择定义 `deny_reason`(字符串)以提供自定义错误消息。请参阅 [`examples/deny_dangerous.rego`](examples/deny_dangerous.rego) 获取工作示例。配置中的相对路径是相对于配置文件所在目录解析的。
Rego 策略评估使用 [`regopy`](https://pypi.org/project/regopy/),它会随 `uv sync` 默认安装——无需额外步骤。
**服务器加载过滤器** — 在建立任何连接之前应用于 `load_mcp_server` 请求。
| 字段 | 描述 |
|---|---|
| `load_control.deny_names` | 要阻止的确切服务器名称 |
| `load_control.deny_name_patterns` | 服务器名称上的 Glob 匹配模式(例如 `evil_*`) |
| `load_control.deny_url_patterns` | URL 上的 Glob 匹配模式(例如 `http://*` 以强制使用 HTTPS) |
| `load_control.allow_url_patterns` | 如果非空,则只允许匹配至少一个模式的 URL(白名单) |
如果没有 `load_control`,LLM 可能会被提示注入去连接恶意服务器。使用 `allow_url_patterns` 来白名单受信任的域,并使用 `deny_url_patterns` 来阻止不安全的协议。
### 编写自定义过滤器
上面描述的参考过滤器是起点,而不是管道功能的极限。过滤器是普通的 Python 对象,实现了以下三种协议之一:
- **`ToolFilter`** — `filter(results: list[SearchResult], query: str) -> list[SearchResult]`。应用于 `search_tools` 结果。
- **`CallFilter`** — `async check(request: CallRequest) -> CallFilterResult`。应用于 `call_tool` 调用。
- **`ServerLoadFilter`** — `async check(request: ServerLoadRequest) -> ServerLoadResult`。应用于 `load_mcp_server` 请求。
一个审计每次调用的自定义调用过滤器:
```
from extensible_mcp import CallFilter, CallRequest, CallFilterResult
class AuditLogFilter:
async def check(self, request: CallRequest) -> CallFilterResult:
log_to_my_system(request.tool_name, request.arguments, request.server_name)
return CallFilterResult(
allowed=True,
tool_name=request.tool_name,
arguments=request.arguments,
)
```
通过编写你自己的入口点将其接入——`create_server` 接受 `extra_search_filters`、`extra_call_filters` 和 `extra_load_filters`:
```
from extensible_mcp.config import find_config_path, load_config
from extensible_mcp.server import create_server
from myorg.filters import AuditLogFilter
config = load_config(find_config_path(None))
server = create_server(config, extra_call_filters=[AuditLogFilter()])
server.run()
```
自定义过滤器在每个管道中的内置参考过滤器之后运行。要拒绝调用,请返回 `CallFilterResult(allowed=False, reason="...", tool_name=..., arguments=...)`。已发现工具的保证在任何自定义调用过滤器之前运行,并且无论你的过滤器集如何,都会被始终强制执行。
### 配置解析顺序
1. `--config` CLI 标志
2. `EXTENSIBLE_MCP_CONFIG` 环境变量
3. `~/Library/Application Support/extensible-mcp/config.json` (macOS)
4. `~/.config/extensible-mcp/config.json`
5. `./config.json`
## 使用
```
# 运行 server
uv run extensible-mcp
# 或者使用显式 config 路径
uv run extensible-mcp --config /path/to/config.json
```
该代理作为基于 stdio 的 MCP 服务器运行。从任何 MCP 客户端连接到它,就像连接到任何其他 MCP 服务器一样。
## 示例
[`examples/`](examples/) 目录提供了开箱即用的配置,用于通过 extensible-mcp 代理 GitHub 的官方 MCP 服务器,在过滤器管道中包含两层安全性:一个阻止所有删除操作的 glob 拒绝模式(`*__delete_*`),以及一个基于参数形状阻止关闭 issue 的 Rego 策略。
- **Claude Desktop** — [`examples/claude-desktop-config.json`](examples/claude-desktop-config.json)
- **OpenClaw** — [`examples/openclaw-config.json`](examples/openclaw-config.json)
请参阅 [`examples/README.md`](examples/README.md) 获取设置说明和建议尝试的提示词。
## 开发
```
# Install 包含 dev 依赖
uv sync --group dev
# 运行 tests
uv run pytest
# 运行单个 test 文件
uv run pytest tests/test_filters.py -v
```
## 许可证
Apache License 2.0 — 请参阅 [LICENSE](LICENSE)。
标签:AI安全网关, API网关, CISA项目, DLL 劫持, LLM网关, MCP代理, Streamlit, 上下文管理, 令牌优化, 动态加载, 可插拔过滤器, 大语言模型, 安全策略, 工具调用, 开源, 按需检索, 提示词设计, 模型上下文协议, 确定性执行, 网络中间件, 访问控制, 逆向工具, 防范提示注入