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 |
|---|---|
| portal_hostA | 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 |
| portal_transferA | 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 portal_patch (hash-protected). Use portal_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. |
| portal_tunnelA | Manage SSH tunnels — a single entry point (like portal_host) where
Actions
Tunnels are a resource you manage explicitly (open → close), so |
| portal_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 |
| portal_auditA | 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 — portal_host(action="list") and portal_tunnel(action="list") — NOT here. portal_audit 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. |
| portal_readA | 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 portal_patch; if the file changed in between, portal_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) |
| portal_patchA | 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:
|
| portal_grepA | 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 |
| portal_globA | 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 portal_grep this does NOT respect |
| portal_shellA | 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 portal_exec is faster (no session setup) and can target many hosts; for a long task to background and poll, use portal_job. Pick one:
Behavior:
★ No sudo or secret injection here (both are one-shot by nature): to run as root use portal_exec(use_sudo=True); for a command needing a secret use portal_exec(secrets=[…]). timeout (per command, seconds; default 1h, operator-lowerable via PORTAL_DEFAULT_TIMEOUT): 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 — for an exploratory / re-runnable command pass a SMALL one first (e.g. 10–30) to fail fast, raising it only for genuinely slow commands. ⚠️ By convention, write operations should target /tmp/ on the remote unless the user approved another path (not enforced here). |
| portal_execA | 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" (a single command; a multi-line string runs as one bash script) | commands=["apt update","apt upgrade"] (a sequence, each with its own exit code, stopping at the first failure when stop_on_error=True). Prefer the commands=[…] array over packing several lines into one string — it can't be silently flattened and you get per-step exit codes (matters most with use_sudo, where each entry runs as its own sudo command). 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, default 1h, operator-lowerable via PORTAL_DEFAULT_TIMEOUT): 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 — for an exploratory / re-runnable command pass a SMALL one first (e.g. 10–30) to fail fast, raising it only for commands you know are slow; keep it generous for ones whose mid-flight kill is dangerous (migrations, package upgrades). 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. Cannot be combined with use_sudo. 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. |
| portal_local_execA | Run a command on the MCP SERVER's OWN machine (local), NOT over SSH — for anything on a remote host use portal_exec instead. Off by default (local execution is a larger threat surface): the operator must 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 No sudo here — for a privileged command, sudo is remote-only via portal_exec(use_sudo=True). timeout (seconds, default 1h, operator-lowerable via PORTAL_DEFAULT_TIMEOUT): held open until the command exits or timeout elapses. Keepalive pings stop the client aborting a hung call, so for an exploratory / re-runnable command pass a SMALL one first (e.g. 10–30) to fail fast, raising it only for slow commands. ★ 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. |
| portal_close_shellA | Close the cached persistent bash session for (the next portal_shell call reopens a fresh one). Rarely needed: the session is created/reused/auto-recreated implicitly by portal_shell — you don't manage its lifecycle. Use this only to reset a session whose state has gotten dirty. |
| portal_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 portal_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 portal_job): you can always background a command yourself with portal_exec(command="nohup mycmd >/tmp/x.log 2>&1 & echo $!") and poll the log with portal_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