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:

  1. Repositories are declared and sealed at process start
  2. MCP surface is exclusively read-only (no feature flag for writes)
  3. Only a compiled-in subset of restic subcommands can be built
  4. No shellexec.CommandContext with fixed argv only
  5. Hosts, tags, paths, and result sizes are bounded
  6. Secrets come from permission-checked files, never RESTIC_PASSWORD_COMMAND
  7. Errors are structured and redacted
  8. Every tool reports an explicit cost class (light / moderate / expensive)

Requirements

  • Go 1.25+ (build)
  • restic 0.17.1+ on PATH (or restic_binary in 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

  1. 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"
  1. Copy and edit config.example.yaml, including the two file paths above.

  2. Run the freshly built binary:

./bin/restic-defensive-mcp --config /path/to/config.yaml
  1. 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)

ToolCostPurpose
restic_capabilitieslightVersions, limits, backends, warnings
list_repositorieslightConfigured IDs and allowlists (no URLs)
list_snapshotslightBounded snapshot list
get_snapshotlightOne snapshot metadata
browse_snapshotmoderateDirectory listing inside allowlisted path
find_filesmoderate–expensiveSimple glob find (not arbitrary regex)
repository_statsmoderate–expensiveSize/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_file over 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_paths are 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 0600 or stricter, while Windows deployments must enforce equivalent NTFS ACLs
  • find_files requires an explicit path when 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)

BackendSupported
local filesystemyes (absolute paths only)
S3yes (credentials via allowlisted env files)
Backblaze B2yes
REST serveryes
SFTPno (spawns ssh)
rcloneno (spawns rclone)
otherno

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_dir or 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

ModuleWhy
github.com/modelcontextprotocol/go-sdkOfficial MCP Go SDK (stdio server/client)
gopkg.in/yaml.v3Config 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-data as a casual tool
  • Telemetry or auto-update

License

MIT — see LICENSE.