Deployment¶
Deployment Options¶
tunnel-manager supports local stdio, a loopback-only development listener, a
least-privilege stdio container, and a remote authenticated HTTPS boundary.
Provider endpoint, credential, selector, identity, and trust material are supplied
at runtime through AgentConfig; none is stored in this repository.
Installed stdio process¶
{
"mcpServers": {
"tunnel-manager": {
"command": "tunnel-manager-mcp",
"args": [],
"env": {"MCP_TOOL_MODE": "intent"}
}
}
}
Loopback development listener¶
Do not expose this listener beyond loopback. Network deployments require direct TLS
or an explicitly trusted TLS-terminating ingress, configured authentication, exact
MCP_ALLOWED_HOSTS, and an exact trusted-proxy CIDR policy.
Least-privilege local container¶
docker run -i --rm \
--read-only \
--cap-drop=ALL \
--security-opt=no-new-privileges \
--pids-limit=256 \
--tmpfs /tmp:rw,noexec,nosuid,nodev,size=64m \
-e TRANSPORT=stdio \
registry.example.invalid/tunnel-manager@sha256:<digest> tunnel-manager-mcp
The operator projects the selected AgentConfig profile into the process at runtime; the image remains immutable and contains no environment connection profile.
Remote authenticated HTTPS endpoint¶
Store the real remote URL, outbound identity reference, and TLS-profile reference in
AgentConfig, not in MCP client JSON or documentation.
This page covers running tunnel-manager as a long-lived server: the transports, a
Docker Compose stack, putting it behind a Caddy reverse proxy, and giving it a DNS
name with Technitium. tunnel-manager ships both an MCP server (console script
tunnel-manager-mcp) and a Pydantic-AI agent server (console script
tunnel-manager-agent); both are covered below.
Run the MCP server¶
The transport is selected with --transport (or the TRANSPORT env var):
Health check (HTTP transports):
Bind to a non-loopback address only inside an isolated container network or
behind an authenticated, authorization-enforcing gateway. Never publish the raw
MCP listener directly. The shared server boundary requires a supported
AUTH_TYPE, exact MCP_ALLOWED_HOSTS, and either direct TLS material or an
explicit trusted TLS-termination proxy before any non-loopback listener starts.
Configuration (environment)¶
tunnel-manager is configured from the environment (or a sibling .env file). The
required / commonly set variables:
| Var | Default | Meaning |
|---|---|---|
HOST |
127.0.0.1 |
Loopback bind for HTTP transports; use an authenticated gateway for remote access |
PORT |
8000 |
Listen port for HTTP transports |
TRANSPORT |
stdio |
stdio, streamable-http, or sse |
TUNNEL_IDENTITY_FILE |
~/.ssh/id_ed25519 |
SSH private key used to connect to hosts |
TUNNEL_KNOWN_HOSTS |
~/.ssh/known_hosts |
Independently checked SSH server-key trust store |
TUNNEL_PASSWORD_REF |
— | Opaque runtime password reference; literal passwords are rejected |
ENABLE_OTEL |
True |
Emit OpenTelemetry traces |
EUNOMIA_TYPE |
none |
Authorization mode — none, embedded, or remote |
DEBUG |
False |
Verbose logging |
Each action-routed tool can be toggled independently:
| Var | Default | Tool |
|---|---|---|
TM_HOSTS_TOOL |
True |
Host inventory management (tm_hosts) |
TM_REMOTE_TOOL |
True |
Single-host SSH operations (tm_remote) |
TM_INVENTORY_TOOL |
True |
Bulk inventory operations (tm_inventory) |
TM_OPERATIONS_TOOL |
True |
Operation lifecycle / sessions (tm_operations) |
TM_SYSTEM_TOOL |
True |
Remote system intelligence (tm_system) |
TM_FILES_TOOL |
True |
Advanced file operations (tm_files) |
TM_SECURITY_TOOL |
True |
Security and compliance auditing (tm_security) |
The runtime setting projection, including the OTEL and Eunomia connection settings,
is documented in .env.example.
Copy it to .env and populate only what the operator uses. The compose files are rendered from
pyproject.toml by the central container render; their image variables are required :? substitutions
checked by the container-inputs build check.
Docker Compose¶
The repo ships docker/mcp.compose.yml.
It reads a sibling .env and publishes the HTTP server on :8000:
services:
tunnel-manager-mcp:
image: example/tunnel-manager@sha256:<digest>
container_name: tunnel-manager-mcp
hostname: tunnel-manager-mcp
restart: always
env_file:
- ../.env
environment:
- PYTHONUNBUFFERED=1
- HOST=0.0.0.0
- PORT=8000
- TRANSPORT=streamable-http
ports:
- "8000:8000"
healthcheck:
test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]
interval: 30s
timeout: 10s
retries: 3
cp .env.example .env # then edit TUNNEL_IDENTITY_FILE and toggles
docker compose -f docker/mcp.compose.yml up -d
docker compose -f docker/mcp.compose.yml logs -f
Agent server¶
tunnel-manager includes a Pydantic-AI graph agent (console script
tunnel-manager-agent) that connects to the MCP server and exposes the Agent Control
Protocol plus an optional web UI. Run it standalone:
export MCP_URL=http://tunnel-manager-mcp:8000/mcp
export PROVIDER=openai
export MODEL_ID=gpt-4o
tunnel-manager-agent --host 0.0.0.0 --port 9002
The combined docker/agent.compose.yml
provisions the MCP server and the agent together; the agent reaches the MCP server by
container name through MCP_URL and is published on :9002:
services:
tunnel-manager-agent:
image: example/tunnel-manager@sha256:<digest>
container_name: tunnel-manager-agent
depends_on:
- tunnel-manager-mcp
env_file:
- ../.env
command: ["tunnel-manager-agent"]
environment:
- PYTHONUNBUFFERED=1
- HOST=0.0.0.0
- PORT=9002
- MCP_URL=http://tunnel-manager-mcp:8000/mcp
- PROVIDER=${PROVIDER:-openai}
- MODEL_ID=${MODEL_ID:-gpt-4o}
- ENABLE_WEB_UI=True
ports:
- "9002:9002"
Behind a Caddy reverse proxy¶
Expose the HTTP server on a hostname with automatic TLS. Add to the operator's Caddyfile:
# Internal (self-signed) — private .example.invalid zone
tunnel-manager.example.invalid {
tls internal
reverse_proxy tunnel-manager-mcp:8000
}
# Public — automatic Let's Encrypt
tunnel-manager.example.com {
reverse_proxy tunnel-manager-mcp:8000
}
Reload Caddy:
DNS with Technitium¶
Point the hostname at the host running Caddy. Via the Technitium API:
curl -s "http://technitium.example.invalid:5380/api/zones/records/add" \
--data-urlencode "token=$TECHNITIUM_DNS_TOKEN" \
--data-urlencode "domain=tunnel-manager.example.invalid" \
--data-urlencode "zone=arpa" \
--data-urlencode "type=A" \
--data-urlencode "ipAddress=192.0.2.10" \
--data-urlencode "ttl=3600"
…or add an A record tunnel-manager.example.invalid → <caddy-host-ip> in the Technitium web
console (http://technitium.example.invalid:5380). The ecosystem
technitium-dns-mcp automates
this as a tool.
Register with an MCP client¶
Add to the operator's client's mcp_config.json (multiplexer nickname tun):
{
"mcpServers": {
"tunnel-manager": {
"command": "uv",
"args": ["run", "tunnel-manager-mcp"],
"env": {
"TUNNEL_IDENTITY_FILE": "~/.ssh/id_ed25519"
}
}
}
}
For a remote HTTP server, point the client at http://tunnel-manager.example.invalid/mcp instead.