restic-defensive-mcp
Structurally read-only stdio MCP server for inspecting restic repositories. Repositories and credentials are sealed at startup; only a fixed subset of restic subcommands can run (snapshots/ls/find/stats). No shell, no backup/restore/forget/prune.
Documentation
restic-defensive-mcp
Structurally read-only MCP server (stdio) for inspecting restic repositories through an untrusted MCP client without exposing a shell, free-form CLI, or mutation path.
Are my backups present and recent, is their metadata readable, and does a snapshot contain the files I expect?
Why not expose restic directly?
Restic is a powerful backup tool. An unrestricted caller can:
- delete retention history (
forget/prune) - overwrite or extract data (
backup/restore/dump) - unlock or rewrite repository state
- accept arbitrary repository URLs and password commands
Thin wrappers that expose "run restic with these args" or per-call repository overrides recreate that blast radius inside MCP.
This project is different by construction:
- Repositories are declared and sealed at process start
- MCP surface is exclusively read-only (no feature flag for writes)
- Only a compiled-in subset of restic subcommands can be built
- No shell —
exec.CommandContextwith fixed argv only - Hosts, tags, paths, and result sizes are bounded
- Secrets come from permission-checked files, never
RESTIC_PASSWORD_COMMAND - Errors are structured and redacted
- Every tool reports an explicit cost class (
light/moderate/expensive)
Requirements
- Go 1.25+ (build)
- restic 0.17.1+ on
PATH(orrestic_binaryin config); tested with 0.19.x - An MCP client that supports local stdio servers
Install (from source)
git clone https://github.com/ThomasCrouzet/restic-defensive-mcp.git
cd restic-defensive-mcp
make build
./bin/restic-defensive-mcp --version
Quick start
- Create password and repository location files (regular files, no symlinks). Avoid placing the password in shell history:
secret_dir="${XDG_CONFIG_HOME:-$HOME/.config}/restic-defensive-mcp"
install -d -m 700 "$secret_dir"
install -m 600 /dev/null "$secret_dir/repository"
printf '%s\n' '/path/to/your/repo' > "$secret_dir/repository"
install -m 600 /dev/null "$secret_dir/password"
"${EDITOR:-vi}" "$secret_dir/password"
-
Copy and edit
config.example.yaml, including the two file paths above. -
Run the freshly built binary:
./bin/restic-defensive-mcp --config /path/to/config.yaml
- Point your MCP host at the binary with
--config(stdio transport only).
Example MCP host snippet (illustrative):
{
"mcpServers": {
"restic-defensive": {
"command": "/absolute/path/to/restic-defensive-mcp",
"args": ["--config", "/etc/restic-defensive-mcp/config.yaml"]
}
}
}
Tools (exactly seven)
| Tool | Cost | Purpose |
|---|---|---|
restic_capabilities | light | Versions, limits, backends, warnings |
list_repositories | light | Configured IDs and allowlists (no URLs) |
list_snapshots | light | Bounded snapshot list |
get_snapshot | light | One snapshot metadata |
browse_snapshot | moderate | Directory listing inside allowlisted path |
find_files | moderate–expensive | Simple glob find (not arbitrary regex) |
repository_stats | moderate–expensive | Size/count stats for allowlisted modes |
There is no run_restic, backup, restore, forget, prune, unlock,
check --read-data, or repository URL argument.
Full request/response shapes: docs/tool-contract.md.
Configuration
See config.example.yaml. Principles:
- MCP callers pass only
repository_id - Prefer
repository_file+password_fileover inline secrets - Repository locations and backend credentials are loaded and sealed at boot; changing their source files does not retarget a running server
allowed_hosts/allowed_tags/allowed_pathsare enforcement, not cosmetics- Empty allowlists mean no restriction for that dimension; the server logs a
boot-time warning (
empty_allowlist) so operators notice - Unknown YAML keys and multiple YAML documents are rejected
- Secret files must be regular, non-symlink files; Unix requires mode
0600or stricter, while Windows deployments must enforce equivalent NTFS ACLs find_filesrequires an explicitpathwhen multiple roots are allowlisted- Runtime limits: zeros become defaults; values outside absolute min/max are rejected (not silently clamped). Defaults are the recommended starting point; values may be raised only up to those ceilings
Supported backends (v0.1)
| Backend | Supported |
|---|---|
| local filesystem | yes (absolute paths only) |
| S3 | yes (credentials via allowlisted env files) |
| Backblaze B2 | yes |
| REST server | yes |
| SFTP | no (spawns ssh) |
| rclone | no (spawns rclone) |
| other | no |
Compatibility notes: docs/restic-compatibility.md.
Local effects: cache and locks
Inspection is not a pure no-op on disk or on the repository lock table:
- restic may create or update a local cache (
cache_diror default) - restic may take a repository lock during snapshots/ls/find/stats
This server never intentionally mutates backup contents or snapshot sets.
It also never runs full data verification (restic check --read-data).
repository_stats answers size/count questions only.
Details: SECURITY.md.
Privacy limits
Snapshot file names, paths, hosts, tags, sizes, and timestamps are sensitive. This server:
- never returns file contents
- never returns repository URLs or secret file paths
- bounds listings and find results
- sanitizes control characters in names
- keeps audit logs free of full paths by default
Demo (local temporary repository)
make demo
Runs the end-to-end harness against a throwaway restic repository created by
internal/testrepo. It exercises every MCP tool, proves path denial and the
absence of mutation tools, then removes the fixture. Repository operations stay
local; normal Go module downloads may still occur on a fresh checkout.
Development
make fmt
make lint
make test # unit tests
make race # race detector
make fuzz-smoke # short run of every fuzz target
make integration # real restic temp repo (requires restic)
make vet
Dependencies
| Module | Why |
|---|---|
github.com/modelcontextprotocol/go-sdk | Official MCP Go SDK (stdio server/client) |
gopkg.in/yaml.v3 | Config file parsing |
Restic itself is an external binary, not a Go module.
Non-goals
- Backup orchestration or scheduling
- Restore / forget / prune / unlock / repair
- Generic restic CLI proxy
- Autorestic config import
- Remote multi-tenant API
- Full
restic check --read-dataas a casual tool - Telemetry or auto-update
License
MIT — see LICENSE.