wtmcp
MCP server with a language-agnostic plugin system. Plugins are simple
executables (Python, bash, or any language) that communicate with the
core over JSON-lines on stdin/stdout. The core handles auth, HTTP
proxying, caching, and output encoding so plugins stay minimal.
Architecture
┌─────────────────────────────────────────────────┐
│ 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 │
│ Landlock + cgroups + netns + Seatbelt │
└────────┬──────────────────────────┬─────────────┘
│ stdio (MCP/JSON-RPC) │ stdin/stdout (JSON-lines)
┌────┴────┐ ┌─────┴──────────┐
│ AI │ │ Plugins │
│ Client │ │ Zero deps │
└─────────┘ │ No HTTP libs │
│ No auth code │
└────────────────┘
Features
- Plugin protocol: JSON-lines over stdin/stdout, any language
- Auth: Bearer, Basic, Kerberos/SPNEGO, OAuth2 with token refresh,
auto-detection from available credentials
- HTTP proxy: Auth injection, domain validation, TLS enforcement,
binary response encoding, multipart upload support
- Cache: In-memory store with namespace isolation and TTL
- File I/O: Atomic writes, path confinement, size limits, per-plugin
output directories, source_path handoff for large files
- Output: TOON encoding for ~40% token savings (optional)
- Plugin setup: Manifest-declared wizard metadata for CLI tooling
- Progressive discovery: Tools default to deferred; only primary
tools are loaded into model context. Deferred tools are
discoverable via
tool_search and called directly through MCP
- Encrypted credentials: Ansible Vault encrypted env.d files,
auto-detected and decrypted transparently at startup
Security
wtmcp enforces security at the core level so plugins don't need to
implement their own auth, input validation, or network restrictions.
HTTP Proxy & SSRF Prevention
All plugin HTTP traffic goes through the core proxy. No plugin makes
direct network connections.
- SSRF-safe dialer validates resolved IPs at connection time —
blocks private, loopback, link-local, multicast, and IPv6-mapped
IPv4 addresses
- Domain allowlisting per plugin — only declared domains are
reachable
- Credential stripping on cross-domain redirects — Authorization,
Cookie, and API key headers are removed when redirected to a
different host
- Dangerous headers stripped from plugin-crafted requests
(Host, Proxy-Authorization, X-Forwarded-For, etc.)
- HTTPS enforcement for authenticated requests; mTLS support
with certificate chain verification
- Userinfo URL rejection —
user:pass@host URLs are blocked
Sandboxing
OS-level plugin isolation via
arapuca is built by
default:
- Landlock LSM filesystem confinement (Linux) — plugins can
only read/write declared paths
- Seatbelt sandboxing (macOS) — equivalent confinement
- cgroup v2 resource limits — memory, CPU, PIDs, file size
(configurable per-plugin)
- Network namespace isolation — plugins cannot make direct
connections; all traffic routes through the core proxy
- OOM detection and resource usage reporting after process exit
Platform note: Linux provides the strongest sandbox protection.
The file I/O path on Linux uses O_NOFOLLOW + /proc/self/fd
readlink to close TOCTOU gaps in source_path validation. On macOS,
the Seatbelt sandbox provides equivalent filesystem confinement but
the file I/O path uses a stat-then-open fallback with a narrower
(but non-zero) TOCTOU window. Production deployments should use
Linux.
Building requires libarapuca — either from a system package
(Fedora) or built from the bundled submodule (requires a
Rust toolchain). The Makefile auto-detects which path to use:
make build # auto-detects system libarapuca or builds from submodule
To build without sandbox (debug/development only):
go build -tags nosandbox ./cmd/wtmcp
WTMCP_UNSANDBOXED=1 ./wtmcp # required at runtime
Binaries built without sandbox loudly warn at startup, in logs,
and in the MCP server description visible to the LLM.
Rate Limiting