wtmcp
带有语言无关插件系统的MCP服务器。插件很简单 与以下对象通信的可执行文件(Python、bash或任何语言) 核心位于stdin/stdout上的JSON行之上。核心处理身份验证、HTTP 代理、缓存和输出编码,使插件保持最小。
建筑
┌─────────────────────────────────────────────────┐
│ wtmcp (Go) │
│ │
│ MCP Server ─── Plugin Manager ─── HTTP Proxy │
│ (mcp-go) Discovery Auth inject │
│ Lifecycle SSRF protect │
│ Dispatch TLS verify │
│ Rate limit │
│ │
│ Audit Log ─── Cache Store ─── Auth Providers │
│ (JSON) (memory/fs) Bearer, Basic, │
│ Kerberos, OAuth2 │
│ Sandbox (optional) │
│ Landlock + cgroups + netns │
└────────┬──────────────────────────┬─────────────┘
│ stdio (MCP/JSON-RPC) │ stdin/stdout (JSON-lines)
┌────┴────┐ ┌─────┴──────────┐
│ AI │ │ Plugins │
│ Client │ │ Zero deps │
└─────────┘ │ No HTTP libs │
│ No auth code │
└────────────────┘特性
- 插件协议:任何语言的stdin/stdout上的JSON行
- 认证:承载、基本、Kerberos/SPNEGO、带令牌刷新的OAuth2,
从可用凭据中自动检测
- HTTP代理:身份注入、域验证、TLS强制、,
二进制响应编码,多部分上传支持
- 缓存:具有命名空间隔离和TTL的内存存储
- 输出:TOON编码可节省约40%的代币(可选)
- 插件设置:CLI工具的清单声明向导元数据
- 渐进式发现:工具默认为延迟;仅初级
工具被加载到模型上下文中。延期工具包括 可通过以下方式发现 tool_search 并通过MCP直接调用
- 加密凭据:Ansible Vault加密的env.d文件,
启动时自动检测并透明解密
安全
wtmcp在核心级别强制执行安全性,因此插件不需要 实现自己的身份验证、输入验证或网络限制。
HTTP代理和SSRF防护
所有插件HTTP流量都通过核心代理。没有插件 直接网络连接。
- SSRF安全拨号器 在连接时验证已解析的IP--
阻止私有、环回、链路本地、多播和IPv6映射 IPv4地址
- 域名分配 每个插件——只有声明的域
可到达的
- 跨域重定向时的凭据剥离 --授权,
Cookie和API密钥头在重定向到 不同的主机
- 危险顶盖已拆除 来自插件精心编制的请求
(主机、代理授权、X-Forwarded-For等)
- HTTPS强制 用于经过身份验证的请求;mTLS支持
使用证书链验证
- 用户信息URL拒绝 —
user:pass@hostURL被阻止
沙盒(可选)
建立 -tags sandbox 通过以下方式启用操作系统级插件隔离 阿拉伯:
- Landlock LSM 文件系统限制——插件只能
读/写声明路径
- cgroup v2 资源限制——内存、CPU、PID、文件大小
(每个插件可配置)
- 网络命名空间隔离 --插件不能直接
连接;通过核心代理的所有流量路由
- OOM检测 进程退出后的资源使用情况报告
# Build with sandbox support (requires libarapuca)
make build-sandbox
# Default build works without libarapuca
make build当配置中启用了沙盒但二进制文件缺少标签时, 服务器以明显错误拒绝启动。当配置使用 默认情况下(隐式启用沙盒),服务器以 警告。
速率限制
令牌桶速率限制,可配置每个插件、每个域、, 以及全球限制。默认值:每个插件120请求/分钟,600请求/分钟 全球的。
http:
rate_limit:
default: "120/m"
global: "600/m"
per_plugin:
jira: "60/m"
per_domain:
api.github.com: "30/m"HTTP检索
瞬时上游采用指数回退的自动重试 失败。只有幂等方法(GET、HEAD、OPTIONS、PUT、, DELETE)被重试——POST/PATCH永远不会被重试。尊重 Retry-After 头部(夹紧至30秒)。上下文感知:工具 呼叫超时自然会限制总重试持续时间。
http:
retries:
max: 3 # retries (not counting initial)
backoff: exponential # 1s, 2s, 4s... capped at 30s
retry_on: [500, 502, 503, 504] # status codes to retry缓存限制
LRU驱逐时的每个插件入口限制。参赛作品超过 max_entry_size 被拒绝。后台清理删除已过期 以配置的间隔输入。
cache:
max_entries_per_plugin: 10000 # LRU eviction when exceeded
max_entry_size: 1048576 # 1MB max per entry
cleanup_interval: 60s # expired entry sweep审计日志
具有UUID v7相关ID的结构化JSON审核日志:
- 工具调用事件:插件、工具、参数(已擦除)、持续时间
- 诱导事件:插件、工具、操作(接受/拒绝/取消/
错误/不支持)
- HTTP代理事件:方法、主机、路径、状态、响应大小
- 凭证清理:字段名(密码、令牌、机密),
JWT检测(eyJ 前缀),高熵字符串检测
- 可配置输出:文件(0600权限)和/或stdout
audit:
log_file: logs/audit.log
stdout: false
scrub_fields: [password, token, secret, api_key, authorization]快速注射防御
- MCP激发 (默认启用)提示用户
在执行任何写入工具之前进行确认。确认书 消息显示了工具名称和已擦除的参数。客户 默认情况下,缺乏启发支持会被阻止 (elicitation_strict: true).完全禁用启发式 security.elicitation: false,或允许客户失误 没有启发式支持 security.elicitation_strict: false
- 输出框架 (默认启用)每个会话
加密随机数——检测插件输出中注入的标签 然后逃走了。禁用 security.tag_tool_output: false
- MCP受众注释 着手
[assistant]在所有工具上
结果(始终处于活动状态)
- JSON模式验证 在编译后的每个工具调用上
插件YAML
- 编写工具约定:默认包含插件
dry_run=true 在他们的模式中,需要明确的退出。 这是一个插件级别的约定,不是核心强制的
- 只读模式 在三个层面上执行:工具注册,
禁用存根和运行时拒绝
security:
elicitation: true # confirm before write tools (default: true)
elicitation_strict: true # block writes if client lacks elicitation (default: true)
tag_tool_output: true # nonce-based output tagging (default: true)凭证隔离
插件进程只接收它们需要的凭据。
- 范围env.d --每个插件只接收其凭据
组变量
- 过滤环境 --所有安全系统变量均已通过
插件(PATH、HOME、LANG、TZ、TMPDIR、XDG\_\*dirs等)
- 文件权限执行 --env.d文件和目录
需要0600/0700(SSH样式)
- Symlink拒绝 在凭据文件、CA证书和env.d上
条目
- 保险库密码清零 --从内存中清除解密密钥
使用后(尽最大努力;Go的GC可能会保留副本)
- 内存支持的安全文件 --已存储解密凭据
通过 memfd_create,切勿触摸磁盘
建立和运行
make build
# Run with a workdir (default: ~/.config/wtmcp)
./wtmcp --workdir ~/.config/wtmcp工作目录布局:
~/.config/wtmcp/
config.yaml Core config (optional)
.env Environment variables
env.d/*.env Additional env files
plugins/
jira/
plugin.yaml Plugin manifest
handler.py Plugin executable编写插件
插件是一个包含清单的目录(plugin.yaml)以及一个处理程序 可执行。核心发现插件,作为子进程启动处理程序 使用JSON行在stdin/stdout上处理和路由工具调用。
看 docs/plugin-guide.md 查看完整指南 有多种语言的例子。
最小示例(bash)
一个oneshot插件,每次工具调用运行一次处理程序:
plugin.yaml:
name: hello
version: "1.0.0"
description: "A greeting plugin"
execution: oneshot
handler: ./handler.sh
tools:
- name: hello_world
description: "Says hello to someone"
params:
name:
type: string
default: "World"
description: "Who to greet"
enabled: truehandler.sh:
#!/bin/bash
read -r INPUT
ID=$(echo "$INPUT" | jq -r '.id')
NAME=$(echo "$INPUT" | jq -r '.params.name // "World"')
echo "{}" | jq -c --arg id "$ID" --arg name "$NAME" \
'{id: $id, type: "tool_result", result: {message: ("Hello, " + $name + "!")}}'API插件示例(Python)
通过核心的HTTP代理调用API的持久插件。 处理程序保持运行并处理多个工具调用。认证 头文件是自动注入的——插件永远不会看到令牌。
plugin.yaml:
name: myapi
version: "1.0.0"
description: "Example API plugin"
execution: persistent
handler: ./handler.py
services:
auth:
type: bearer
token: "${MY_API_TOKEN}"
http:
base_url: "${MY_API_URL}"
tools:
- name: myapi_get_status
description: "Get API status"
params: {}
- name: myapi_search
description: "Search the API"
params:
query:
type: string
required: true
enabled: truehandler.py:
#!/usr/bin/env python3
import json, sys
def _send(msg):
print(json.dumps(msg, separators=(",", ":")), flush=True)
def _recv():
line = sys.stdin.readline()
if not line:
sys.exit(0)
return json.loads(line.strip())
def http(method, path, query=None):
msg = {"id": "1", "type": "http_request", "method": method, "path": path}
if query:
msg["query"] = query
_send(msg)
resp = _recv()
return resp.get("status", 0), resp.get("body", {})
def get_status(_params):
status, body = http("GET", "/status")
return body
def search(params):
status, body = http("GET", "/search", query={"q": params["query"]})
return body
TOOLS = {"myapi_get_status": get_status, "myapi_search": search}
while True:
msg = _recv()
if msg.get("type") == "init":
_send({"id": msg["id"], "type": "init_ok"})
elif msg.get("type") == "shutdown":
_send({"id": msg["id"], "type": "shutdown_ok"})
break
elif msg.get("type") == "tool_call":
fn = TOOLS.get(msg.get("tool"))
if fn:
result = fn(msg.get("params", {}))
_send({"id": msg["id"], "type": "tool_result", "result": result})
else:
_send({"id": msg["id"], "type": "tool_result",
"error": {"code": "unknown_tool", "message": msg.get("tool")}})关键概念
- Oneshot 每次工具调用都会生成插件。写起来最简单。
- 持久 插件启动一次,通过主循环处理许多调用。
- HTTP代理:插件发送
http_request核心信息
带有auth并返回的调用 http_response。不需要HTTP库。
- 缓存:插件发送
cache_get/cache_set信息。核心
管理存储和TTL。
- 身份验证变体:单个插件可以支持多种身份验证方法
(例如,Cloud Basic+服务器承载+Kerberos)具有自动检测功能。
插件管理
插件可以在运行时重新加载,而无需重新启动服务器。
来自AI助手:
plugin_reload(name="jira")
plugin_list()从终端 (控制目录):
touch ~/.config/wtmcp/control/commands/reload-jira
touch ~/.config/wtmcp/control/commands/reload-all结果显示在 ~/.config/wtmcp/control/results/。服务器写入 其PID为 ~/.config/wtmcp/control/mcp.pid 用于过程跟踪。
当工具或资源发生变化时,MCP客户端会自动收到通知。
OAuth插件管理
插件身份验证(特别是对于启用OAuth的插件)是通过 wtmcpctl 命令行实用程序。看 README-wtmcpctl.md 了解使用说明和设置。
加密凭据
env.d文件可以用加密 Ansible保险库 用于休息保护。服务器通过以下方式自动检测加密文件 魔术头,并在启动时透明地解密它们。插件 像往常一样接收明文凭据——不需要更改插件。
快速开始
# Create a vault password file (umask prevents brief permission race)
(umask 077 && openssl rand -base64 32 > ~/.vault-pass)
# Tell wtmcp where the password file is
# (add to ~/.config/wtmcp/config.yaml)
# secrets:
# vault_password_file: ~/.vault-pass
# Encrypt an env.d file
ansible-vault encrypt --vault-password-file ~/.vault-pass \
~/.config/wtmcp/env.d/jira.env
# Start the server — decrypts automatically
wtmcp加密文件可以安全地提交到git、共享或备份 起来。任何获得它们的人仍然需要保管库密码才能 解密。
密码来源
保管库密码按优先级顺序解析:
WTMCP_VAULT_PASSWORD环境变量(CI/CD便利性)WTMCP_VAULT_PASSWORD_FILE环境变量(文件路径)secrets.vault_password_file在config.yaml中(推荐)
对于生产和工作站,首选基于文件的密码。环境 var用于装载文件的CI/CD管道 不方便。
多密码支持(保险库ID)
Ansible Vault 1.2支持标记密码(Vault ID)。不同 env.d文件可以使用不同的密码:
# Encrypt with a vault ID label
ansible-vault encrypt --vault-id prod@~/.vault-pass-prod \
~/.config/wtmcp/env.d/jira.env在config.yaml中配置每个ID的密码文件:
secrets:
vault_password_file: ~/.vault-pass # default
vault_ids:
prod: ~/.vault-pass-prod
dev: ~/.vault-pass-dev还支持每ID环境变量: WTMCP_VAULT_PASSWORD_PROD, WTMCP_VAULT_PASSWORD_DEV.
如果找不到每个ID的密码,服务器将回退到 默认密码链自动。
诊断
wtmcp check报告保管库密码状态和每组加密详细信息 (仅显示加密组):
vault password: file (~/.vault-pass)
- jira (encrypted, vault 1.1, decryption ok)
- snyk (encrypted, vault 1.2 id=prod, decryption failed)迁移现有文件
- 创建vault密码文件(请参阅快速入门)
- 配置
secrets.vault_password_file在config.yaml中 - 加密一个env.d文件:
ansible-vault encrypt --vault-password-file ~/.vault-pass env.d/jira.env
- 验证:
wtmcp check应显示“解密正常” - 对剩余文件重复此操作
- 可选地将加密文件提交到git
单个env.d目录可以混合纯文本和加密文件。 增量迁移--一次一个文件。
如果之前以明文形式提交env.d文件,则加密 它们不会从git历史记录中删除明文。旋转 迁移后受影响的凭据,并考虑使用 git filter-repo 从历史记录中删除旧明文。
重新加载加密凭据
凭证更改将于生效 plugin_reload 没有服务器 重新启动。每次从密码源重新读取保管库密码 reload,因此密码轮换会自动进行。
安全须知
- Ansible Vault使用AES-256-CTR和PBKDF2-SHA256(10000
迭代)。使用强密码(20+个字符或 openssl rand -base64 32)以补偿低迭代次数 计数。
- 备份您的保管库密码文件。 失去它意味着永久
无法访问加密凭据。将副本存储在 单独的安全位置。
- Ansible Vault是对开发和
CI/CD。需要密钥轮换、审计的受监管环境 日志记录或FIPS验证的加密货币应使用HashiCorp Vault或 云KMS。
凭证文件加密
除了env.d文件外 credentials// 也可以进行vault加密。支持 文件夹:
client-credentials.json(OAuth2客户端凭据)- 传输层安全
client_cert和client_keyPEM文件
令牌文件(token-*.json)都是 不 加密——它们是 自动旋转、短暂且从客户端凭据派生。
加密的凭据文件被解密为内存支持的文件 描述符(Linux上的memfd,macOS上的未链接tmpfile)因此 解密的内容永远不会触及持久存储。插件 像往常一样接收相同的文件路径——不需要更改插件。
wtmcpctl保险库命令
加密和解密文件,无需 ansible-vault:
# Encrypt a file
wtmcpctl vault encrypt env.d/jira.env
# Encrypt with vault ID
wtmcpctl vault encrypt --vault-id prod env.d/jira.env
# Decrypt a file
wtmcpctl vault decrypt env.d/jira.env
# Verify decryption without writing
wtmcpctl vault decrypt --check env.d/jira.env
# View decrypted content without modifying the file
wtmcpctl vault view env.d/jira.env密码来源于 --vault-password-file, WTMCP_VAULT_PASSWORD env-var、config.yaml或交互式提示(带回声抑制)。
包含的插件
谷歌插件
谷歌插件使用OAuth2提供对谷歌工作区服务的访问 身份验证:
| 插件 | 描述 |
|---|---|
| 谷歌驱动器 | 文件元数据、搜索和导出 |
| 谷歌日历 | 日历事件和管理 |
| 谷歌gmail | 电子邮件阅读和发送 |
所有Google插件都需要OAuth2身份验证。看 README-wtmcpctl.md 有关设置说明。
Jira插件
附带的Jira插件涵盖了读取、写入、冲刺和导出 操作:
| 类别 | 示例 |
|---|---|
| 阅读 | jira_search, jira_get_myself, jira_get_transitions |
| 写 | jira_create_issue, jira_add_comment, jira_assign_issue |
| Sprint | jira_list_available_sprints, jira_get_sprint_issues |
| 出口 | jira_export_sprint_data, jira_download_attachment |
所有写入工具默认为 dry_run=true云感知(ADF格式, accountId分配)。身份验证变体:Cloud Basic、服务器承载、, 服务器Kerberos。
渐进式工具发现
默认情况下(tools.discovery: full),所有工具都加载到 模型的上下文。随着逐步发现,只有主要工具 加载;可通过以下方式发现延迟工具 tool_search.
启用 config.yaml:
tools:
discovery: progressive插件作者用以下符号标记关键工具 visibility: primary 在 plugin.yaml。所有其他工具默认为延迟。看 docs/plugin-guide.md 了解详情。
测试
# Go core tests
go test ./...
# Go core tests with race detector
go test -race ./...
# Sandbox tests (requires libarapuca)
make test-sandbox
# Python plugin tests
.venv/bin/pytest tests/ -v
# All pre-commit checks
pre-commit run --all-files项目布局
cmd/
wtmcp/ MCP server entry point
wtmcpctl/ Plugin management CLI tool
internal/
audit/ Structured JSON audit logging
auth/ Auth providers (bearer, basic, kerberos, oauth2)
cache/ Key-value cache with TTL
config/ Env var resolution, YAML config
encoding/ TOON output encoding
google/ Google OAuth helper (shared by Google plugins)
plugin/ Manager, manifest, transport, dispatch
protocol/ Wire protocol message types
proxy/ HTTP proxy with SSRF prevention
ratelimit/ Token-bucket rate limiting
sandbox/ OS-level plugin isolation (optional)
secrets/ Vault decryption, secure file descriptors
server/ MCP server, output framing, tool index
stats/ Per-tool call statistics
plugins/
google-drive/ Google Drive plugin (Go)
google-calendar/ Google Calendar plugin (Go)
google-gmail/ Gmail plugin (Go)
jira/ Jira plugin (Python, zero external deps)
confluence/ Confluence plugin (Python)
gitlab/ GitLab plugin (Python)
tests/
plugins/ Plugin unit tests
docs/
plugin-guide.md Plugin development guide
wtmcpctl.md OAuth management tool guide许可证
此项目根据GNU通用公共许可证v3.0获得许可。 看 许可证 全文。
