portal-mcp-server
Server Configuration
Describes the environment variables required to run the server.
| Name | Required | Description | Default |
|---|---|---|---|
| PORTAL_JOB_TTL | No | Seconds before completed jobs are cleaned up. Default: 3600. | |
| PORTAL_LOG_DIR | No | Directory for audit and server logs. Default: ~/.local/state/portal-mcp-server/log/ | |
| PORTAL_TEST_HOST | No | Target host for live tests. Default: 127.0.0.1 | |
| PORTAL_TEST_LIVE | No | Set to '1' to enable live SSH tests. For dev only. | |
| PORTAL_TEST_PORT | No | Port for live tests. Default: 22 | |
| PORTAL_TEST_USER | No | User for live tests. Default: $USER or root | |
| PORTAL_AUTH_TOKEN | No | Bearer token for HTTP transport. Not used in stdio mode. | |
| PORTAL_HOSTS_YAML | No | Path to hosts YAML file. Default: ~/.config/portal-mcp-server/hosts.yaml | |
| PORTAL_SSH_CONFIG | No | Path to OpenSSH client config. Default: ~/.ssh/config. Set to 'none' to disable ssh config lookup. | |
| PORTAL_JOB_PERSIST | No | Set to '0' or 'false' to disable job table persistence across restarts. Default: enabled. | |
| PORTAL_JOB_MAX_LIVE | No | Max concurrent live background jobs. Default: 50. | |
| PORTAL_SECRETS_YAML | No | Path to secrets YAML file. Default: ~/.config/portal-mcp-server/secrets.yaml | |
| PORTAL_AUDIT_BACKUPS | No | Number of rotated audit files to keep. Default: 5. | |
| PORTAL_POLICIES_YAML | No | Path to policies YAML file. Default: ~/.config/portal-mcp-server/policies.yaml | |
| PORTAL_SSH_POOL_SIZE | No | Max TCP connections per host. Default: 5. | |
| PORTAL_TEST_KEY_PATH | No | Key path for live tests. Default: ~/.ssh/id_ed25519 | |
| PORTAL_JOB_STATE_FILE | No | Path to job state file. Default: <state>/jobs.json | |
| PORTAL_READ_MAX_BYTES | No | Max bytes per page for portal_read. Default: 16384. | |
| PORTAL_READ_MAX_LINES | No | Max lines per page for portal_read. Default: 2000. | |
| PORTAL_AUDIT_FAIL_OPEN | No | Set to '1' to continue on audit write failure. Default: unset (fail-closed). | |
| PORTAL_AUDIT_MAX_BYTES | No | Max bytes for audit.jsonl rotation. Default: 10485760 (10 MiB). | |
| PORTAL_DEFAULT_TIMEOUT | No | Default timeout in seconds for exec/shell/local_exec. Default: 3600. | |
| PORTAL_ALLOW_LOCAL_EXEC | No | Set to '1' to enable portal_local_exec. Default: disabled. | |
| PORTAL_SSH_MAX_CONN_AGE | No | Max connection lifetime in seconds. Default: 3600. | |
| PORTAL_SSH_MAX_IDLE_TIME | No | Seconds before idle connection is closed. Default: 600. Set 0 to disable. | |
| PORTAL_BASH_HEARTBEAT_INTERVAL | No | Seconds between keepalive heartbeats during shell/exec. Default: 5. | |
| PORTAL_CREDENTIAL_AGENT_SOCKET | No | Path to credential agent unix socket. Default: auto-detected via agent.json. | |
| PORTAL_SSH_MAX_CHANNELS_PER_CONN | No | Max concurrent channels per TCP connection. Default: 5. |
Capabilities
Features and capabilities supported by this server
| Capability | Details |
|---|---|
| tools | {
"listChanged": false
} |
| prompts | {
"listChanged": false
} |
| resources | {
"subscribe": false,
"listChanged": false
} |
| experimental | {} |
Tools
Functions exposed to the LLM to take actions
| Name | Description |
|---|---|
| hosts | Manage the SSH host registry. Modes
Hosts already defined in ~/.ssh/config are auto-resolved on first use; explicit
registration is only needed for tag-based grouping. This MCP tool only
accepts key-based hosts — password auth is intentionally not exposed
here so credentials cannot leak into LLM tool-call traces. To use
password auth, declare the host in hosts.yaml with action="list" may include a per-host |
| remote_transfer | Transfer files between local and remote via SFTP (binary-safe, atomic). Modes
Args:
checksum: for the incremental modes (sync/mirror/upload-list/
download-list), compare files by sha256 instead of size+mtime
(slower, requires Progress is reported to the MCP client during transfers. Returns a JSON status dict. Single-file: {status, direction, host, bytes, duration_s, ...}. sync/mirror/upload-list/download-list: {status, uploaded|downloaded, skipped, failed[], bytes_total, bytes_transferred, duration_s}. For text-only edits prefer remote_patch (hash-protected). Use remote_transfer when SFTP semantics are needed: binary files, large files, whole directory trees. Note: directory modes copy files only — symlinks and special files are skipped, and empty directories are not created on their own. |
| remote_tunnel | Manage SSH tunnels — a single entry point (like hosts) where
Actions
Tunnels are a resource you manage explicitly (open → close), so |
| policy_checkA | Dry-run a host (and optional command) through the security policy.
Returns "ALLOWED" or "BLOCKED: ". Does not execute anything. Use this before risky multi-host operations to surface policy errors early. Being a dry-run, it does NOT consume a rate-limit token (a pre-flight check never throttles the real operation it is checking for). ⚠️ Default policy is PERMISSIVE — out of the box |
| inspect | Inspect MCP server internal state and the audit log — the read-only introspection hub for plumbing (the connection pool, persistent bash sessions) and history (audit log, stats, policy). Note the "resource vs plumbing" split: things the agent manages explicitly (registered hosts, open tunnels) are listed by their own resource tools — hosts(action="list") and remote_tunnel(action="list") — NOT here. inspect only surfaces server-internal plumbing the agent never explicitly creates. Views
Read-only. Used to introspect what the MCP server has been doing and what limits are in place. |
| remote_read | Read a file (or a 1-based line range) from a remote host with SHA-256 hashes. Returns JSON with: content, file_hash, range_hash, start, end, total_lines, truncated. The file_hash MUST be supplied to remote_patch; if the file changed in between, remote_patch will refuse to overwrite. The read is paged so a large file never has to come back as one
oversized blob: a single call returns at most Usage:
* Whole file (auto-paged): call Args:
host: SSH host alias (from ~/.ssh/config) or registered host name
path: Absolute remote path
start: 1-based starting line (default 1)
end: 1-based ending line of the requested range, inclusive
(default: end of file)
limit: max number of lines to return in this page (default: the
PORTAL_READ_MAX_LINES cap). The byte cap may shorten the page further.
encoding: Text encoding (default utf-8)
use_sudo: read via |
| remote_patch | Apply patches to a remote file with hash-based conflict detection. Workflow:
patches_json must decode to a list of patch objects: [{"start": int, "end": int|null, "contents": str, "range_hash": str}, ...] Notes:
|
| remote_grep | Search file contents with a regex on a remote host (ripgrep, fallback
grep). Prefer this over running raw Args:
host: SSH host alias / registered name.
pattern: the regex to search for (rg/PCRE-ish syntax).
path: file or directory to search under (default: cwd "."). Result
paths are returned relative to it.
glob: filter files by a glob, e.g. ".py" or "!.test.ts".
file_type: rg --type filter, e.g. "py", "rust", "js".
output_mode:
- "files_with_matches" (default): just the matching file paths,
NEWEST FIRST. Cheapest; use it to locate, then re-grep with
output_mode="content" on the file you care about.
- "content": matching lines as {file, line, text} (context lines
carry "context": true). Respects |
| remote_glob | Find files by a glob pattern on a remote host, newest first.
Prefer this over running raw Args: host: SSH host alias / registered name. pattern: a glob, e.g. "/*.py", "src//.{ts,tsx}", ".toml". path: directory to search under (default: cwd "."). Returned filenames are relative to it. Returns {filenames:[…newest first], num_files, truncated, duration_ms}.
Unlike remote_grep this does NOT respect |
| remote_shell | Run a command (or a sequence) on ONE remote host in a persistent shell session: cwd and environment (cd / export / venv activation) survive across calls. Use it only when you need that continuity — otherwise remote_exec is faster (no session setup) and can target many hosts; for a long task to background and poll, use remote_job. Pick one:
Behavior:
★ No sudo or secret injection here (both are one-shot by nature): to run as root use remote_exec(use_sudo=True); for a command needing a secret use remote_exec(secrets=[…]). timeout (per command, seconds; REQUIRED — no default): the call is held open until the command exits or timeout elapses. Keepalive pings stop the client aborting a hung call, so timeout is your real cut-off — pass a SMALL one for an exploratory / re-runnable command (e.g. 10–30) to fail fast, raising it only for a genuinely slow but bounded command. Hard ceiling (PORTAL_MAX_TIMEOUT, default 300s): a request above it is refused — use remote_job for a long task. ⚠️ By convention, write operations should target /tmp/ on the remote unless the user approved another path (not enforced here). |
| remote_exec | Run a shell command on one or more remote hosts over SSH and get the
result immediately (exit code + SEPARATE stdout/stderr). This is the default
way to execute ANYTHING on a remote machine — reach for it instead of
hand-rolling ★ sudo: to run a command as root, call this with use_sudo=True — NEVER put a
bare ★ credentials: when a command needs a secret (API token, deploy key, …), do
NOT have the user paste it into the chat. They run
Targets (pick one): host="web01" | host=["web01","web02"] | group_tag="prod" (all registered hosts carrying that tag). Commands (pick one): command="uptime" — ONE command per call is the DEFAULT
and preferred shape. Run it, read the real output, check it against what you
expected (an exit-0 step can still be wrong), THEN decide the next call —
that keeps you in the loop and able to course-correct. commands=["a","b"]
batches a sequence into a SINGLE call (each with its own exit code; stops at
the first failure when stop_on_error=True, and with use_sudo each entry runs
as its own sudo command) — reach for it ONLY for a fixed, independent
sequence you do NOT need to inspect between steps. Don't bury a long or
branching pipeline in one Multi-host fan-out is parallel by default; serialize=True (+ delay_s, stop_on_error) does a rolling, stop-on-first-failure rollout. timeout (seconds, REQUIRED — no default; consciously choose a cut-off): the call is held open until the command finishes or timeout elapses. Keepalive pings stop the client aborting a hung call, so timeout is your real cut-off — pass a SMALL one for an exploratory / re-runnable command (e.g. 10–30) to fail fast, raising it only for a genuinely slow but bounded command. Hard ceiling (PORTAL_MAX_TIMEOUT, default 300s): a request above it is REFUSED — background a long task with remote_job instead of pinning it on a blocking call. login (default on): run the command in a LOGIN shell (bash -lc) so the
remote user's ~/.profile / ~/.bashrc PATH + env (conda / nvm / pyenv,
~/.local/bin, …) is loaded; pass login=False for a bare non-login shell.
Silently degrades to a plain exec on a host without bash. Affects only this
plain path (not sudo / secrets); operators set the default via
PORTAL_LOGIN_SHELL or a per-host use_sudo: run via secrets: a list of NAMES, not values (e.g. ["github_token"]); each is resolved server-side, fed over SSH stdin (never on argv/audit), exported as its uppercased env var ($GITHUB_TOKEN), and redacted to *** in the output. May be combined with use_sudo (values are read+exported inside the sudo'd shell, so sudo's env_reset can't strip them). Returns one dict {host, command, exit_code, stdout, stderr, elapsed_s} for a single host + command, else a JSON list (a multi-command host carries {host, results:[…]}). ★ When use_sudo or secrets is used the result is flagged "high_risk": briefly tell the user you ran a privileged / credentialed command with their stored sudo password / secret, or only do so with their explicit prior permission. |
| local_execA | Run a command on the MCP SERVER's OWN machine (local), NOT over SSH — for anything on a remote host use remote_exec instead. This departs from the project's core goal (driving remote hosts as if local); it's a useful but off-target derivative, so it is OFF by default: the operator must explicitly set PORTAL_ALLOW_LOCAL_EXEC=1 in the server process's env to enable it. ★ credentials: if the command needs a secret, don't have the user paste it
into the chat — they run ★ use_sudo: run the command under sudo on THIS machine (local_exec). The
sudo password is resolved out-of-band (NEVER an argument): the per-user
credential agent populated by timeout (seconds, REQUIRED — no default): held open until the command exits or timeout elapses. Keepalive pings stop the client aborting a hung call, so pass a SMALL one for an exploratory / re-runnable command (e.g. 10–30) to fail fast, raising it only for a genuinely slow but bounded command. Hard ceiling (PORTAL_MAX_TIMEOUT, default 300s): a request above it is refused — local_exec has no background runner, so split the work into shorter steps. ★ secrets flags the result "high_risk": briefly tell the user you ran a local command with their stored credential, or only do so with prior permission. |
| remote_closeA | Close the cached persistent bash session for (the next remote_shell call reopens a fresh one). Rarely needed: the session is created/reused/auto-recreated implicitly by remote_shell — you don't manage its lifecycle. Use this only to reset a session whose state has gotten dirty. |
| remote_jobA | Run a command in the background and get a job_id back immediately, so you can keep thinking while it runs, poll for incremental output, and cancel it. Use this for long tasks; for a command that finishes quickly just use remote_exec (it waits and returns the result). Actions
Limits (L1): job_ids are best-effort persisted across a server restart
(the table reloads from /jobs.json and a poll re-probes the remote
PID); set PORTAL_JOB_PERSIST=0 to disable. It's not a durable queue — a
crash mid-write loses the view, but the remote process keeps running and is
recoverable via Manual fallback (no remote_job): you can always background a command yourself with remote_exec(command="nohup mycmd >/tmp/x.log 2>&1 & echo $!") and poll the log with remote_exec(command="tail /tmp/x.log"). |
Prompts
Interactive templates invoked by user choice
| Name | Description |
|---|---|
No prompts | |
Resources
Contextual data attached and managed by the client
| Name | Description |
|---|---|
No resources | |
Latest Blog Posts
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/TMYTiMidlY/portal-mcp-server'
If you have feedback or need assistance with the MCP directory API, please join our Discord server