flowchart LR
start([Tool call]) --> tool{Tool<br/>configured?}
tool -- yes --> deny{Deny rule<br/>matches?}
deny -- no --> allow{Allow rule<br/>matches?}
allow -- yes --> flags{Flags +<br/>constraints<br/>pass?}
flags -- yes --> pargs{Positional args<br/>within limit?}
pargs -- yes --> exec([Execute])
tool -- no --> refuse([Refuse])
deny -- yes --> refuse
allow -- no --> refuse
flags -- no --> refuse
pargs -- no --> refuse
classDef ok fill:#efe,stroke:#2a7
classDef no fill:#fee,stroke:#E60028
class exec ok
class refuse no
Configuration reference
Top-level shape
Configuration uses top-level mcp and admin sections. The former server section and its nested web section are not supported: move MCP settings from server to mcp, and move Web UI settings from server.web to admin.
| Previous key | Replacement |
|---|---|
server.transport |
mcp.transport |
server.listen |
mcp.listen |
server.auth_token_file |
mcp.auth_token_file |
server.request_log |
mcp.request_log |
server.allowed_cwd_roots |
mcp.allowed_cwd_roots |
server.web.listen |
admin.listen |
server.web.auth_token_file |
admin.auth_token_file |
mcp: # how the MCP transport is exposed
admin: # operator-only request review UI
tools: # one entry per wrapped CLI tool
mcp_servers: # local stdio MCP servers, filtered by tool namemcp_servers declares local stdio MCP servers that wraptool spawns and filters. Only explicitly allowed upstream tool names are exposed; deny wins. Argument schemas and values pass through without CLI-style constraints. Capability requests and the config editors do not manage these rules. See MCP integration.
If no config exists at the resolved path, wraptool serve and wraptool up write a commented starter there (mode 0600) and log the location, rather than erroring. They never overwrite an existing file. The starter enables one privileged tool — git pull — so the server comes up and demonstrates the point of wraptool: it wraps commands that need host credentials the AI never sees. Read-only local git (status, log, diff) is intentionally not included — it needs no credentials and the assistant can run it itself. Edit the file to expose the tools you want; it hot-reloads on save.
mcp
| Field | Type | Default | Description |
|---|---|---|---|
transport |
sse | unix |
sse |
MCP transport |
listen |
string | — | host:port (sse) or socket path (unix) |
auth_token_file |
string | — | Bearer token file for sse/unix |
log_level |
string | info |
parsed and defaulted; currently does not change logging |
request_log |
string | XDG data dir | path to capability-request JSONL |
allowed_cwd_roots |
[]string |
— | when non-empty, constrains a client-supplied cwd to these roots (symlinks resolved); a cwd outside them is refused. Empty = no containment |
The sse/unix TCP listener serves both MCP HTTP transports on the one port — legacy SSE at /sse and Streamable HTTP at /mcp — behind the same auth_token_file Bearer check. Clients hit whichever their generated config targets.
admin
Starts an operator-facing Web UI for reviewing capability requests inside the serve process, on its own listener — independent of the MCP transport, so it works regardless of transport. Leave listen empty to disable.
| Field | Type | Default | Description |
|---|---|---|---|
listen |
string | — | host:port for the UI; bind 127.0.0.1 — it is not AI-facing |
auth_token_file |
string | — | required token, distinct from mcp.auth_token_file. Browsers authenticate via ?token= or a login form ⇒ an HttpOnly session cookie; scripts send it as a Bearer header |
mcp:
transport: sse
listen: 127.0.0.1:8717
auth_token_file: ~/.config/wraptool/mcp-token
admin:
listen: 127.0.0.1:8718
auth_token_file: ~/.config/wraptool/admin-tokenApproving with apply to config runs the same config.ApplyRequest merge as requests approve --apply; the running server hot-reloads via the config file-watch.
tools.<name>
Each tool is keyed by the user-visible name (which becomes the MCP tool prefix: git → git_pull, git_push, …).
tools:
gcloud:
binary: /usr/bin/gcloud # absolute path, validated at startup
working_dir: /var/lib/wraptool # optional cwd for child processes
timeout: 30s # per-call timeout
env: # inline env vars (override env_files)
CLOUDSDK_CORE_PROJECT: my-project
env_files: # KEY=VALUE files, merged left-to-right
- /etc/wraptool/gcloud.env
introspection:
cache_ttl: 24h
overrides_file: ~/.config/wraptool/overrides/gcloud.yaml
allow:
- subcommand: [compute, instances, list]
flags: ["--format", "--filter"]
deny_flags: ["--quiet"]
flag_constraints:
"--filter": { pattern: "^[a-zA-Z0-9_ =.\"'-]+$" }
arg_constraints:
positional_max: 2
deny:
- subcommand: [auth]Allow rules
| Field | Type | Description |
|---|---|---|
subcommand |
[]string |
Token list; matched exactly ([remote, set-url] does not match [remote]) |
flags |
[]string |
Whitelist of permitted flags |
deny_flags |
[]string |
Flags blocked even though in flags |
flag_constraints |
map[flag]Constraint |
Value-level rules |
arg_constraints.positional_max |
int |
Max positional args |
Help flags are always permitted. At config load wraptool seeds -h and --help into every allow rule’s flags (config.SeedHelpFlags), so they are callable and introspectable on any approved subcommand without listing them. The MCP schema renders them as boolean params. To revoke help on a rule, list the flag under its deny_flags — deny wins.
Deny rules
Evaluated before allow rules. Take absolute precedence.
deny:
- subcommand: [exec]
- subcommand: [auth]Flag constraints
| Field | Type | Description |
|---|---|---|
required |
bool | Flag must be provided |
type |
string | integer, boolean, or string |
min, max |
int | Range for integer values |
pattern |
string | Regex the value must match |
deny |
bool | Flag is always denied |
Boolean (valueless) flags. A flag’s value-taking-ness is decided by, in order: an explicit type constraint, then --help introspection, then a name heuristic. The heuristic treats --no-*/--enable-*/--disable-* prefixes and the well-known switches --yes/-y, --force, --dry-run, --quiet, --verbose as boolean, so they render as a bare --flag (not --flag true) without needing a flag_constraints entry. Short -f is deliberately excluded (it’s a filename flag as often as a force switch). If a tool uses one of these names with a value, introspection detects it (or set type: string).
Evaluation order
Validating a config
Check a config before the server loads it:
wraptool config validate # default / $WRAPTOOL_CONFIG path
wraptool config validate ./config.yaml # an explicit fileIt exits non-zero with a message on the first problem, and touches nothing — no server, no spawned tools. Validation, server startup, and hot reload all strictly decode the YAML: a misspelled key such as flag_constraint (instead of flag_constraints) is reported rather than silently dropped. It then applies defaults and runs the same semantic checks serve does at startup.
Handy before an edit that the server would otherwise hot-reload: a config that fails validation is rejected on reload and the previous policy stays in effect, so validating first tells you whether a change will actually take.
CLI introspection
wraptool parses --help output at startup to enrich MCP tool schemas with real flag descriptions and types. Supported styles:
- Git/Cobra (
git commit -h,kubectl get -h) - GNU (
curl --help,wget --help) - Fallback regex extraction
Results cache to ~/.cache/wraptool/introspection/. Override misparses with a YAML file referenced from the tool’s introspection block:
# ~/.config/wraptool/overrides/git.yaml
subcommands:
commit:
description: "Record changes to the repository"
flags:
"--message":
short: "-m"
type: string
description: "Commit message"Environment isolation
Each child process gets a minimal base environment:
PATH— the tool binary’s own directory prepended to the server’s inheritedPATH(falling back to/usr/bin:/binonly when that is unset), so a tool finds both co-located helpers (e.g.kubectl→gke-gcloud-auth-plugin) and genuine runtime deps in a separate prefix (e.g.git→sshon a store-based distro like Guix)HOME— copied from the server environment; override it in toolenvwhen user-global configuration or credentials must not be visibleLANG=C.UTF-8- Variables from
env_files(merged in order) - Variables from inline
env(overrideenv_files)
No other parent variable is inherited. This minimizes ambient environment exposure, but inheriting HOME means it is not by itself credential isolation for host-side child processes.
mcp_servers.<name>
| Field | Type | Default | Description |
|---|---|---|---|
command |
string | required | Child command; resolved by exec, not required to be absolute |
args |
[]string |
— | Child arguments |
working_dir |
string | — | Parsed, but currently not applied to the child process |
env, env_files |
map, list | — | Child environment additions |
timeout |
duration | 30s |
Per-tool-call timeout |
init_timeout |
duration | 10s |
Initialization budget |
name_prefix |
string | map key | Prefix for exposed tools |
allow, deny |
list of {tool: name} |
— | Exact upstream names; default-deny and deny-first |
Platform paths
| Purpose | Linux | macOS |
|---|---|---|
| Config | ~/.config/wraptool/ |
~/Library/Application Support/wraptool/ |
| Data | ~/.local/share/wraptool/ |
~/Library/Application Support/wraptool/ |
| Cache | ~/.cache/wraptool/ |
~/Library/Caches/wraptool/ |
Override with WRAPTOOL_CONFIG or --config.