内容
# subwire
一个小巧、可配置的 **HTTP/REST MCP 服务器**,用于与您的整个家庭实验室(homelab)以及更多设备进行通信。检查 Prometheus 指标,调用容器的 API,与本地 LLM 聊天,或调用身份验证的外部服务并将结果输送到其他地方。只有一个通用的 `http_request` 工具,按主机配置文件命名,每个主机有自己的 TLS 控制,并且具有为可达内部主机而设计的网络安全策略。
> 状态:v1.0。MIT 许可。欢迎提出问题和拉取请求。
## 为什么还需要另一个 HTTP MCP 服务器?
现有的服务器接近但在三个方面存在不足,无法满足家庭实验室的需求:
- **TLS 配置全有或全无。** 内部设备使用自签名证书或内部 CA;大多数服务器要么严格验证(并失败),要么暴露一个全局的“禁用 SSL”开关,这也会削弱您的 **公共** 调用。subwire 按范围解析 `verify` —— 将全局默认值指向您的内部 CA,以便整个 `*.home.lan` 设备都经过验证 **开启**,为一个无法重新颁发的设备跳过它,为公共主机保留严格验证 —— 所有这些都相互独立。参见 [内部 CA / 通配符证书](#internal-ca--wildcard-certs)。
- **单一基准 URL 锁定。** 家庭实验室会访问许多主机。subwire 允许您定义任意多个命名目标,或者只需传递一个绝对 URL 而无需目标。
- **URL 作为盲字符串 → SSRF。** MCP 生态系统的安全审查表明,LLM 提供的 URL 可以通过提示注入引导(通过提示注入)到仅限内部的服务或云元数据端点。subwire 保留了刻意的内部访问,但 **默认阻止链接本地/元数据**,并提供允许/拒绝列表、只读模式和按目标的方法允许列表。
无需 shell-out 到 `curl`;请求引擎是 `httpx`。
## 安装
从源码(推荐在初期阶段):
```bash
git clone https://github.com/Corundex/subwire && cd subwire
make install # = pip install .
```
或者,一旦发布到 PyPI:
```bash
pip install subwire
```
需要 Python 3.11+(用于本地安装)或仅 Docker(用于服务器设置 — 主机上无需安装任何内容)。依赖项:`httpx`、`mcp`、`pyyaml`。运行 `make help` 以查看所有快捷方式。
## 快速入门
首先,创建您的配置文件(此操作会复制示例文件;请编辑它以列出您的主机):
```bash
make config # 从 config.example.yaml 创建 config.yaml
# 或:cp config.example.yaml config.yaml
```
然后选择您想要运行它的方式:
### A. 在您从 Claude Desktop 使用的机器上(最简单)
```bash
make install # 安装 `subwire` 命令
make run # 在标准输入/输出上运行,使用 config.yaml
```
然后将其添加到您的客户端 — Claude Code 或 Claude Desktop(参见 [下面](#connecting-it))。
### B. 作为家庭实验室 / 远程主机上的服务器(Docker)
配置 **被编译到镜像中**,因此无需挂载任何内容,它在本地或远程表现相同:
```bash
make up # 构建镜像(内部包含您的 config.yaml)并启动它
# 等效于:docker compose up -d --build
```
MCP 端点现在是 `http://<host>:8080/mcp`。将您的客户端指向它(参见 [下面](#connecting-it))— `claude mcp add` 用于 Claude Code,或 `mcp-remote` 用于 Claude Desktop。
> 没有配置?`subwire` 仍然运行 — 它只是没有命名目标,您可以直接传递完整的 URL。默认策略:访问局域网 + 公共,阻止云元数据。
## 连接它
### Claude Code(CLI / VS Code 扩展)
一条命令 — `claude mcp add` 为您写入条目:
```bash
# 本地标准输入/输出(在同一台机器上安装了 subwire)
claude mcp add --scope user subwire subwire
# 远程可流式 HTTP(在您的家庭实验室中,在 Docker 上运行 subwire)
claude mcp add --transport http --scope user subwire http://subwire.home.lan/mcp
```
`--scope user` 使服务器在所有项目中可用。 使用 `claude mcp list` 或 `/mcp` 斜杠命令验证。 在第一次添加后重启 Claude Code(新建聊天或重新打开扩展)— MCP 服务器在会话启动时加载。
### Claude Desktop
**本地(标准输入/输出):**
```json
{
"mcpServers": {
"subwire": {
"command": "subwire",
"args": ["--config", "/path/to/config.yaml"]
}
}
}
```
**远程(您的局域网上的 HTTP 服务器,通过 `mcp-remote`):**
```json
{
"mcpServers": {
"subwire": {
"command": "npx",
"args": ["-y", "mcp-remote", "http://subwire.home.lan/mcp",
"--transport", "http-only", "--allow-http"]
}
}
}
```
## 工具
### `http_request`
发出请求。传递绝对 `url` 或 `target` + 相对 `url`。
| 参数 | 类型 | 备注 |
| ----------- | -------------- | -------------------------------------------------- |
| `url` | 字符串 | 绝对 URL,或相对于目标的 `base_url` |
| `method` | 字符串 | 默认 `GET` |
| `headers` | 对象 | 合并到目标默认值上 |
| `params` | 对象 | 查询字符串 |
| `json_body` | 任意 | JSON 正文(设置 Content-Type) |
| `body` | 字符串 | 原始正文(非 JSON) |
| `target` | 字符串 | 要使用的命名配置文件 |
| `timeout` | 数字 | 秒数 |
| `verify` | 布尔值 | 字符串 | TLS 覆盖:`true`、`false` 或 CA 包路径 |
| `max_bytes` | 整数 | 响应正文上限 |
返回 JSON: `{request, status, ok, elapsed_ms, headers, body, truncated}`。
JSON 响应正文自动解析为结构化数据。
### `list_targets`
列出已配置的目标(基准 URL、允许的方法、TLS 状态、身份验证类型 — 无秘密)以及活动安全策略。模型可以访问的良好初始调用。
## 配置
YAML。每个设置都按优先级解析 **显式参数 > 目标 > 默认值**。参见 [`config.example.yaml`](https://github.com/Corundex/subwire/blob/main/config.example.yaml) 获取注释副本。
```yaml
defaults:
timeout: 30
verify: /etc/subwire/home-ca.pem # 内部 CA 根:整个实验室的信任锚点
# (对公共网络使用 `true`,禁用使用 `false`)
allow_http: true # 允许纯 http://
read_only: false # true => 仅 GET/HEAD/OPTIONS
max_response_bytes: 100000
follow_redirects: true
security:
allow_private: true # RFC1918 + *.home.lan 等
allow_loopback: true
allow_metadata: false # 阻止 169.254.169.254(保持 false)
allow_hosts: [] # 额外显式允许通配符
deny_hosts: [] # 显式拒绝通配符(胜过所有)
targets:
prometheus:
base_url: http://prometheus.home.lan:9090
allowed_methods: [GET] # 监控:只读
llama:
base_url: http://llama.home.lan
dozzle:
base_url: https://dozzle.home.lan # CA 签名 => 继承默认值,验证
legacy-appliance:
base_url: https://nas.home.lan
verify: false # 例外:无法重新颁发的证书
smartoffs:
base_url: https://www.smartoffs.com
auth: { type: bearer, token_env: SMARTOFFS_TOKEN }
```
### 认证
凭证 **从不** 存储在文件中 — 认证块引用环境变量 **名称**,在请求时解析:
```yaml
auth: { type: basic, username_env: SVC_USER, password_env: SVC_PASS }
auth: { type: bearer, token_env: SVC_TOKEN }
auth: { type: apikey, header: X-API-Key, value_env: SVC_KEY }
```
需要一种内置不支持的方案?`auth.py` 是一个小而独立的模块,具有清晰的结构 — 添加一个案例即可。
### 内部 CA / 通配符证书
在整个家庭实验室中获取验证 TLS 的最干净方法是 **一个内部 CA**,位于 `defaults.verify` — 而不是一大堆每台主机的 `verify: false`。使用您的 CA 签署每个服务的证书,将全局默认值指向 CA 根,然后每个 `*.home.lan` 主机都经过验证 **开启**。仅为无法重新颁发证书的特殊设备覆盖每个目标。
```yaml
defaults:
verify: /etc/subwire/home-ca.pem # 整个实验室的信任锚点
```
当 `verify` 指向 CA 包时,subwire **合并** 它与标准公共信任存储(`certifi`)在请求时 — 因此相同的设置信任您的内部 CA **和** 每个公共根,`https://api.github.com` 在 `https://dozzle.home.lan` 旁边继续工作。您无需为外部主机覆盖 `verify`。
更喜欢单个自签名 **通配符** 证书而不是 CA?这也可行 — 自签名证书是其自己的信任锚点,因此将 `verify` 指向证书文件。两个注意事项:
- **它必须在 `subjectAltName` 中包含名称,而不是仅限 CN。** 现代 OpenSSL(以及 httpx)忽略 CN 进行主机名匹配 — 仅限 CN 或无 SAN 的证书都会失败验证,无论您信任什么。这是通常自签名证书“无效”的原因。
- **通配符匹配恰好一个标签。** `*.home.lan` 涵盖 `dozzle.home.lan` 但 **不** 涵盖顶点 `home.lan` 或 `a.b.home.lan`。为这些添加额外的 SAN 条目(或使用 CA 方法)。
使用正确的 SAN 生成通配符自签名证书:
```bash
openssl req -x509 -newkey rsa:2048 -nodes -days 3650 \
-keyout home-lan.key -out home-lan.pem \
-subj "/CN=*.home.lan" \
-addext "subjectAltName=DNS:*.home.lan,DNS:home.lan"
```
如果 `verify` 指向不存在的 CA/证书路径,subwire 会用一条明确的消息命名路径(而不是不透明的 SSL 错误)使请求失败 — 当 Docker 卷挂载错误时非常方便。
> 注意:subwire 故意将 `verify` 保留在两个范围 — 全局默认值和每目标范围内。全局默认值 **是** 通配符;每目标范围处理例外。故意没有“信任 CA X 以匹配模式 Y”的映射,因为它为两个范围已经涵盖的功能添加了配置表面。
### 环境变量覆盖
`SUBWIRE_CONFIG`、`SUBWIRE_HOST`、`SUBWIRE_PORT`、`SUBWIRE_READ_ONLY`、`SUBWIRE_ALLOW_HTTP`、`SUBWIRE_VERIFY`(`true`/`false`/CA 路径)、`SUBWIRE_MAX_RESPONSE_BYTES`、`SUBWIRE_ALLOWED_HOSTS`(逗号分隔,用于 `--http` 传输允许列表)、`SUBWIRE_DISABLE_DNS_REBINDING_PROTECTION`。
## Docker
有两种方法可以在 Docker 中运行 subwire,取决于您是谁:
### 使用您自己的数据运行固定版本(推荐给用户)
使用 [`deploy/`](https://github.com/Corundex/subwire/tree/main/deploy) 模板。它将 **您的** 配置和证书保留在 **您** 的文件夹中,从 GitHub 在构建时拉取固定的 subwire 版本,并将它们一起编译 — 因此更新 subwire 永远不会触及您的数据,也不需要卷挂载。
```bash
cp -r deploy ~/subwire-deploy # 从代码仓库复制模板
cd ~/subwire-deploy
nano config.yaml # 添加您的目标
docker compose up -d --build # 拉取 subwire + 编译您的数据
```
稍后更新是一个版本更新 `deploy/docker-compose.yaml`,然后重建 — 参见 [`deploy/README.md`](https://github.com/Corundex/subwire/blob/main/deploy/README.md)。这是家庭实验室最干净的路径,尤其适用于将数据发送到远程主机(在服务器上克隆您的部署仓库并运行 `up`)。
### 从本地克隆构建(贡献者/代码黑客)
如果您克隆了此仓库来修改 subwire 本身,则根目录 `Dockerfile` 直接从本地源代码构建,并将 `config.yaml` 和 `certs/` 编译到其中:
```bash
make config # 创建 config.yaml(一次),然后编辑它
make up # 从本地源代码构建,启动分离
```
在两种情况下,目标引用的机密(例如 `token_env: SMARTOFFS_TOKEN`)都作为环境变量传递 — 永远不会编译到镜像中。使用内部 CA?将 `.pem` 放入相关的 `certs/` 文件夹中,然后在构建之前引用它为 `/etc/subwire/certs/home-ca.pem`。
## 安全模型(请阅读本节)
subwire 被设计为有意访问内部主机,因此不能简单地阻止私有 IP。相反,按顺序评估:
1. `deny_hosts` 通配符匹配 → 拒绝。
2. `http://` 且 `allow_http: false` → 拒绝。
3. 不允许的方法(只读模式和/或目标允许列表)→ 拒绝。
4. 地址类:** 默认阻止链接本地/云元数据 **(`allow_metadata: false`);如果启用,则允许环回/私有;允许公共。
TLS 验证按目标进行,永远不会在全局范围内被静默禁用。机密存储在环境变量中,配置的身份验证头在请求中被遮蔽。
**已知限制:** 出站分类器中没有 DNS 重新绑定防御(主机按字面/模式匹配,而不是在请求时重新解析,因此攻击者可以控制允许列表中的名称,并将其重新绑定到被拒绝的地址);允许/拒绝是通配符,而不是正则表达式。
## 故障排除
**subwire 在客户端不可见。** MCP 服务器在启动时加载。对于 **Claude Code**:在 `claude mcp add` 后打开新聊天或重启 VS Code 扩展;`claude mcp list` 或 `/mcp` 应显示它已连接。对于 **Claude Desktop**:完全退出并重新打开(不要只关闭窗口),然后进入 设置 → 连接器。对于远程/HTTP 设置,请确保容器已启动 (`make logs`) 并且 URL 以 `/mcp` 结尾。
**“未找到配置文件”**。您传递的 `--config` 路径不存在。运行 `make config` 以创建 `config.yaml`,或者只需运行 `subwire` 而不带任何标志 — 它会在第一次运行时自动从捆绑的示例中创建 `./config.yaml`,因此服务器会启动并带有几个演示目标,您可以立即尝试。
**请求因“TLS 验证设置为 CA 包 ... 但该文件不存在”而失败。** 您的 `verify:` 路径不正确,或者(在 Docker 中)证书未被编译。 将文件放入 [`certs/`](https://github.com/Corundex/subwire/tree/main/certs),将其引用为 `/etc/subwire/certs/<file>`,并使用 `make up` 重建。
**请求因内部主机的证书错误而失败。** 主机的证书很可能缺少 `subjectAltName`(仅限 CN)。重新颁发证书并带有正确的 SAN — 参见 [内部 CA / 通配符证书](#internal-ca--wildcard-certs)。
**客户端收到“421 错误请求”/ 服务器日志“无效主机头”。** MCP 流式 HTTP 传输拒绝任何不在其允许列表中的 `Host` 头(默认仅限 localhost)。在 `defaults.allowed_hosts` 下列出客户端使用的主机名(例如 `subwire.home.lan`、`master.home.lan:8081`;支持 `host:*` 端口通配符),或设置 `defaults.disable_dns_rebinding_protection: true` 以适用于受信任的局域网。相同的控件作为 `--allowed-host` / `--disable-dns-rebinding-protection` 在 CLI 上和 `SUBWIRE_ALLOWED_HOSTS` / `SUBWIRE_DISABLE_DNS_REBINDING_PROTECTION` 在环境中。
**“被安全策略阻止”**。工作如预期 — 请求击中了某个门槛。消息指示哪一个(拒绝列表、只读模式、目标的方法允许列表或元数据阻止)。在 `config.yaml` 中调整相关设置(如果您确实想访问该主机)。
**纯 `http://` 被拒绝。** 您(或环境变量覆盖)设置了 `allow_http: false`。将其设回 `true` 以适用于家庭实验室。
## 许可证
MIT — 参见 [LICENSE](https://github.com/Corundex/subwire/blob/main/LICENSE)。