frpdeck
frpdeck is a lightweight Python 3.11+ CLI for managing FRP instances from structured source files. It focuses on practical single-host operations: initialize instance directories, validate configuration, render generated artifacts, sync managed runtime files, apply changes locally, inspect state, and maintain structured proxy definitions without introducing a larger control plane.
It is also MCP-friendly. frpdeck includes a local stdio MCP thin wrapper so an LLM can assist with structured proxy maintenance against one bound instance directory at a time.
Highlights
- Lightweight FRP deployment and maintenance workflows for client and server instances.
- Structured proxy management backed by
proxies.yaml, with import, typed add, update, remove, and preview support. - Stable JSON outputs for automation and scripting.
- Append-only audit logging and revision snapshots for write operations.
- Local stdio MCP support for LLM-assisted proxy maintenance.
Quick start
For setup steps and first client/server workflows, start with QUICKSTART.md.
Minimal client path:
pip install frpdeck
frpdeck init client my-client
# Edit ./my-client/node.yaml, ./my-client/proxies.yaml, and secrets first.
frpdeck apply --instance ./my-client --sudo
frpdeck status --instance ./my-client
The normal workflow is init → edit configuration and secrets → apply → status. apply already validates and renders, so separate validate and render commands are optional advanced steps.
Installation
Install from PyPI:
pip install frpdeck
frpdeck --help
Install from source when you want a local checkout:
python3.11 -m venv .venv
. .venv/bin/activate
python -m pip install .
For development:
python3.11 -m venv .venv
. .venv/bin/activate
python -m pip install -e '.[dev]'
Documentation
Key design notes now live under docs/:
QUICKSTART.md: install and first client/server workflowsdocs/architecture.md: layer boundaries and dependency directiondocs/configuration.md: instance config shape, defaults, path resolution, and logging semanticsdocs/development.md: local development, tests, packaging, and MCP testingdocs/release.md: version bump, build, and tag/publish checklistCHANGELOG.md: release history
Features
initcreates a new client or server instance directory.validatechecks source config only: schema, placeholder values, token sources, path resolution, and simple proxy conflicts.rendergenerates FRP TOML, proxy includes, and systemd units underrendered/only.syncmirrors managed files fromrendered/intoruntime/configonly.reloadis client-only and asks the frpc control/web server endpoint to reload the currentruntime/configwithout restarting systemd.applyvalidates, renders, syncs runtime files, installs binaries if needed, installs the systemd unit, and restarts the service.restartrestarts the configured systemd service using the current runtime config;statusinspects instance and service state.check-updateandupgradeinspect or replace the managed FRP binary; they do not update the frpdeck Python package.doctorchecks the current-directory instance and system by default, with explicit instance and system-only modes available.python -m frpdeck.mcp.serverstarts a local stdio MCP server that exposes proxy-management tools and read-only status resources.
All mutating commands support --sudo. When a non-root user passes --sudo, frpdeck re-execs the full command via sudo before loading instance config or touching managed files. Without --sudo, mutating commands fail early with a retry hint when required paths are not readable or writable by the current user.
Current scope
frpdeck is a focused operations tool, not a full FRP control platform. It currently centers on structured instance management, proxy maintenance, local apply workflows, auditing, and MCP-assisted maintenance. HTTP control planes, remote auth layers, and web dashboards are intentionally out of scope for now.
Non-goals
- Remote HTTP transport for MCP
- Authentication or authorization for remote MCP access
- Web dashboard or visualization service
- Remote centralized control
- Interactive TOML editing
Command semantics
The normal deployment path is:
init → edit configuration and secrets → apply → status
Use the following commands when you deliberately need preflight checks or staged deployment:
validateis an optional read-only preflight. It readsnode.yamlandproxies.yaml, validates them, and does not writerendered/orruntime/config.renderwrites the full generated snapshot intorendered/. It does not touchruntime/config, reload FRP, or restart systemd.syncmirrors the managed rendered snapshot intoruntime/config. It does not run validation, rendering, reload, or restart logic.reloadis client-only. It asks the frpc control/web server endpoint to reload the currentruntime/configwithout restarting the systemd service. If runtime config is missing, runsyncorapplyfirst.restartrestarts the instance's systemd service with the current runtime config; it does not validate, render, or sync source changes.applyis the preferred full operational path: validate, render, sync, install/upgrade the managed binary if needed, install the systemd unit, and restart the service.proxy previewis a temporary client-side preview of proxy include output. It does not modifyrendered/. Top-levelrenderwrites the full instance snapshot intorendered/.
frpdeck check-update checks for a newer managed FRP binary, and frpdeck upgrade upgrades that binary. Neither command upgrades the installed frpdeck Python package; use your Python package manager for that.
Doctor modes:
frpdeck doctor # current-directory instance plus system
frpdeck doctor --instance ./my-client # selected instance plus system
frpdeck doctor --system-only # system environment only
Write commands accept --sudo and re-exec the entire command via sudo when needed. This applies to instance-scaffold, render/sync/apply/reload/restart/upgrade/uninstall workflows, structured proxy mutations, and MCP wrapper install or uninstall. You can still run sudo frpdeck ... manually, but frpdeck ... --sudo is the preferred retry path because the command itself can fail early before doing partial work.
Uninstall installed artifacts while keeping source configuration:
frpdeck uninstall --instance ./my-client
Delete the instance directory as well:
frpdeck uninstall --instance ./my-client --purge
Server vhost modes
By default, a new server instance does not set server.vhost_http_port, server.vhost_https_port, or server.subdomain_host.
- With the default scaffold, rendered
frps.tomldoes not bind80or443and does not enable subdomain host handling. - When you explicitly set
server.vhost_http_portorserver.vhost_https_port,frpdeckrenders those values intofrps.toml. - When you explicitly set
server.subdomain_host,frpdeckrenderssubDomainHost.
Example server config with vhost enabled:
server:
bind_addr: 0.0.0.0
bind_port: 7000
vhost_http_port: 80
vhost_https_port: 443
subdomain_host: frp.example.com
log:
to: runtime/logs/frps.log
level: info
max_days: 7
disable_print_color: true
auth:
method: token
token_file: secrets/token.txt
HTTP/HTTPS proxies
Client proxy definitions for http and https stay in proxies.yaml with the existing snake_case source config style.
HTTP with custom_domains:
proxies:
- name: app_http
type: http
local_ip: 127.0.0.1
local_port: 8080
custom_domains:
- app.example.com
HTTPS with custom_domains:
proxies:
- name: app_https
type: https
local_ip: 127.0.0.1
local_port: 8443
custom_domains:
- secure.example.com
HTTP with subdomain:
proxies:
- name: app_subdomain
type: http
local_ip: 127.0.0.1
local_port: 8080
subdomain: app
custom_domains and subdomain may be set together. That is supported by the implementation, although in practice it is usually clearer to choose the one that matches the deployment pattern.
http and https proxies must define at least one of:
custom_domainssubdomain
Blank strings are rejected for custom_domains, subdomain, and server.subdomain_host.
Proxy CLI shortcuts
Import one proxy mapping from a YAML file:
frpdeck proxy import ./app-http.yaml --instance ./my-client
Update one existing proxy from a YAML patch file:
frpdeck proxy update ssh ./ssh-patch.yaml --instance ./my-client
Add an HTTP proxy with one or more custom domains:
frpdeck proxy add http \
--instance ./my-client \
--name app-http \
--local-port 8080 \
--custom-domain app.example.com \
--custom-domain www.example.com
Add an HTTPS proxy:
frpdeck proxy add https \
--instance ./my-client \
--name app-https \
--local-port 8443 \
--custom-domain secure.example.com
Add an HTTP proxy using a subdomain:
frpdeck proxy add http \
--instance ./my-client \
--name app-subdomain \
--local-port 8080 \
--subdomain app
--custom-domain is repeatable, and it can be combined with --subdomain when you want both selectors on the same proxy.
Disable a proxy while retaining its definition:
frpdeck proxy disable app-http --instance ./my-client
Permanently delete a proxy definition from proxies.yaml:
frpdeck proxy remove app-http --instance ./my-client
In short, proxy disable preserves the definition with enabled: false; proxy remove permanently deletes it. Both require a later apply to deploy the changed desired state.
MCP
frpdeck ships with a local stdio MCP thin wrapper over structured proxy CRUD, import, and preview tools plus read-only status resources. It is designed to bind to one instance directory at a time and is best used through a generated wrapper script.
Recommended workflow: generate a bound wrapper script with frpdeck mcp install-stdio-wrapper and point your local MCP client at that script. Prefer the generated wrapper over writing your own unless you have a specific reason to customize startup behavior. The target must already be a valid frpdeck instance with a loadable node.yaml. The wrapper binds to the chosen instance directory and, by default, embeds the Python interpreter running frpdeck when the script is created. Use --python /path/to/python if you need to override that explicitly.
In practice, wrapper scripts are most commonly generated for client instances, because proxy configuration is usually managed on the client side. That is a usage pattern rather than a hard restriction: the MCP wrapper is tied to an instance directory, not to a separate client-only mode in the documentation.
Recommended MCP setup
On the FRP machine, change into your instance directory and generate the wrapper:
cd /path/to/your-instance
frpdeck mcp install-stdio-wrapper
This is equivalent to:
frpdeck mcp install-stdio-wrapper --instance /path/to/your-instance
The command writes /path/to/your-instance/start-mcp-stdio.sh, binds that script to the resolved absolute instance path, embeds the Python interpreter that is running frpdeck at generation time, and prints a local stdio configuration example that executes the wrapper directly. Replace the example path with your own instance directory. There is no built-in SSH destination.
The local example has this shape:
claude mcp add --scope user --transport stdio frpdeck -- \
/path/to/your-instance/start-mcp-stdio.sh
For an explicitly remote setup, ask the install command to print additional SSH examples:
frpdeck mcp install-stdio-wrapper \
--instance /path/to/your-instance \
--ssh-host your-ssh-host
--ssh-host only controls the extra output examples; it does not change the generated wrapper file.
If you need to start the server manually without the wrapper, you can still use:
python -m frpdeck.mcp.server
For a bound one-instance server, the direct form is:
python -m frpdeck.mcp.server --instance-dir /path/to/your-instance
For the explicit remote mode, manually verify the SSH command from the MCP client machine. Replace the host name and path with your own SSH destination and instance directory:
ssh your-ssh-host /path/to/your-instance/start-mcp-stdio.sh
That command should normally stay attached and wait for stdin/stdout traffic because the MCP stdio server is waiting for client messages. If it exits immediately or prints an error, fix the remote Python environment, instance path, or SSH setup first.
Once the manual SSH command works, add the MCP entry in Claude Code:
claude mcp add --scope user --transport stdio frpdeck -- \
ssh your-ssh-host /path/to/your-instance/start-mcp-stdio.sh
Current MCP scope is intentionally small:
- Local stdio MCP server only.
- Structured proxy CRUD/import/preview only; instance-level
validate/sync/applystay in the CLI. - No HTTP transport.
- No remote auth layer.
- No web UI.
Audit and safety notes
Write operations append audit records under state/audit/audit.jsonl, and proxy mutations also create revision snapshots under state/revisions/. This is intended to make changes traceable and manually recoverable without turning the tool into a full control plane.
Optional remote SSH and BatchMode
This section applies only when you explicitly use --ssh-host. BatchMode yes is useful for unattended or scripted SSH sessions because it disables interactive password prompts and host-key confirmation. Do not treat it as the first step.
Recommended order:
- Manually run the SSH wrapper command until it works without prompts.
- Confirm that host keys are trusted and key-based auth is already working.
- Only then consider enabling
BatchMode yesin~/.ssh/config.
Example SSH config shape:
Host your-frp-host
HostName <host-or-ip>
User <user>
IdentityFile ~/.ssh/id_ed25519
# Add BatchMode yes only after manual SSH testing succeeds
# BatchMode yes
Test fixtures
Repository fixtures now live under tests/fixtures/instances/. They exist for tests and development reference only. Daily usage should start from frpdeck init ..., not by editing fixture directories directly.
Notes on paths
- Relative paths in YAML are resolved against the instance directory, not the shell working directory.
- Rendered systemd units always use absolute runtime paths.
- By default, runtime files are installed under
runtime/inside the instance directory, while the systemd unit is written to/etc/systemd/system. - FRP's own logs are controlled by
client.logorserver.logand are written into generated frpc/frps config. frpdeck's own logs are configured by top-levelfrpdeck_logginginsidenode.yaml.- Source configuration remains YAML.
node.yamlis always present, whileproxies.yamlis used for client proxy definitions and may be absent on server instances. There is no separate runtime config file for frpdeck in the current design.
Metadata
Release files for frpdeck 1.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| frpdeck-1.2.0.tar.gz | 103.3 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| frpdeck-1.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 209.9 kB
Release files / frpdeck-1.2.0.tar.gz
| Download URL | frpdeck-1.2.0.tar.gz |
|---|---|
| Size | 103.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
5d0819b219c9a38755ca62718b91d560f292debc06b459157468f0c9a1f9be34
|
|
BLAKE2b-256 checksum How to use checksums |
66a2401e32416c5bf35530551894d3c06740fb93e61cb67d271b9dd6dd478bbd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 2, 2026.
Transparency logRelease files / frpdeck-1.2.0-py3-none-any.whl
| Download URL | frpdeck-1.2.0-py3-none-any.whl |
|---|---|
| Size | 106.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b25c45a075e6e4c30ac065b37430d3bdee73d5faf2aa7e10e78815ef17ea2d79
|
|
BLAKE2b-256 checksum How to use checksums |
8b8832a8df3aa0c1f8d1e08da74853cd6741b796051827fdd082303d26e2ef8a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 2, 2026.
Transparency logSecurity for frpdeck 1.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for frpdeck-1.2.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 2, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.