portal-mcp-server
The portal-mcp-server is an SSH orchestration MCP server that enables AI coding agents to operate remote machines securely and efficiently, with persistent sessions, hash-protected file editing, secure credential handling, and multi-host orchestration.
Remote Command Execution
portal_exec: Run commands on one or many remote hosts simultaneously (parallel or rolling), with separate stdout/stderr, exit codes, sudo support, and secret injectionportal_shell: Run commands in a persistent shell session per host wherecd,export, and venv activations persist across callsportal_job: Submit long-running commands to the background and get a job ID immediately; poll for incremental output, cancel, or list all jobs — jobs survive SSH disconnectsportal_local_exec: Run commands on the MCP server's own machine (disabled by default; requiresPORTAL_ALLOW_LOCAL_EXEC=1)portal_close_shell: Reset a dirty persistent shell session for a host
Hash-Protected Remote File Editing
portal_read: Read a remote file (or line range) with SHA-256 hash; supports pagination for large filesportal_patch: Apply line-range patches with hash-based conflict detection — rejects writes if the file changed since last read; uses atomic rename
Remote File Search
portal_grep: Search file contents with regex (ripgrep with grep fallback); returns structured JSON with file paths, matching lines, and contextportal_glob: Find files by glob pattern on a remote host, sorted newest-first, capped at 100 results
File Transfer
portal_transfer: Transfer files/directories via SFTP; supports upload, download, directory sync/mirror, and batch file-pair lists with incremental skipping, resume on interrupted uploads, and progress reporting
SSH Tunnel Management
portal_tunnel: Open, close, and list SSH tunnels — local port forwarding, reverse tunnels, and SOCKS5 proxies — with managed lifecycle and explicit tunnel IDs
Host Registry Management
portal_host: Register, remove, and list SSH hosts; auto-resolves~/.ssh/configaliases; supports tag-based grouping for multi-host fan-out
Security & Policy
portal_check: Dry-run a host or command through the security policy (allowlist/blocklist/rate-limit) without executing anythingportal_audit: Inspect server internals — connection pool, persistent sessions, audit log history, operation stats, and loaded security policy
Key Security Features
Credentials (SSH passwords, key passphrases, sudo passwords, API tokens) are never exposed to the LLM — stored in a per-user credential agent with TTL and resolved server-side
All state-changing operations pass through a security gate and are written to an audit log (
audit.jsonl)Secrets are injected as environment variables and redacted from output
Allows using 1Password CLI (op) as a source for sudo passwords, enabling automated privilege escalation without exposing credentials to the LLM or process arguments.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@portal-mcp-serverStart a persistent bash session on the production server"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
portal-mcp-server
让 coding agent 通过 MCP 操作远端机器
SSH 命令、持久 shell、文件编辑与传输、隧道、多机与后台任务。
简体中文 | English
简介
Portal 让支持 MCP 的 coding agent 通过 SSH 操作远端机器:运行命令、编辑文件、传输数据、建立隧道,以及管理多台主机上的任务。服务端基于 AsyncSSH 和 FastMCP,可在 Linux、macOS、Windows 上运行。
日常使用分为两部分:MCP server 提供给 agent 调用的工具,portal CLI 供你在终端配置凭据。 主机可以直接沿用 ~/.ssh/config 中的别名;需要分组、密码来源或额外配置时,再使用 hosts.yaml。
Related MCP server: mcp-remote-ssh
项目特色
连接复用:命令、SFTP 和隧道共用服务端连接池,减少重复 SSH 握手;不依赖 OpenSSH 的
ControlMaster。可保留状态的 shell:
remote_shell跨调用保留工作目录和环境,remote_exec处理一次性命令,remote_job管理远端后台任务。带冲突检查的文件编辑:先读取文件与 SHA-256,再校验整文件和修改范围的 hash,通过 SFTP 临时文件和原子替换完成写入。
结构化操作:文件搜索、增量传输、多机并发与滚动执行、SSH 隧道均有对应工具和返回结构。
凭据与对话分开:复用 ssh-agent,或通过 CLI 无回显输入 SSH 密码、私钥口令、sudo 密码和 token;工具调用只传主机名或 secret 名。
策略与审计:主机和命令策略、速率限制、审计日志,以及可选的 cc-safety-net 检查。
下面按安装、接入、主机、凭据和工具的顺序介绍。完整签名、调优参数与设计说明放在折叠区,首次使用可以先跳过。
安装与快速上手
需要 Python 3.10+ 和支持 MCP 的客户端。推荐用 uv 安装:
uv tool install portal-mcp-server
portal-mcp-server --help安装后会得到两个等价的命令:portal-mcp-server 和 portal。不带子命令时启动 MCP server;带 ssh、passphrase、sudo、secret 或 agent 子命令时执行 CLI 操作。日常手动输入凭据时用短名 portal 即可。
1. 准备一台主机
如果已有可用的 SSH 别名,可以直接沿用。否则在 ~/.ssh/config 添加:
Host web01
HostName server.example.com
User deploy
IdentityFile ~/.ssh/id_ed25519在终端确认地址、账号和认证方式正确:
ssh web01私钥有口令时,先按 ssh-agent 配置 解锁。使用 SSH 密码登录时,可以通过 portal ssh set web01 无回显输入密码,详见 密码登录。
2. 接入 MCP 客户端
以 Claude Code 为例:
claude mcp add --scope user portal -- portal-mcp-server其他客户端的 JSON、TOML 与配置文件位置见 接入方式。如果客户端没有继承终端的 agent 环境,在 MCP 配置的 env 中显式传入 SSH_AUTH_SOCK,或在 SSH 配置里指定 IdentityAgent。
3. 开始使用
在对话中提出一个具体任务,例如:
查看 web01 上 /var/log/syslog 的最后 50 行。
agent 可以调用 remote_exec("web01", "tail -50 /var/log/syslog", timeout=30)。也可以先调用 hosts(action="list") 核对主机来源和配置告警。
不安装到 PATH 也能试用:
uvx portal-mcp-server@latest --help对应 MCP 配置使用 "command": "uvx" 和 "args": ["portal-mcp-server@latest"]。这不会把 portal 短命令安装到 PATH;经常需要设置凭据时,推荐 uv tool install。
更新已安装版本:
uv tool upgrade portal-mcp-server更新后重启 MCP server,使运行中的进程使用新版本。
SpatiumPortae/portal 文件传输工具也使用 portal 这个命令名。若安装了两者,用 which -a portal(Windows 用 where portal)检查 PATH 顺序,或直接使用全名 portal-mcp-server。
接入方式
不同客户端的配置格式和环境继承规则各有差异。以下客户端说明按需展开;agent 入口的优先级见 SSH 密钥与 ssh-agent。
portal-mcp-server 是一个本地 stdio MCP server,所有支持 MCP 的 host 都能接入。下面给常见 host 的最小配置,命令示例默认用装好的 portal-mcp-server(先 uv tool install portal-mcp-server);零安装就把 command 换成 uvx + portal-mcp-server@latest(上方一键 badge 走的正是这个零安装形式)。
如果 MCP client 找不到
portal-mcp-server(或uvx)——常见于 GUI app(Claude Desktop / VS Code)不继承你 shell 的 PATH——用which portal-mcp-server(Windows 用where portal-mcp-server)查绝对路径填进command。uv tool装的这个路径(~/.local/bin/portal-mcp-server)跨uv tool upgrade不变,写死也稳。
通用配置片段
大多数 host 都接受
{ "mcpServers": { "<name>": { "command": ..., "args": [...] } } }这种顶层 schema;VS Code 和 Codex 用各自专有 schema,单独列出。
已安装(推荐,command 就是短二进制名):
{
"mcpServers": {
"portal": {
"command": "portal-mcp-server",
"args": []
}
}
}零安装(不装,走 uvx 现拉现跑):
{
"mcpServers": {
"portal": {
"command": "uvx",
"args": ["portal-mcp-server@latest"]
}
}
}需要传入 agent socket 或自定义配置路径时,在 server 条目中追加 env(UID 和路径均为示例,按实际环境替换;只保留需要的字段):
"env": {
"SSH_AUTH_SOCK": "/run/user/1000/ssh-agent.socket",
"PORTAL_HOSTS_YAML": "/path/to/hosts.yaml",
"PORTAL_POLICIES_YAML": "/path/to/policies.yaml",
"PORTAL_LOG_DIR": "/path/to/logs"
}Claude Code CLI
直接编辑 <project>/.mcp.json(同上 schema),或用 CLI / 斜杠命令登记:
# 推荐:user 级,对所有 repo 生效(已 uv tool install,command 用短名 portal-mcp-server)
claude mcp add --scope user portal -- portal-mcp-server
# 不加 --scope 默认是 local,只在「当前目录」生效,换个目录 claude mcp list 就看不到
claude mcp add portal -- portal-mcp-server
# 零安装:把 portal-mcp-server 换成 uvx portal-mcp-server@latest
# 或在 Claude Code 会话内输入 /mcp 检查和管理连接⚠️ Claude Code 有三档 scope:
local(默认,仅当前目录)、user(所有 repo)、project(写进 repo 的.mcp.json,随仓库共享)。要「装一次处处可用」务必带--scope user——这点和 Codex(mcp add即 global)/ Copilot CLI(mcp add即 User 级)不一样,最易踩坑。
写 <project>/.mcp.json 即在该项目内生效;或一行命令登记到 user 级(对所有项目生效):
copilot mcp add portal -- portal-mcp-server
# 零安装:把 portal-mcp-server 换成 uvx portal-mcp-server@latest
# 或在 Copilot CLI 会话内输入 /mcp 走交互登记验证:
copilot mcp list # 应看到 portal
copilot mcp get portal # 检查 Source 是 Workspace / User点上方 「Install in Cursor」badge 即可一键安装;或手动把通用片段写进 ~/.cursor/mcp.json(全局生效)或 <project>/.cursor/mcp.json(仅当前项目)。Cursor → Settings → Tools & MCP 里能看到 portal 并启用。
工作区可以在项目根目录使用前文的 .mcp.json / mcpServers 通用格式。也支持 .vscode/mcp.json 的 VS Code 原生格式:
{
"servers": {
"portal": {
"type": "stdio",
"command": "portal-mcp-server",
"args": []
}
}
}跨工作区配置可通过命令面板的 MCP: Open User Configuration 打开;新增服务也可使用 MCP: Add Server。两种文件格式的顶层字段不同,按所选位置使用相应格式。配置位置和当前支持情况见 VS Code 官方文档。
把通用片段贴到 claude_desktop_config.json 的 mcpServers 下,重启 Claude Desktop。配置文件位置:
macOS:
~/Library/Application Support/Claude/claude_desktop_config.jsonWindows:
%APPDATA%\Claude\claude_desktop_config.json
Windsurf 用同一份 mcpServers schema。在 Cascade 面板点插件按钮 → 「Manually configure MCP」,把通用片段写进 ~/.codeium/windsurf/mcp_config.json,回 Cascade 启用即可。
新版 Codex 直接一行命令登记(global,对所有目录生效):
codex mcp add portal -- portal-mcp-server
# 零安装:把 portal-mcp-server 换成 uvx portal-mcp-server@latest
codex mcp list # 应看到 portal也可以编辑 ~/.codex/config.toml:
[mcp_servers.portal]
command = "portal-mcp-server"
args = []
# 零安装:command = "uvx",args = ["portal-mcp-server@latest"]
# 可选:显式传入 agent socket;换成实际路径
[mcp_servers.portal.env]
SSH_AUTH_SOCK = "/run/user/1000/ssh-agent.socket"启动 Codex 后在 TUI 输入 /mcp 确认 portal 已加载。
Cline / Continue / Roo Code 等 VS Code 插件:通常都接受
{ "mcpServers": ... }通用片段,写到各自插件的 MCP 设置面板或工作区配置即可任意 MCP 兼容 host:把通用片段贴到该 host 的 MCP 配置入口;stdio 不需要额外代理
主机配置
最简单的方式是直接使用 ~/.ssh/config 中的 Host web01 别名。Portal 复用 AsyncSSH 的解析器,支持 Include、HostName、User、Port、IdentityFile、IdentityAgent、ProxyJump 等配置。
需要 Portal 的分组标签、密码命令或主机级设置时,创建 ~/.config/portal-mcp-server/hosts.yaml:
hosts:
web01:
use_ssh_config: true
tags: [web, prod]
# user: deploy # 可选,覆盖 SSH 配置中的 User
# sudo_password_command: pass show sudo/web01use_ssh_config: true 表示以同名 SSH 别名为基础,再叠加 YAML 中显式设置的字段。省略 host 可以继承 HostName;如果填写,必须与别名解析出的 HostName 一致,否则拒绝连接。
也可以完全在 YAML 中定义主机:
hosts:
web02:
host: server2.example.com
user: deploy
port: 22
key: ~/.ssh/id_ed25519
tags: [web]查找优先级:运行时注册表 / hosts.yaml → SSH 配置别名。 同名 YAML 主机若没有 use_ssh_config: true,不会以该别名合并 SSH 配置;需要继承别名的 IdentityAgent、IdentityFile 或 ProxyJump 时,应明确启用合并。
hosts(action="list") 同时列出配置主机和 SSH 别名,返回 source 与告警。hosts(action="register", name="web01") 只给名字时,会从已有 SSH 别名创建合并配置;运行时登记的主机只存在于当前 MCP server 的注册表中。
PORTAL_SSH_CONFIG 控制 Portal 的 SSH 配置来源:
设置 | 读取范围 |
不设置 | 用户 |
绝对路径 | 只读该文件,抑制系统配置,类似 |
| 禁用 Portal 的 SSH 配置文件查找 |
其他相对路径 | 告警并忽略 |
list 的 source 包括 hosts.yaml、runtime、ssh-config;启用合并时为 hosts.yaml+ssh-config 或 runtime+ssh-config。别名枚举跟随 Include,排除含 *、?、! 的模式。
常用 YAML 字段包括 proxy_jump、keepalive_interval、forward_agent、use_ssh_agent 和 login_shell。未显式设置的合并字段沿用 SSH 配置。proxy_jump: none 强制直连;空字符串或 null 不表示直连,应删除字段或使用 none。
跳板机需要自己的密钥时,优先定义其 SSH 别名并启用合并,把跳板密钥加入 ssh-agent。裸 proxy_jump: user@jump 的默认身份与口令继承有额外限制,见 连接排障 和 ADR-0002。
完整字段模板见 examples/hosts.yaml,配置路径规则见 文件路径。
认证
先区分两种本地服务:ssh-agent 保存已加载的 SSH 密钥并提供签名;Portal 凭据 agent 保存 CLI 输入的密码、私钥口令与 secret。 它们使用不同的 socket,互不替代。
需要提供什么 | 推荐入口 | 使用位置 |
SSH 密钥 |
| 通过 ssh-agent 完成 SSH 签名 |
SSH 登录密码 |
| 建立 SSH 连接 |
加密私钥的口令 |
| 在本机解锁私钥;通常优先用 ssh-agent |
远端 sudo 密码 |
|
|
本机 sudo 密码 |
|
|
API token 等 secret |
|
|
portal … set 在你自己的终端中无回显输入,默认缓存 900 秒,可用 --ttl 调整。不要把凭据粘贴到 agent 对话或 MCP 工具参数中。无人值守场景也可以配置密码管理器命令,见下文各节。
SSH 密钥与 ssh-agent
已有可用密钥时无需新建。需要创建和安装公钥时,例如:
ssh-keygen -t ed25519 -C "you@example.com"
ssh-copy-id -i ~/.ssh/id_ed25519.pub deploy@server.example.comssh-agent 启动时不一定有密钥;用 ssh-add 加入,输入一次私钥口令:
# 仅在没有可用 agent 时启动;已有系统/桌面 agent 时沿用其入口
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519
ssh-add -lPortal 通过 AsyncSSH 选择 agent 入口,不会扫描机器上所有 agent socket,也不会自动替你解锁私钥:
连接读取的 SSH 配置中有
IdentityAgent时,使用该入口。否则读取 Portal 进程环境中的
SSH_AUTH_SOCK。这个变量可以由 MCP 客户端从父进程环境传入,也可以在 MCP 配置的env中显式指定。没有可用 agent 时,是否能连接取决于可读取的密钥文件、私钥口令或其他已配置认证来源。
例如,Linux 上一个已经启动的用户级 agent 固定监听 /run/user/1000/ssh-agent.socket,可在 SSH 配置中写:
Host *
IdentityAgent /run/user/%i/ssh-agent.socket
AddKeysToAgent yes%i 表示本地 UID。这里的 socket 必须真实存在,路径应与实际 agent 一致。 IdentityAgent 覆盖 SSH_AUTH_SOCK;它不会设置环境变量,也不会启动 agent。AddKeysToAgent 是 OpenSSH 客户端的密钥缓存选项,不能让 Portal 自动解锁或加载密钥。ssh-add 不读取 IdentityAgent,仍需正确的 SSH_AUTH_SOCK。
也可以直接为 MCP server 传入入口:
{
"mcpServers": {
"portal": {
"command": "portal-mcp-server",
"args": [],
"env": {
"SSH_AUTH_SOCK": "/run/user/1000/ssh-agent.socket"
}
}
}
}示例中的 UID 1000 需要换成实际值,或使用已有 agent 提供的路径。MCP 客户端可能筛选继承的环境,GUI 客户端也未必继承终端环境;显式配置 env 能避免这一差异。修改启动环境后,重新启动 MCP server。在另一个终端后来执行 export,不会更新已运行 Portal 的环境。
密钥可以复用到被移除或 agent 重启;它的缓存寿命与 OpenSSH ControlPersist、Portal 连接池的连接寿命各自独立。agent socket 必须对运行 Portal 的用户可访问。
先检查 command -v ssh-agent 和 systemctl --user cat ssh-agent.service。如果已有合适的服务,沿用它的 socket;没有时,可以创建 ~/.config/systemd/user/ssh-agent.service:
[Unit]
Description=OpenSSH authentication agent
Documentation=man:ssh-agent(1)
[Service]
Type=simple
Environment=SSH_AUTH_SOCK=%t/ssh-agent.socket
ExecStart=/usr/bin/ssh-agent -D -a $SSH_AUTH_SOCK
UMask=0077
[Install]
WantedBy=default.target根据 command -v 的结果调整程序路径,然后加载、启用并解锁:
systemctl --user daemon-reload
systemctl --user enable --now ssh-agent.service
export SSH_AUTH_SOCK="/run/user/$(id -u)/ssh-agent.socket"
ssh-add ~/.ssh/id_ed25519systemd 的 %t 是用户运行时目录,与 SSH 配置的 %i 含义不同。若需要新终端自动找到 socket,将相同的 export 写入自己的 shell 启动配置,或在 MCP 配置里显式设置。需要登出后或开机无人登录时运行 user service,可按系统策略启用 loginctl enable-linger;agent 重启仍需要重新加入密钥。
macOS 可沿用登录会话的 agent;Windows OpenSSH 使用其 ssh-agent 服务和命名管道,无需照抄 Linux socket 路径。
hosts.yaml 的 use_ssh_agent 可控制密钥路径:
值 | 行为 |
省略 | 保留 AsyncSSH 默认选择:可用文件身份与 agent 身份均可参与认证 |
| Portal 不显式传入 |
| 禁用 agent,使用文件身份与配置的私钥口令 |
SSH 密码登录
先确保 web01 已在 SSH 配置或 hosts.yaml 中定义,然后在自己的终端执行:
portal ssh set web01
# 或指定缓存时间(秒)
portal ssh set web01 --ttl 1800首次 set 会按需安装并启动 Portal 凭据 agent。密码按主机别名缓存;CLI 设置主机凭据时请使用与 MCP 工具相同的名字。
默认先尝试密钥认证;失败为 PermissionDenied 且存在已缓存密码或 password_command 时,再尝试密码认证。没有密码来源时保留原始认证错误。要明确采用密码登录路径,可配置:
hosts:
web01:
host: server.example.com
user: deploy
auth: password密码来源的顺序是 CLI 凭据缓存 → password_command → 报错。自动化场景可以从密码管理器读取:
hosts:
web01:
host: server.example.com
user: deploy
auth: password
password_command: pass show ssh/web01也可使用 op、bw、secret-tool 等能输出凭据的命令。不要写 password: 明文:该字段会被忽略并产生告警。命令来源有 10 秒超时,非零退出、空输出或非 UTF-8 输出均会失败;stderr 不会回传为凭据错误内容。完整约束见 SECURITY.md。
私钥口令:portal passphrase set
如果不使用 ssh-agent,可以向 Portal 提供本机私钥的解锁口令:
portal passphrase set web01或在该主机配置 passphrase_command: pass show ssh/web01-passphrase。Portal 优先读取凭据缓存,再调用 passphrase_command,将口令交给 AsyncSSH 解锁文件。私钥口令、SSH 登录密码和 sudo 密码分别缓存;use_ssh_agent: true 路径跳过 Portal 的私钥口令解析。
sudo:portal sudo set
portal sudo set web01然后由 agent 调用 remote_exec("web01", "id", timeout=30, use_sudo=True),或支持 sudo 的 remote_read / remote_patch。密码来源是 CLI 凭据缓存 → sudo_password_command → 报错:
hosts:
web01:
use_ssh_config: true
sudo_password_command: pass show sudo/web01若 SSH 登录密码与 sudo 密码确实相同,可以显式设置 sudo_password_same_as_ssh: true。此时 portal ssh set web01 会同时写入两种缓存;默认不启用,也不会把私钥口令当作 sudo 密码。
sudo 操作使用 sudo -S -k,通过 stdin 输入密码。它不继承 remote_shell 之前保留的工作目录与环境;需要时在本次命令里写明。交互式 sudo 提示不适合 MCP 工具,确需交互时在自己的 SSH 终端操作。
local_exec 默认关闭,需要 PORTAL_ALLOW_LOCAL_EXEC=1。本机 sudo 使用保留身份 <local>:
portal sudo set-local也可以在 hosts.yaml 顶层配置 "<local>": {sudo_password_command: ...}。这与通过 SSH 连接的 localhost 别名不同。
sudo 与 secrets 同用时,密码和值依次走 stdin。提权后的 shell 在 sudo 的 env_reset 之后读入并 export secret,因此不需要 sudoers env_keep;命令本体的 stdin 也会受到这条传输路径限制。实现见 secrets_store.py 的 sudo_stdin_secret_script()。
secret:portal secret set
在终端输入 token:
portal secret set github_tokenagent 只传名称,例如 remote_exec("web01", "gh auth status", timeout=30, secrets=["github_token"])。服务端将值注入该次命令的 GITHUB_TOKEN 环境变量;local_exec 支持同样的参数。名称默认转为大写,更多配置见 examples/secrets.yaml。
读取顺序是 CLI 凭据缓存 → secrets.yaml 的命令来源:
secrets:
github_token:
command: pass show api/githubsecrets 可与 use_sudo 同用,后台 remote_job 暂不支持这两项。返回给 agent 的命令输出会对已知 secret 值做替换;仍应避免主动打印凭据,完整限制见 安全。
Portal 凭据 agent 与日常管理
ssh、passphrase、sudo、secret 共用一套 CLI 操作:
portal ssh show web01 # 指纹与剩余 TTL,不显示明文
portal ssh list # 当前 kind 的缓存列表
portal ssh confirm web01 # 输入两次,比对一致后写入/更新缓存
portal ssh clear web01 # 移除这一条
portal agent status # 服务地址、状态与缓存统计
portal agent clear # 清空所有 kind 的缓存换成 passphrase、sudo 或 secret 即可管理其他种类。confirm 比较的是本次两次输入,不会读取旧缓存来核对密码是否正确,也不会验证远端登录。show / list 只展示指纹与有效期,没有明文导出命令。
主机类 set / confirm 会先检查别名,避免把密码存到拼错的名字下。CLI 看不到只在 MCP 运行时注册的主机时,可用 portal ssh set web01 --force 跳过检查;secret 与 sudo set-local 不需要此选项。
凭据存在服务内存中,默认 900 秒后过期,服务重启也会清空。MCP server 与 CLI 必须使用同一用户的配置和凭据服务。新安装的服务可在下一次凭据请求时被发现;修改 MCP 启动环境仍需重启 MCP server。
通常直接 portal … set 即可自动安装;显式管理命令是:
portal agent install --now
portal agent uninstall平台 | 用户级服务 | 本地通信 |
Linux | systemd | Unix socket |
macOS | launchd LaunchAgent,运行并保持服务 | Unix socket |
Windows | 当前用户登录计划任务,InteractiveToken 主体 | 命名管道 |
安装器把实际地址写入配置目录的 agent.json;PORTAL_CREDENTIAL_AGENT_SOCKET 可以覆盖它。Linux 默认 socket 为 %t/portal-mcp-server/credentials.sock,与 SSH agent 的 socket 不同。macOS 的 LaunchAgent 和 Windows 计划任务均以用户身份运行;Windows 不以 SYSTEM 运行,也不存登录密码,设置 ExecutionTimeLimit=PT0S 和失败重启策略。Linux socket 按同用户边界校验客户端;各平台访问控制见 SECURITY.md。
凭据只向执行认证或命令的授权本地进程提供,不通过人可见的 CLI 输出、MCP 参数或审计日志展示。CLI 输入的值不写入配置文件;密码管理器命令每次现取,不受这套缓存 TTL 限制。
配置凭据也意味着:能够调用此 MCP server 的 agent,在凭据有效期间可以使用相应能力。按任务选择有限的 TTL、主机/命令策略与 sudo 权限。凭据未就绪时,工具返回错误并引导你在另一个终端设置;agent 应等待你完成后重试,不索取对话中的明文。
工具列表
Portal 提供 14 个 MCP 工具。先按任务选择,再查折叠区中的完整签名。
执行命令
工具 | 适用任务 | 需要知道的行为 |
| 一次性命令;单机、多机并发或滚动执行 | 返回分离的 stdout/stderr 与退出码;支持 |
| 需要跨调用保留 | 每台主机一个持久 shell;PTY 输出合并 stdout/stderr |
| 长任务,或需要在 MCP server 关闭后继续运行 |
|
| 在 MCP server 所在机器执行命令 | 默认关闭,需 |
| 重置某台主机的持久 shell | 关闭会话,下次 |
remote_exec、remote_shell、local_exec 的 timeout 必填,单位为秒;默认上限 PORTAL_MAX_TIMEOUT=300。长任务使用 remote_job。执行期间会发 MCP progress 心跳,但客户端是否据此重置超时取决于客户端实现。
连接复用与 shell 状态是两回事。 所有 SSH 工具都可以共享连接池,只有 remote_shell 保留 shell 状态。remote_exec 普通路径默认使用登录 shell;sudo/secrets 路径的环境处理见 ADR-0004。
文件、隧道与管理
工具 | 用途 |
| 先读内容、整文件 hash 与范围 hash,再校验并原子修改;支持 sudo |
| 正则搜索内容;文件名、匹配内容或计数输出,支持分页和截断标记 |
| 按 glob 找文件;按修改时间排序,最多返回 100 个结果 |
| SFTP 上传、下载、目录增量同步或路径列表传输;上传支持断点续传与校验 |
| 管理 local / reverse / SOCKS 隧道: |
| 主机列表、登记与移除;查看来源和配置告警 |
| 不执行命令的策略预检; |
| 查看 server、连接池、shell 会话、历史、统计与策略 |
remote_grep 尊重 .gitignore,remote_glob 不尊重;传输目录不会跟随本地符号链接。remote_patch 成功后会清理同目录中超过一小时的孤立临时文件,具体边界见完整参考。
给 agent 的使用约定
建议写入自己的 AGENTS.md 或系统提示,按实际授权范围调整:
一次调用完成一个可判断的步骤,读取真实输出与退出码后再决定下一步。
commands=[…]用于不需要中间决策的固定批次。文件读取和修改使用
remote_read → remote_patch;hash 冲突后重新读取,不覆盖并发修改。搜索、传输、隧道和后台任务使用对应工具;多机执行用
remote_exec(host=[…])或group_tag。先核对主机别名、配置告警和授权范围;
/tmp/是建议的初始工作范围,用户已有授权优先。凭据由用户在另一个终端通过 CLI 输入;未就绪时说明具体命令,等待完成后重试,不要求在对话中提供明文。
修改 root 文件用
remote_read(use_sudo=True)/remote_patch(use_sudo=True)。sudo patch 保留 hash 校验、原子替换和原属主/权限,目标须已存在。要在 MCP server 停止后继续运行的工作使用
remote_job。普通前台调用与传输随进程结束而中断;上传可在重试时续传。
所有工具对大模型可见的完整签名(
ctx是 MCP 进度 / keepalive 上下文,由 FastMCP 在异步工具上注入,不出现在下面的 schema 里)。针对 host 的工具都收host,解析顺序 hosts.yaml / 运行时注册表 → OpenSSH 客户端 config(复用 asyncssh 的SSHClientConfig、跟Include),详见环境变量 → 文件路径。状态变更工具写audit.jsonl;只读工具(remote_read/remote_grep/remote_glob/policy_check/inspect及remote_tunnel/remote_job的读 action)刻意不审计。
跑命令:exec 家族
工具 | 签名 | 返回结构 / 关键行为 |
|
| 走连接池的无状态一次性。单 host + 单 command → 一个 dict(分离的 stdout/stderr + exit code);多 host / |
|
| 每 host 一个持久交互 shell。单 command → |
|
|
|
|
| 在 MCP server 自己机器上跑(不走 SSH),默认关闭,须 |
|
| 关掉某 host 缓存的 |
文件编辑(hash 保护)
工具 | 签名 | 返回结构 / 关键行为 |
|
| → |
|
| hash 保护的行范围 patch:文件自 |
远端搜索(忠实移植 Claude Code)
工具 | 签名 | 返回结构 / 关键行为 |
|
| 正则内容搜索( |
|
| 按 glob 找文件, |
文件传输(SFTP)
工具 | 签名 | 返回结构 / 关键行为 |
|
| 二进制安全、原子 SFTP。单文件模式( |
资源(agent 显式管理)
工具 | 签名 | 返回结构 / 关键行为 |
|
|
|
|
| 运行时 host 注册表。 |
内省 / 策略
工具 | 签名 | 返回结构 / 关键行为 |
|
| 过安全策略 dry-run,不执行 → |
|
| 只读内省 server plumbing + 历史。 |
凭据 CLI(非 MCP 工具):用户在终端用
portal {ssh,sudo,passphrase,secret} set输入值,凭据服务按需交给本地消费者;工具参数只引用名称。show/list返回指纹与 TTL,confirm比较本次两次输入后写入缓存。平台安装和访问边界见认证与凭据 agent。
源码位置
模块 | 负责的工具 / 职责 |
| 全部 |
| asyncssh 连接池 + host 注册表(SSH 工具共用; |
|
|
|
|
| 持久交互式 shell 会话(bash/zsh;cwd/env、exit code,OSC 133 边界协议、soft-cancel、超时中断) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 各类凭据解析 + 输出脱敏 |
| 凭据 agent 的同用户对端校验(Linux |
| 策略引擎:host allowlist、command blocklist/allowlist、per-host rate limit、cc-safety-net 接入 |
|
|
v4 起:所有工具去掉
portal_前缀——远程操作类加remote_(remote_exec/remote_shell/remote_read/remote_patch/remote_grep/remote_glob/remote_transfer/remote_tunnel/remote_job/remote_close),本机执行local_exec,控制面portal_host→hosts/portal_check→policy_check/portal_audit→inspect。客户端本就按 config key 命名空间化(portal-remote_exec),portal_前缀是冗余 stutter;详见 ADR-0001。下表另附更早的portal_bash时代迁移(合并 / 删除的工具):
旧 | 新 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 删除—— |
|
|
— | 新增 |
环境变量
Portal 自身的设置使用 PORTAL_* 前缀,在 MCP server 的 env 中传入。SSH_AUTH_SOCK 是额外沿用的 SSH 标准变量,IdentityAgent 来自 SSH 配置,详见 认证。
文件路径
下表是 Linux 默认路径;其他平台使用对应的用户配置/状态目录。可以用环境变量指定绝对路径:
变量 | 用途 | Linux 默认 |
| 主机配置 |
|
| 安全策略 |
|
| secret 命令来源 |
|
| SSH 配置读取范围 | 用户配置 + 系统 fallback;可用绝对路径或 |
| 审计与服务日志 |
|
| Portal 凭据服务地址 | 安装器写入的 |
配置路径优先级是 显式环境变量 → 平台用户目录;Linux 支持 XDG_CONFIG_HOME / XDG_STATE_HOME。当前工作目录和仓库的 examples/ 不会自动成为真实配置来源。SSH 文件范围的规则见 主机配置。
需要 YAML 配置时,从 examples/ 中选用相应模板,复制到用户配置目录后编辑。仅使用 SSH 别名时不必先创建三份 YAML。真实配置与凭据来源不应提交进 Git。
安全与认证
环境变量 | 含义 | 默认 |
| 设 | (unset) |
| 设 | (unset) |
| 设 | (unset) |
| HTTP transport( | (none) |
连接池
控制 asyncssh 进程内连接池的行为。默认值适合大多数场景,仅在高并发或特殊网络环境下需要调整。详细的池行为说明见 § 进程内连接池。
环境变量 | 含义 | 默认 |
| 每 host 最大 TCP 连接数。连接池满且所有连接都达到 channel 上限时,会复用最空闲的连接(带 warning) |
|
| 每条 TCP 上最大并发 channel 数(SFTP 会话、exec、tunnel 等共享)。超出后新建 TCP,直到 |
|
| 无活跃 channel 的连接空闲多久后自动关闭(秒)。注意 |
|
| 连接最大存活时间(秒),超龄且无活跃 channel 时关闭。防止防火墙 / NAT 静默断连 |
|
可靠性
环境变量 | 含义 | 默认 |
|
|
|
|
| 内置 |
|
|
|
Shell 会话
remote_shell 持久会话的时序旋钮,正常部署无需改。
环境变量 | 含义 | 默认 |
| 单命令输出内存上限(字节);超限丢弃头部并标 |
|
| 首次拉起持久会话(注入集成脚本 + 就绪标记)的超时(秒) |
|
| bootstrap 完成前的静默确认窗口(秒) |
|
| 侦测到交互提示后、判定卡死并 soft-cancel 前的宽限(秒) |
|
| soft-cancel(含前台超时中断)后等 OSC133 |
|
测试(仅 dev)
只在跑 tests/ 时用到,正常 MCP 部署不需要设置。详细测试用法见 § 测试。
环境变量 | 含义 | 默认 |
| 设 | (unset) |
| live 测试目标主机 |
|
| live 测试目标端口 |
|
| live 测试登录用户 |
|
| live 测试用的私钥路径 |
|
审计轮转、后台任务与读取上限
变量 | 用途 | 默认 |
| 审计日志轮转阈值 |
|
| 保留的轮转文件数量 |
|
| 任务表持久化; | 开 |
| 持久化路径;显式指定时使用固定文件 | 状态目录的 |
| 存活后台任务数量上限 |
|
| 完成任务保留时间,过期清理状态与远端临时文件 |
|
|
|
|
|
|
|
旧版本的变量前缀、工作目录配置与工具名迁移见 CHANGELOG.md 和 旧工具名迁移。
安全
Portal 的能力取决于运行 server 的用户权限、SSH 身份、已配置凭据和策略。建议先限制主机、命令与 sudo 范围,再按任务提供凭据。
策略检查:主机 allowlist、命令 allowlist/blocklist、速率限制与多机两阶段检查。可选
policies.safety_net.enabled接入 cc-safety-net;检查器不可用时默认拒绝操作。授权范围:先在远端
/tmp/工作是给 agent 的使用约定,不是文件系统强制沙箱。操作用户目录或项目源文件应遵循用户授权。凭据:CLI 输入不进入工具参数或配置文件,命令来源从密码管理器读取。能调用 server 的 agent 也能使用这些凭据对应的能力;TTL 与策略仍然重要。
HTTP:默认监听
127.0.0.1;绑定非 loopback 地址必须设置PORTAL_AUTH_TOKEN。服务提供 HTTP,需要公网访问时由前置代理终止 TLS。隧道与传输:隧道默认绑定 loopback,扩大暴露需要
PORTAL_ALLOW_TUNNEL_EXPOSURE=1;目录传输不跟随本地符号链接,但仍拥有 server 用户可访问的本地文件权限。审计:状态变更写
$PORTAL_LOG_DIR/audit.jsonl。日志写入发生在操作之后;失败默认让工具报错,不会回滚已经完成的远端修改。PORTAL_AUDIT_FAIL_OPEN=1可改为告警后继续。文件修改:hash 检查与原子替换用于检测冲突并减少中断造成的损坏,属于乐观并发控制,不能消除所有竞争窗口。普通 SFTP 写入不保证保留属主/权限,sudo patch 会显式保留。
已知 secret 值会从返回输出中替换,但转换、拆分、外传或意外记录仍需由命令与环境约束。远端非交互 bash 通常不记录历史;若管理员通过 BASH_ENV 等配置强制开启 history,stdin 注入的内容可能写入 ~/.bash_history。部署时检查实际远端环境,不依赖输出脱敏代替权限控制。
完整威胁模型、访问校验、审计边界和已知限制见 SECURITY.md。漏洞请通过 GitHub Security Advisories 私下披露;项目目标响应窗口为 48 小时确认、7 天初评、关键问题 30 天修复。
常见问题
终端 SSH 能连,Portal 却提示认证失败
终端可能已解锁私钥或复用 OpenSSH master;Portal 使用自己的 AsyncSSH 连接。先核对实际入口:
ssh -G web01
echo "$SSH_AUTH_SOCK"
ssh-add -l
# 核对固定 socket(路径换成实际值)
SSH_AUTH_SOCK=/run/user/1000/ssh-agent.socket ssh-add -l检查生效的 IdentityAgent 与 MCP server 的 env.SSH_AUTH_SOCK,再确认 agent 中有目标密钥。ssh-add 只看环境变量,它失败不代表读取 IdentityAgent 的客户端一定找不到 agent。若 hosts.yaml 覆盖了同名别名,确认是否需要 use_ssh_config: true。详见 主机配置 和 认证。
portal … set 显示未知主机,或设置后仍不可用
使用与 MCP 调用相同的主机别名,确认 CLI 与 MCP server 读取相同配置。只在运行时登记的主机可以用 --force;用 portal agent status 与 portal ssh show web01 检查服务与 TTL。修改启动环境后重启 MCP server;新安装凭据服务本身可以按需发现。
本地改动未在 agent 上生效
不管是 uv tool install 装的 portal-mcp-server 还是 uvx portal-mcp-server,跑的都是 PyPI 发布版,不是你的工作树——改了本地代码 agent 看不到。
你在哪改 | agent 的 MCP server 看得见吗 |
本地工作树 | ❌ 看不见(除非 editable 安装,见下) |
已发布到 PyPI 的新版本 | ✅ |
本地调试想让 agent 用上工作树的改动,两选一:
# 推荐:editable 安装,command 保持 portal-mcp-server 不变,源码改动即时生效
uv tool install --force --editable .或走 uvx,把 .mcp.json 里的 args 临时改成(路径必须绝对):
"args": ["--from", "/absolute/path/to/portal-mcp-server", "portal-mcp-server"]别把这条本地路径 commit 进项目级的 .mcp.json。
连接超时 / Permission denied (publickey)
确认
ssh user@host能在终端直连检查私钥权限:
chmod 600 ~/.ssh/id_ed25519如果用了
~/.ssh/config,确认Host别名、HostName、User、IdentityFile配正确跳板机(ProxyJump)场景:asyncssh 原生支持
~/.ssh/config的ProxyJump,确认跳板机也能手动 ssh 通。留意跳板凭据的边界:hosts.yaml 里裸写的proxy_jump: user@jump只用默认 key/agent 连跳板,而且会把为目标解出的 passphrase 拿去解本机上那把登录跳板的私钥(跳板 key 若加密且 passphrase 不同 →Incorrect passphrase);它不读跳板专属的IdentityFile。要让跳板用它自己的 key/passphrase:开use_ssh_config: true(asyncssh 会读跳板Host段的IdentityFile),并把跳板 key 加进 ssh-agent(agent 认证不吃 passphrase,冲突自然消失)。想强制直连(忽略 ssh config 的ProxyJump)写proxy_jump: none。
MCP client 重启后连接断了
这是正常行为——连接池跟随 MCP server 进程生命周期。MCP client 重启会关闭 server 进程,连接池随之释放。下次 agent 调用任意 portal 工具时会自动重建连接。
更新到最新版
# 装了的(推荐):升级到 PyPI 最新版
uv tool upgrade portal-mcp-server # 或 uv tool upgrade --all
# 零安装 uvx:刷新缓存重新拉取最新版
uvx portal-mcp-server@latest --help然后重启 MCP client。
架构与设计
MCP 客户端通过 stdio(或可选 HTTP)调用 Portal。服务端负责主机解析、策略检查、凭据解析与审计,SSH 操作进入 AsyncSSH 连接池;local_exec 在服务端本机运行。
与直接使用 ssh / scp 的关系
已有脚本可以继续使用 OpenSSH。Portal 提供的是 agent 可调用的统一接口:连接池、保留状态的 shell、带 hash 检查的编辑、结构化搜索、凭据输入和任务管理。
能力 | 直接使用 SSH / scp / rsync | Portal |
连接复用 | Linux/macOS 可配置 | 在 Python 进程内复用 AsyncSSH 连接,跨工具共享 |
shell 状态 | 每次独立 |
|
文件编辑 | 需脚本自行处理并发冲突、暂存与替换 | 整文件与范围 hash 校验、临时写入、原子替换 |
搜索与传输 | 需解析命令输出、管理增量与失败重试 | 结构化结果、增量判断、传输进度与续传 |
多机与后台任务 | 需脚本管理并发、进程和状态 | 多机并发/滚动调用、 |
隧道 | 自行维护 SSH 进程 |
|
凭据与审计 | 需额外安排输入通道、策略和日志 | CLI 凭据服务、工具级策略与审计 |
连接复用通常减少后续操作的握手开销;实际延迟取决于网络、服务器和任务。Portal 不依赖 OpenSSH 的控制 socket,也不会继承已有 OpenSSH master 连接。
调用路径
MCP client → Portal tools → host / policy / credentials → AsyncSSH pool → remote hosts
│
└→ local_exec → server's local machine
User terminal → portal CLI → Portal credential agent ← MCP server
User terminal → ssh-add → ssh-agent ← AsyncSSHCLI 与 MCP server
portal 与 portal-mcp-server 指向同一个 Python 入口,但分别由用户终端和 MCP 客户端启动。CLI 不直接向正在运行的 MCP server 写入密码;它通过凭据服务保存,server 在需要时读取。
共享配置包括 hosts.yaml、policies.yaml、secrets.yaml 和记录凭据服务地址的 agent.json。CLI 与 server 各自读取配置;建议一起升级,避免新字段只在一端生效。ssh-agent 是独立的签名服务,通过 IdentityAgent 或 SSH_AUTH_SOCK 选择。
设计原则
优先使用少量职责清楚的工具:一次执行、持久会话、后台任务、文件、传输和资源管理。相同资源的 action / view 在一个工具中表达,参数用 Literal 形成可校验的 schema 枚举。相关取舍也参考 Writing Tools for Agents。
14 个工具的 name、description、inputSchema 合计约 9k tokens,原始测量使用 tiktoken o200k_base,具体数值会随描述和版本变化。保留工具的依据是它能提供有用的状态管理、并发/写入保证、凭据边界或结构化结果;便利命令可由执行工具覆盖,无需逐个变成新工具。
按步骤执行与后台任务
remote_exec / remote_shell 是前台步骤:执行、读取输出,再决定下一步。长时间无人交互的工作使用 remote_job。
前台 timeout 必填,上限由 PORTAL_MAX_TIMEOUT 控制;超过上限时拒绝调用。commands=[…] 适合固定批次,复杂依赖应拆成可检查的步骤。MCP progress 心跳用于向客户端报告调用仍活跃,不能代替服务端超时或保证所有客户端都会续期。
前台调用运行在 MCP server 进程中;stdio 客户端关闭通常也会结束 server。remote_job 将命令放到远端后台,任务表按进程尽力持久化,受任务上限和 TTL 管理。它能在客户端断开后继续执行,但不是完整的工作流调度系统。传输不会自行存活,上传重试可使用续传机制。
连接池与持久 shell
连接池按主机管理,每台默认最多 5 条 TCP 连接、每条默认 5 个并发 channel。SFTP、exec、隧道等共享连接。满载时可复用最空闲连接并告警;空闲与超龄连接按配置在后续获取连接时清理,见 调优参数。
连接只负责传输;remote_shell 的会话另行管理。持久 shell 使用 PTY 和 OSC 133 标记识别命令边界,不靠短暂静默猜测命令是否完成。交互提示会触发软取消;能恢复到提示符时保留会话,恢复超时则销毁。输出受内存上限约束,超限会标记 truncated。
AsyncSSH 让会话、SFTP、隧道与异步取消处于同一进程内,便于管理资源和复用凭据。这里的后台任务能力来自远端 nohup,并不通过另外启动一套本地 SSH 客户端来维持前台调用。
统一认证与可见反馈
所有 SSH 工具经同一连接管理器解析身份;sudo 和 secret 值由服务端按需注入。密码没有 MCP 工具参数入口,后台任务目前不支持 sudo/secrets。设计取舍见 ADR-0003。
MCP 客户端可以捕获或忽略 server stderr,因此需要用户知道的配置告警放进 hosts(action="list") 的结果;致命错误由相关工具返回。日志供排障与审计使用,不作为唯一用户反馈。工具 description 说明凭据缺失时如何引导 CLI 输入;不依赖客户端把 server-level instructions 加入模型上下文。
维护时需要保留以下边界:
命令结果可以剥除尾部换行,文件读取必须保留原始内容,否则 hash 无法对应。
SSH 别名合并需要用别名连接,
HostName不一致时拒绝;见 ADR-0002。sudo patch 先记录原属主、组和权限,暂存到用户私有目录,再在目标旁原子替换;不能统一改成 root 或默认权限。
策略拒绝与只读操作的审计行为、写入后的审计失败语义应以 SECURITY.md 为准。
测试
开发环境
git clone git@github.com:TMYTiMidlY/portal-mcp-server.git
cd portal-mcp-server
uv sync --all-extras
uv run ruff check portal_mcp_server/ tests/
uv run python -m pytest tests/ -v要让 MCP 客户端使用当前源码,可执行 uv tool install --force --editable .,再重启 server。完整开发流程见 CONTRIBUTING.md。
单元 + 安全(不需要真实 SSH)
pytest tests/ -v
# live SSH 测试默认 skip(受 PORTAL_TEST_LIVE 环境变量控制)覆盖:command injection regression、safety validators、hash-protected editor、concurrency、resource lifecycle、multi-host policy enforcement、password_command/passphrase_command 安全不变量、audit fail mode。
端到端 live smoke
tests/live_smoke.py 直接 import 本地工作树驱动一系列真实 SSH 行为:hosts.yaml 残留 password: 字段处理、ssh_exec 基础调用、remote_exec(group_tag=...) 在真实主机上的 gate(blocked 命令 + 不在 allowlist 的主机均拦截)、remote_shell 单命令的 gate、remote_shell + remote_patch 在远端 /tmp/ 的 round-trip(含 stale-hash 拒绝路径)、audit.jsonl 是否吃到新加的 operation tag。
PORTAL_AUDIT_FAIL_OPEN=1 \
PORTAL_TEST_HOST=server.example.com PORTAL_TEST_PORT=22 PORTAL_TEST_USER=deploy \
PORTAL_TEST_KEY_PATH=$HOME/.ssh/id_ed25519 \
uv run --with-editable . --with pytest --with pytest-asyncio \
python tests/live_smoke.py⚠️ 它会在远端 /tmp/portal-mcp-server-smoke-<pid>.txt 写一次再删除——只动 /tmp。
CI / Release
仓库用 GitHub Actions 自动化跑测试和发布,本地不需要手动 build:
CI(
ci.yml):每个 PR / push tomain在 Python 3.10 / 3.11 / 3.12 / 3.13(ubuntu)上ruff check portal_mcp_server/ tests/+pytest tests/(产品代码和测试一起 lint),另有 macOS 全量 job 与 Windows 命名管道 / 计划任务 job;全绿才能 merge。Release(
release.yml):push 一个v*tag 自动触发(含 PEP 440 预发布 / dev / post,如v4.0.0a0)——python -m build产出 wheel + sdist → 从CHANGELOG.mdawk 抽出对应版本段做 GitHub Release body → 通过 PyPI trusted publishing(OIDC 短令牌,无静态 token)发布到 PyPI。
完整发布流程、CHANGELOG 格式约束与 release 失败排障见 CONTRIBUTING.md § CI & Release 自动化。
贡献
欢迎 issue 与 PR。简版要点:
Python 3.10+,I/O 全部
async/await,无阻塞调用不出现硬编码 hostname / username / IP / path
新工具写好 docstring(FastMCP 用作 MCP description)+ 同步 README「工具列表」节(含折叠的完整签名 + 源码位置表)
状态变更工具必须过
_gate+ 写audit_log测试覆盖关键路径;
pytest tests/ -v必须全绿不 commit secret;
examples/hosts.yaml是唯一 schema 模板commit message 走 Conventional Commits
完整开发流程、新工具开发清单、PR 模板、安全 / 隐私规则见 CONTRIBUTING.md(English)。
协议与致谢
Apache License 2.0(见 LICENSE)。
衍生关系与 third-party 算法引用见 NOTICE:
jaguar999paw-droid/ssh-shell-mcp(Apache 2.0)——git ancestry,底层模块(asyncssh 引擎、连接池、tunnel 管理、orchestrator、安全策略)沿用;上层 14 个 portal 工具是新设计tumf/mcp-text-editor(MIT)——remote_text_editor.py的 SHA-256 hash-protected edit 算法参考来源,针对 AsyncSSH SFTP 重写
⚠️ 本工具让 agent 拥有对远端系统的 SSH 访问能力。请只在你拥有或被授权的系统上使用。
Available Tools
14 toolshostsA
Manage the SSH host registry.
Modes
action="list": list all known hosts. Includes hosts.yaml / runtime- registered entries AND every
Hostalias discoverable in the OpenSSH client config (~/.ssh/config+ system-wide fallback; or only$PORTAL_SSH_CONFIGwhen it is an absolute path; or none of them when it is "none" — thessh -F noneanalogue), resolving real HostName/User/Port. Each entry has asourcefield: "hosts.yaml", "runtime", "ssh-config", or a "…+ssh-config" overlay (metadata from the declared origin, connection params from ssh config). Example: hosts(action="list")action="register": add a host to the registry. Pass
host(ip/hostname), or justnameif ~/.ssh/config has a matching Host alias (registers a use_ssh_config overlay). Optional: user (default root), port (default 22), key_path (else asyncssh falls back to ~/.ssh/id_* or ssh-agent), tags (comma-separated, used by remote_exec's group_tag). Example: hosts(action="register", name="web01", host="10.0.0.1", user="ubuntu", tags="web,prod")action="remove": remove a host from the registry. Required: name. Example: hosts(action="remove", name="web01")
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 auth: password and
a password_command:. See README §Authentication.
action="list" may include a per-host warnings array (e.g. a plaintext
password: field in hosts.yaml that is being ignored). When present,
relay these warnings to the user — they flag misconfigurations the user
must fix, and server-side logs are not visible to them.
| Name | Required | Description | Default |
|---|---|---|---|
| host | No | ||
| name | No | ||
| port | No | ||
| tags | No | ||
| user | No | root | |
| action | Yes | ||
| key_path | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations exist, so the description carries the full burden and does so well: it discloses the security constraint (key-based only, password auth intentionally withheld to avoid leaking credentials into tool-call traces), the source/overlay resolution model, defaults (root/22), and the per-host warnings array that the agent must relay. This is behavior an agent could not infer from the schema.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loaded with a Modes section and per-mode examples, which is the right structure. The list-mode paragraph is dense with parenthetical detours (the ssh -F none analogue, the overlay naming), which is informative but slightly over-long.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a multi-mode registry tool with 7 params, an output schema, and no annotations, the description covers modes, defaults, auth constraints, error-adjacent warnings, and external fallbacks. Nothing an agent needs to select or invoke a mode correctly is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 0%, yet the description documents all six non-action parameters: host (ip/hostname) vs name (ssh-config alias), user default root, port default 22, key_path with asyncssh fallback chain, and tags (comma-separated, consumed by remote_exec's group_tag). It fully compensates for the missing schema descriptions and adds defaults and cross-tool semantics.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
Names a specific resource (the SSH host registry) and enumerates its three operations (list/register/remove) with concrete example invocations, so the agent knows exactly what each mode does. It is clearly distinct from the remote_* siblings that operate on hosts rather than the registry itself.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Gives explicit when-to-use guidance: ssh-config hosts are auto-resolved on first use, so explicit registration is 'only needed for tag-based grouping'. It also routes the agent to the hosts.yaml + password_command path (and README §Authentication) for the password case this tool deliberately refuses.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
inspectA
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
view="snapshot" (default): server metadata + connection pool + bash sessions + audit stats + security policy summary. Use this for an all-at-once diagnostic. (Hosts and tunnels are intentionally absent — see the note above.)
view="server": just the server-level metadata (version, python_version, pid, started_at, uptime_s, transport, resolved config paths). Cheap; use this when you only need to know "which version am I talking to?" without pulling the full snapshot.
view="sessions": the
host → session_idmap of cached persistent bash sessions (what remote_shell reuses per host). This is plumbing diagnostics — the sessions are implicit, which is why they live in inspect rather than carrying their own list like tunnels/hosts.view="history": last
limitaudit log entries (default 50). Optionalhost_filter. Example: inspect(view="history", limit=20, host_filter="web01")view="stats": aggregate audit stats (counts by operation, error rate, etc.).
view="policy": current security policy details (host allowlist, command blocklist, allowlist, rate limit).
Read-only. Used to introspect what the MCP server has been doing and what limits are in place.
| Name | Required | Description | Default |
|---|---|---|---|
| view | No | snapshot | |
| limit | No | ||
| host_filter | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full behavioral burden, and it does so well: it declares 'Read-only' and describes what each view surfaces, including security policy internals (host allowlist, command blocklist, rate limit). It does not discuss auth requirements, error behavior, or any cost/latency beyond calling one view 'cheap', so it falls short of fully compensating for the missing annotations.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loaded with a one-line summary, then a scannable '## Views' list where each bullet earns its place. It is somewhat long and repeats the resource-vs-plumbing distinction twice (the note paragraph and the sessions bullet), which is mild redundancy rather than waste.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists, so return values need not be explained; the description focuses on the decision-relevant content — which view to pick and what it contains. For a multi-view introspection tool with an enum selector, nothing an agent needs to call it correctly is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 0%, so the description must compensate, and it does: it defines all six enum values with behavioral meaning, explains limit as 'last limit audit log entries (default 50)', and documents host_filter as optional with a concrete example. This adds substantial meaning beyond the bare schema.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ('Inspect MCP server internal state and the audit log') and frames itself as the 'read-only introspection hub' for plumbing and history. It explicitly carves out what it does NOT cover, naming sibling tools hosts(action="list") and remote_tunnel(action="list"), so an agent can distinguish it without opening a schema.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Every view has an explicit when-to-use note (e.g., view='server' is 'cheap; use this when you only need to know which version am I talking to?', view='snapshot' is 'for an all-at-once diagnostic'). It also states the negative case — hosts and tunnels live in their own resource tools, not here — leaving nothing to inference.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
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 portal secret set <name> in their own terminal;
you pass secrets=[""] and reference it as the uppercased env var
($NAME), injected into the local child's env and redacted from output. This
lets a local script use an API token without the value ever entering this
conversation.
★ 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 portal sudo set-local, or a top-level
<local>: section's sudo_password_command in hosts.yaml. Fed to sudo -S -k
on stdin (never on argv/audit). May be combined with secrets (their values
ride the same stdin channel, read+exported inside the sudo'd shell). This is
the LOCAL counterpart of remote_exec(use_sudo=True); the reserved identity
<local> is distinct from any SSH host named local / localhost.
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.
| Name | Required | Description | Default |
|---|---|---|---|
| command | Yes | ||
| secrets | No | ||
| timeout | Yes | ||
| use_sudo | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations provided, so the description carries full burden. It covers credentials, sudo, timeout behavior, keepalive pings, and risk flagging. However, it does not explicitly describe the return format or output schema.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is lengthy but well-structured with bullet points and sections. It front-loads the purpose. While verbose, the complexity of the tool justifies the detail.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given 4 parameters, no annotations, and existing output schema, the description covers all necessary aspects: usage, parameters, behavior, and safety. It is complete enough for an agent to select and invoke correctly.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
With 0% schema description coverage, the description adds rich semantics for all 4 parameters: command, secrets (redacted env vars), timeout (required, no default, ceiling), and use_sudo (out-of-band password). Provides clear usage details.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool runs a command on the MCP server's own machine, distinguishing it from remote_exec. The verb 'run a command' and resource 'local machine' are specific and accurate.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicitly states when to use (local commands) and when not (remote, use remote_exec). Also explains it is off by default and requires an environment variable. Provides guidance on secrets and sudo usage.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
policy_checkA
Dry-run a host (and optional command) through the security policy.
command="" : check whether the host is accessible at all. Example: policy_check(host="web01")
command="rm -rf /" : check whether this command would be allowed on this host. Example: policy_check(host="web01", command="systemctl stop nginx")
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 policies.yaml has an
empty host_allowlist (any host), empty command_blocklist / allowlist
(any command), and only a per-host rate limit. So policy_check will
return ALLOWED for almost anything until you populate
$XDG_CONFIG_HOME/portal-mcp-server/policies.yaml (default
~/.config/portal-mcp-server/policies.yaml)
with explicit rules. Use inspect(view="policy") to inspect what
the server actually has loaded. ALLOWED therefore means "no rule
currently blocks this", not "this is safe to run".
| Name | Required | Description | Default |
|---|---|---|---|
| host | Yes | ||
| command | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description fully discloses behavior: dry-run, no execution, returns 'ALLOWED' or 'BLOCKED: <reason>', does not consume rate-limit tokens. It also warns about default permissive policy, which is critical for correct interpretation.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is well-structured with clear sections, examples, and a warning. It is moderately concise; the warning block is valuable but slightly lengthy. Still, it earns its space.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
The description covers return values, default policy, and how to inspect policy state. With output schema present, it is complete for a dry-run tool, leaving no ambiguity about behavior or expected results.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 0%, but the description compensates thoroughly: explains host as accessibility check, command as permission check, and provides examples with expected behavior, adding meaning beyond schema field names.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states it's a dry-run to check security policy, provides examples, and distinguishes from siblings by emphasizing non-execution. The examples for host and command make the purpose concrete.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description advises using before risky multi-host operations and warns about default permissive policy. It explains the two usage forms but does not explicitly exclude alternatives like local_exec for actual execution.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
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.
| Name | Required | Description | Default |
|---|---|---|---|
| host | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations are provided, so the description carries full burden. It discloses that the tool closes a cached session and that the next call reopens a fresh one. It mentions idempotency implicitly but lacks detail on side effects like process termination. Still, it provides useful behavioral context.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is concise with three sentences: one for action, one for context, one for usage guidance. It is front-loaded with the primary purpose and avoids unnecessary words.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the tool's simplicity (one parameter, no output schema needed? context says output schema exists but not shown), the description adequately covers purpose, usage, and behavior. The existence of an output schema means return values need not be described.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The sole parameter 'host' has 0% schema description coverage, and the tool description adds no additional meaning beyond its name. The description does not specify expected format, validation, or examples for the host value.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the verb 'close' and the resource 'cached persistent bash session for <host>'. It distinguishes itself from sibling tools (local_exec, policy_check, remote_job) by focusing on session lifecycle management.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description explicitly says 'Rarely needed' and specifies the condition for use: 'to reset a session whose state has gotten dirty'. It also explains that the session is auto-managed by remote_shell, guiding when not to use.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
remote_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 ssh user@host …; it reuses a warm connection pool and fans
out across hosts. Stateless: need cwd/exports to persist → remote_shell;
a long task to background and poll → remote_job.
★ sudo: to run a command as root, call this with use_sudo=True — NEVER put a
bare sudo … in the command or in remote_shell (there is no TTY and the
password can't be fed). The password is supplied out-of-band; you never pass
it. (For root on the server's OWN machine, sudo is not available — see
local_exec.)
★ credentials: when a command needs a secret (API token, deploy key, …), do
NOT have the user paste it into the chat. They run
portal secret set <name> in their own terminal; you pass
secrets=[""] and reference it as the uppercased env var ($NAME).
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 a && b && c string: you lose per-step visibility
and can't intervene. A multi-line command string runs as one bash script.
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 login_shell: in hosts.yaml.
use_sudo: run via sudo -S, with the password resolved out-of-band per host
(the per-user credential agent from portal sudo set <host>, or the
host's sudo_password_command) — never from you. Refused with guidance if
no source is set. Consumes the command's stdin (tools that read their
own stdin aren't supported under sudo). May be combined with secrets
(their values ride the same stdin channel, after the password).
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.
| Name | Required | Description | Default |
|---|---|---|---|
| host | No | ||
| login | No | ||
| command | No | ||
| delay_s | No | ||
| secrets | No | ||
| timeout | Yes | ||
| commands | No | ||
| use_sudo | No | ||
| group_tag | No | ||
| serialize | No | ||
| stop_on_error | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations are provided, so the description carries the full burden and does so comprehensively: statelessness, warm connection pool, parallel-by-default fan-out, hard timeout ceiling (PORTAL_MAX_TIMEOUT, refused above it), login-shell degradation, sudo stdin consumption, out-of-band secret resolution/redaction, and the high_risk result flag. These are exactly the behavioral traits an agent needs to call it safely.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loaded with the core purpose and cleanly sectioned with ★ callouts for the high-stakes gotchas (sudo, credentials), and nearly every sentence adds unique information. It is nonetheless very long and has some prose repetition, which keeps it short of a 5.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For an 11-parameter tool with no annotations but an output schema, the description supplies all the missing behavioral and parameter context while leaving return-value structure to the schema. An agent has everything needed to call it correctly and safely.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 0% across 11 parameters, so the description must compensate, and it does: host/host-list/group_tag target selection, command vs commands semantics, stop_on_error behavior, timeout semantics (required, no default, real cut-off), login default and degradation, secrets as names-not-values, and use_sudo/serialize interactions are all explained beyond the bare schema types.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb+resource ('Run a shell command on one or more remote hosts over SSH') plus the immediate return shape (exit code + separate stdout/stderr). It explicitly distinguishes itself from remote_shell, remote_job, and local_exec, so an agent can pick it apart from siblings without opening schemas.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicitly names when to use each alternative: need cwd/exports to persist → remote_shell; long background task → remote_job; root on the server's own machine → local_exec. It also gives when-to-use/when-not guidance for command vs commands batching and for serialize rollouts, leaving nothing to inference.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
remote_globA
Find files by a glob pattern on a remote host, newest first.
Prefer this over running raw find/ls through remote_exec — it
returns structured JSON, sorts by modification time, and hard-caps at 100
files. Use remote_grep when you need to match file contents instead.
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 .gitignore (matches CC Glob).
| Name | Required | Description | Default |
|---|---|---|---|
| host | Yes | ||
| path | No | . | |
| pattern | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations, so the description carries the full behavioral burden, and it does well: discloses newest-first sort, a hard cap of 100 files, a 'truncated' flag, and that .gitignore is NOT respected (unlike remote_grep). It leaves auth/permission requirements and error behavior unstated, which is the remaining gap for a zero-annotation tool.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loads the core action and the key routing decision, then uses compact labelled sections for args and returns. Every sentence carries non-redundant information; nothing is filler.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given a 3-param tool with 0% schema coverage and no annotations, the description covers action, routing, parameter formats, defaults, result shape, and a notable behavioral caveat (.gitignore). An output schema exists, yet the extra return summary is a harmless bonus rather than a necessity.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 0%, so the description must compensate, and it documents all three parameters: host (SSH alias/registered name), pattern with concrete glob examples, and path with its default and the relative-path semantics of returned filenames.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb+resource ('Find files by a glob pattern on a remote host') plus a distinguishing trait (newest first). It explicitly separates itself from remote_exec (raw find/ls) and remote_grep (content matching), so an agent can pick it without opening a schema.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicitly says to prefer this over running find/ls via remote_exec, and names remote_grep as the alternative for content matching. Both the when and the when-not are stated with the selecting condition.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
remote_grepA
Search file contents with a regex on a remote host (ripgrep, fallback
grep). Prefer this over running raw rg/grep through remote_exec —
it returns structured JSON and caps output so a broad search can't blow up
your context. Pair it with remote_glob to find files by name.
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). head_limit caps the TOTAL lines
returned and offset pages through them.
- "count": per-file match counts + a grand total.
ignore_case: case-insensitive match.
before_context / after_context / context: lines of context to include
around each match in "content" mode (context = both sides).
head_limit: cap on results (files / content lines / count rows). Default
250; a truncated flag in the result tells you more were dropped.
offset: skip the first offset results (pagination).
multiline: let . and the pattern span line boundaries.
Respects .gitignore (ripgrep's default). Returns JSON whose shape depends
on output_mode; every shape includes a truncated flag.
| Name | Required | Description | Default |
|---|---|---|---|
| glob | No | ||
| host | Yes | ||
| path | No | . | |
| offset | No | ||
| context | No | ||
| pattern | Yes | ||
| file_type | No | ||
| multiline | No | ||
| head_limit | No | ||
| ignore_case | No | ||
| output_mode | No | files_with_matches | |
| after_context | No | ||
| before_context | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations are provided, so the description carries the full burden and does well: it discloses .gitignore respect, output capping to protect context, a truncated flag, newest-first ordering, and pagination via offset. It does not mention auth/permission requirements or rate limits, which is the only real gap for a remote operation.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loads the most valuable guidance (prefer over remote_exec, pair with remote_glob) before the args list, and each arg line earns its place given zero schema documentation. It is lengthy, but the length is justified by the 13 undocumented parameters.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists, so return values need not be spelled out, yet the description still notes that the JSON shape depends on output_mode and always carries a truncated flag. Combined with the threading of grepping workflow and pagination, nothing needed to invoke it correctly is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 0%, so the description must compensate, and it does: it documents the meaning of every non-trivial parameter (glob syntax and negation, file_type examples, all three output_mode values, context/before_context/after_context, head_limit default and truncation, offset paging, multiline). This is far beyond bare schema titles.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ('Search file contents with a regex on a remote host') with the implementation noted (ripgrep, fallback grep). It explicitly distinguishes itself from two siblings: it is preferred over raw rg/grep via remote_exec and pairs with remote_glob for filename search.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicit when-to-use routing: 'Prefer this over running raw rg/grep through remote_exec' plus 'Pair it with remote_glob to find files by name'. It also gives workflow guidance for the output_mode choice ('use it to locate, then re-grep with output_mode=content').
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
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
action="submit": start
commandonhostin the background — in a LOGIN shell (bash -lc, loading ~/.profile/.bashrc PATH+env; login=False to opt out) via nohup + remote tmp files — returning {job_id, host, remote_pid, started_at, status} right away. The job keeps running even if the SSH connection drops. NOTE: sudo / secret injection are NOT supported in the background and passing use_sudo=True or secrets=[...] here is rejected with guidance (sudo -S needs a stdin the nohup process detaches; secrets would land on argv /psfor the whole job) — run those with remote_exec (one-shot) or remote_shell instead.action="poll": fetch this job's status + new output on demand, not all at once. Required: job_id. Pass since= to get only the bytes produced since then; each poll returns at most
max_bytes(default 64 KiB) so a big backlog doesn't dump in one shot. Keep polling with since=new_offset while the returnedmoreis true to drain the rest. Or pass tail=N to just peek the last N lines. Returns {status: running|done|failed|cancelled| unknown, exit_code?, output_chunk, new_offset, more, finished_at?}.action="cancel": signal the job. Required: job_id. signal=TERM (default) or KILL. Best-effort —
killdoesn't guarantee instant death; poll to confirm. Returns {job_id, signal_sent, status_after}.action="list": list all known jobs {job_id, host, status, started_at, age_s, exit_code?}.
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 ps. Finished jobs are swept after a TTL (default 1h) and
their tmp files removed. There is a cap on concurrent live jobs (default 50).
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").
| Name | Required | Description | Default |
|---|---|---|---|
| host | No | ||
| tail | No | ||
| login | No | ||
| since | No | ||
| action | Yes | ||
| job_id | No | ||
| signal | No | TERM | |
| command | No | ||
| secrets | No | ||
| use_sudo | No | ||
| max_bytes | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations provided, so description carries full burden. It thoroughly discloses behavioral traits: background execution via nohup, login shell behavior, persistence across restarts, concurrent job limits, TTL cleanup, and why sudo/secrets are rejected. Also notes best-effort persistence and recovery via ps.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is quite long but well-structured with sections for actions and limits. However, it includes a 'Manual fallback' section that may be extraneous for an AI agent. It is front-loaded with the main purpose but could be more concise by trimming less essential details.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the tool's complexity (11 parameters, 4 actions, persistence, limits), the description is highly complete. It covers all actions with parameter requirements, explains output schema fields implicitly, mentions persistence behavior and limits, and provides a fallback. The presence of an output schema (context indicates exists) reduces the need to describe return values.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 0%, so description must compensate. It explains most parameters in context of actions: for poll, it explains since, max_bytes, tail; for cancel, signal; for submit, command, host, login. It explains the login parameter's default and ability to opt out. However, not all parameters are explicitly described (e.g., host's default is not clarified, tail default is not mentioned), but overall adds significant meaning beyond schema.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool runs a command in the background and returns a job_id immediately. It distinguishes itself from sibling tools by noting when to use remote_exec (quick commands) versus this tool (long tasks). The four actions (submit, poll, cancel, list) are clearly enumerated with specific purposes.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicit usage guidance: use for long tasks, use remote_exec for quick tasks. Also specifies what not to do: sudo/secrets are rejected and explains why, directing to remote_exec or remote_shell instead. Provides a manual fallback alternative using remote_exec.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
remote_patchA
Apply patches to a remote file with hash-based conflict detection.
Workflow:
Call remote_read to obtain content + file_hash + range_hash for each region.
Call remote_patch with the SAME file_hash and per-patch range_hash.
If the file was modified by anyone else in between, this call returns {"result": "error", "reason": "Content hash mismatch ...", "current_file_hash": ...} — re-read and try again. The remote file is untouched.
patches_json must decode to a list of patch objects: [{"start": int, "end": int|null, "contents": str, "range_hash": str}, ...]
Notes:
Patches are applied bottom-to-top so line numbers stay valid.
Overlapping patches are rejected.
Writes are atomic (tmp file + rename) and re-hashed after write.
When auto_newline is true, missing trailing newlines on patch contents are auto-appended only if the slice they replace ended with one. The result includes a "warnings" list either way.
use_sudo: read+write the file as root, for a root-owned file the SSH user can't touch. Keeps the SAME hash safety + atomicity as the normal path: content is read via
sudo cat, staged over SFTP into the user's private home, then placed with one sudo step (cp → temp beside the target → restore owner/group/mode → atomic rename). The target must already exist; the sudo password is resolved out-of-band per host and the result is flagged high_risk. (Implementation note in the README.)
| Name | Required | Description | Default |
|---|---|---|---|
| host | Yes | ||
| path | Yes | ||
| encoding | No | utf-8 | |
| use_sudo | No | ||
| file_hash | Yes | ||
| auto_newline | No | ||
| patches_json | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full burden and succeeds: it discloses atomic write behavior (tmp file + rename), bottom-to-top application, rejection of overlaps, auto_newline semantics, sudo behavior including staging and high_risk flag, exact error response shape, and that the remote file is untouched on mismatch. This is unusually rich behavioral context.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Well-structured with a front-loaded purpose, a numbered workflow, a patches_json format block, and notes. It is somewhat long, but every section adds distinct value for a complex, multi-behavior tool. Minor trimming of the implementation note could tighten it, but nothing is wasted.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given 7 parameters, 0% schema description coverage, no annotations, and an existing output schema, the description is complete enough: it covers the workflow, error handling, patch format, sudo path, atomicity, and warnings. The remaining implicit items (host, path, encoding) are standard and unlikely to trip an agent.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 0%, so the description must compensate. It fully specifies patches_json format (list of objects with start, end, contents, range_hash), explains file_hash usage, use_sudo, and auto_newline. Host, path, and encoding are left implicit, but those are largely self-evident; the complex parameters are well covered.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb ('Apply patches') and resource ('remote file') and distinguishes itself from siblings via the hash-based conflict detection mechanism. An agent can immediately tell this is the safe, hash-verified patch tool rather than a generic shell or transfer tool.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Provides an explicit workflow: call remote_read first, then remote_patch with the same file_hash and range_hash. It also explains the retry path on hash mismatch. It does not compare against alternatives like remote_exec or remote_shell, so it lacks explicit when-not-to-use guidance, but the prerequisite flow is clear.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
remote_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 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 limit lines (default
PORTAL_READ_MAX_LINES=2000) and at most PORTAL_READ_MAX_BYTES (default
16384) of content, whichever binds first — but always at least one line.
When the page stops before the requested end, truncated is true and
next_start gives the line to continue from (pass it back as start).
Pages are cut on line boundaries, so range_hash stays valid for patching.
Usage:
* Whole file (auto-paged): call remote_read(host, path); if the result
has truncated: true, call again with start=<next_start> and repeat
until truncated is false, concatenating each page's content in order.
* A specific range: remote_read(host, path, start=40, end=80) (still
paged if that range is huge).
* Just the first N lines: remote_read(host, path, limit=N) — handy to
peek the head of a big file without pulling it all.
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 sudo cat (root) for a file the SSH user can't read
(root-owned / mode 600). The sudo password is resolved out-of-band
per host (never a parameter) and the result is flagged high_risk.
Exact bytes are preserved (no newline stripping) so file_hash stays
valid for remote_patch.
| Name | Required | Description | Default |
|---|---|---|---|
| end | No | ||
| host | Yes | ||
| path | Yes | ||
| limit | No | ||
| start | No | ||
| encoding | No | utf-8 | |
| use_sudo | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full burden and does so: it discloses paging caps (PORTAL_READ_MAX_LINES/BYTES), truncation and next_start continuation semantics, line-boundary cutting that keeps range_hash patch-valid, and sudo behavior including out-of-band password resolution and a high_risk flag. This is unusually rich behavioral context.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loaded with the return shape and patch dependency, then organized under Usage and Args headings. Dense and mostly waste-free, though the Usage bullets slightly restate the paging mechanics already described above.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists, yet the description still names the return fields and explains how to consume them (paging loop, hash handoff to remote_patch). For a 7-param paged read tool, nothing needed for correct invocation is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 0%, so the description must compensate, and it does: every one of the 7 params is documented with meaning and defaults, including the 1-based inclusivity of start/end, the interaction between limit and the byte cap, and that use_sudo is never a password parameter.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb+resource (read a file or 1-based line range from a remote host) with the added value of SHA-256 hashes. It is clearly distinguishable from siblings like remote_patch, remote_grep, and remote_transfer.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicitly enumerates three usage patterns (whole-file auto-paged, specific range, first-N-lines) with concrete syntax, and ties the returned file_hash to the downstream remote_patch requirement. When-and-how guidance is unambiguous.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
remote_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 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:
command="pytest" → {host, session_id, command, exit_code, output, duration_s}. ONE command per call is the preferred shape: the session PERSISTS across separate calls, so cwd / exports / venv from an earlier call still apply to the next — you keep step-by-step continuity AND get to read each step's real output before deciding the next.
commands=["cd /app","source .venv/bin/activate","pytest"] → runs the sequence in order in ONE call (same session). Reach for it ONLY for a fixed sequence you do NOT need to inspect between steps; you can't judge one step's output before the next runs. Returns {host, session_id, results:[…]}; stop_on_error=True (default) halts at the first failure and adds stopped_at.
Behavior:
bash or zsh; an unknown login shell falls back to bash (if bash is missing the call is refused — use remote_exec).
Output is the COMBINED stdout+stderr stream (use remote_exec when you need them split). Oversize output is capped and flagged truncated=true.
A command that wedges on an interactive prompt (sudo password, ssh first-connect, passphrase, …) is auto-aborted, returning exit_code -1 + error:"interactive_prompt_blocked" + session_preserved:true — cwd/env survive, so the next command runs straight away.
★ 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).
| Name | Required | Description | Default |
|---|---|---|---|
| host | Yes | ||
| command | No | ||
| timeout | Yes | ||
| commands | No | ||
| stop_on_error | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full burden and discharges it: shell fallback (bash/zsh, refusal if bash missing), combined stdout+stderr, truncation flag, auto-abort of interactive prompts with exit_code -1 and session_preserved:true, no sudo/secret injection, and the hard PORTAL_MAX_TIMEOUT ceiling. It also discloses the /tmp write convention that is not enforced.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loaded and sectioned (Pick one / Behavior / timeout / warning), and almost every sentence earns its place. However, session persistence is explained twice and the markdown bolding/emoji adds some bulk, so it is slightly longer than strictly needed.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the complexity (persistent sessions, two modes, mandatory timeout) and that an output schema exists, coverage is nearly complete. The one real omission is session lifecycle: session_id appears only in return shapes, and nothing says how a session is created, reused, or torn down via the sibling remote_close.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 0%, so the description must compensate, and it does for command, commands, stop_on_error (default true, halts at first failure and adds stopped_at) and timeout (required, no default, keepalive rationale, ceiling). The required 'host' parameter gets no format guidance (hostname vs alias from the hosts sibling) and session_id is only ever an output, never explained as an input, leaving a gap on 1 of 5 parameters.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
Opens with a specific verb+resource+scope: 'Run a command (or a sequence) on ONE remote host in a persistent shell session.' It immediately names the two siblings it is not (remote_exec for one-shot/multi-host, remote_job for backgrounded long tasks), so an agent can route without opening any schema.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicit routing rules: use this 'only when you need that continuity,' otherwise remote_exec is faster, and remote_job for long backgrounded work. It further splits the two invocation modes (command vs commands) and states exactly when each applies, including the caveat that sequences cannot be inspected between steps.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
remote_transferA
Transfer files between local and remote via SFTP (binary-safe, atomic).
Modes
direction="upload": local_path → remote_path (single file). Example: remote_transfer(direction="upload", host="web01", local_path="/tmp/app.jar", remote_path="/opt/app/app.jar")
direction="download": remote_path → local_path (single file). Example: remote_transfer(direction="download", host="web01", remote_path="/var/log/syslog", local_path="/tmp/syslog")
direction="sync": recursively sync local_path directory → remote_path directory (upload). Files already present with a matching size+mtime (or sha256 when checksum=True) are skipped.
direction="mirror": recursively mirror remote_path directory → local_path directory (download); the remote→local counterpart of "sync". Example: remote_transfer(direction="mirror", host="web01", remote_path="/srv/www/", local_path="./www/")
direction="upload-list" / "download-list": transfer an explicit list of file pairs given in paths_json (an arbitrary local→remote mapping, not a whole directory). Each pair is skipped when already present with a matching size+mtime (or sha256 when checksum=True), so re-runs only move the changed files; a single pair's failure is collected in failed[] without aborting the batch. local_path / remote_path are ignored in these modes. Example: remote_transfer(direction="upload-list", host="web01", paths_json='[{"local":"/tmp/a.conf","remote":"/etc/app/a.conf"}, {"local":"/tmp/b.conf","remote":"/etc/app/b.conf"}]')
Args:
checksum: for the incremental modes (sync/mirror/upload-list/
download-list), compare files by sha256 instead of size+mtime
(slower, requires sha256sum on the remote; missing remote files or
an unavailable sha256sum force a re-transfer).
paths_json: JSON array of {"local": ..., "remote": ...} objects, required
by the upload-list / download-list modes (ignored otherwise).
resume: (upload only) if a SMALLER partial already exists at remote_path
from an interrupted transfer, append just the missing tail instead
of restarting, then verify the whole file's sha256 and re-upload
fresh once on mismatch. Default on; set False to force a fresh
overwrite. The result carries resumed=true when the tail path ran.
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.
| Name | Required | Description | Default |
|---|---|---|---|
| host | Yes | ||
| resume | No | ||
| checksum | No | ||
| direction | Yes | ||
| local_path | Yes | ||
| paths_json | No | ||
| remote_path | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full burden and does so: atomic/binary-safe claims, skip-on-matching-size+mtime or sha256 behavior, per-pair failure collection without aborting the batch, resume tail-append plus sha256 re-upload fallback, symlink/special-file skipping and no empty-dir creation, and progress reporting. This is unusually rich disclosure for a mutation-heavy tool.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loaded with the mode list and each section earns its place, but the multi-line indented examples and the repeated skip-explanation across sync and list modes add some length that a tighter phrasing could avoid.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a 7-parameter, six-mode tool with an output schema, everything an agent needs is present: mode semantics, required vs ignored params, incremental behavior, failure handling, and edge cases (symlinks, empty dirs), plus sibling routing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 0%, so the description must compensate and does — checksum, paths_json (format plus an inline JSON example), resume (default, semantics, resumed=true flag), and direction are all documented beyond their bare schema titles, with local/remote path roles clarified per mode.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ('Transfer files between local and remote via SFTP') and then enumerates the six direction modes with concrete input/output semantics for each. An agent can distinguish this from remote_patch, remote_read, or remote_shell purely from the description.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicitly routes the agent: 'For text-only edits prefer remote_patch (hash-protected). Use remote_transfer when SFTP semantics are needed: binary files, large files, whole directory trees.' It also states the mode-selection rules (list modes ignore local_path/remote_path).
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
remote_tunnelA
Manage SSH tunnels — a single entry point (like hosts) where
action selects the operation and the other args parameterise it.
Actions
action="open": open a tunnel through
host.kindpicks the type:kind="local" : forward localhost:local_port → remote_host:remote_port via host. Required: remote_host, remote_port (local_port 0 = auto). Example: remote_tunnel(action="open", kind="local", host="bastion", local_port=5432, remote_host="db.internal", remote_port=5432)
kind="reverse": expose local_bind:local_port to host as host:remote_port. Required: remote_port, local_bind, local_port. Example: remote_tunnel(action="open", kind="reverse", host="bastion", remote_port=8080, local_bind="127.0.0.1", local_port=3000)
kind="socks" : SOCKS5 proxy on localhost:local_port via host. Required: local_port (default 1080). Example: remote_tunnel(action="open", kind="socks", host="bastion", local_port=1080)
Returns {tunnel_id, type, host, local, remote}.
action="close": close a live tunnel. Required: tunnel_id (from open). Example: remote_tunnel(action="close", tunnel_id="ab12cd34")
action="list": list all active tunnels (JSON array).
Tunnels are a resource you manage explicitly (open → close), so list
lives here rather than in inspect.
| Name | Required | Description | Default |
|---|---|---|---|
| host | No | ||
| kind | No | local | |
| action | Yes | ||
| tunnel_id | No | ||
| local_bind | No | 127.0.0.1 | |
| local_port | No | ||
| remote_host | No | ||
| remote_port | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full burden and does well: it discloses defaults (local_port 0 = auto, socks default 1080), required arguments per kind, and the return shape. It omits permission/auth requirements, failure behavior, and whether tunnels persist or are torn down on close — gaps that matter for a connection-mutating tool.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loaded with the action selector, then organized under an 'Actions' heading with per-kind bullet detail and compact examples. Length is justified by the 8-parameter, three-mode surface; no filler sentences.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Complete for a multi-action tunnel manager: all actions, kinds, required args, defaults, and an example are present, and the return shape is noted even though an output schema exists. An agent can invoke every mode correctly from the description alone.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 0%, so the description must compensate and it does: it defines host, kind, action, tunnel_id, local_bind, local_port, remote_host, and remote_port with per-kind semantics and concrete examples. This is well beyond what the bare schema conveys.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb+resource ('Manage SSH tunnels') and enumerates the three operations via the action selector. It also explicitly distinguishes itself from the sibling 'inspect', explaining why 'list' lives here rather than there.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Gives explicit when-to-use routing for each action and kind, including required parameter combinations per kind and the rationale that tunnels are an explicitly managed resource (open → close). The note about 'list' versus 'inspect' is exactly the sibling differentiation an agent needs.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
10 tool updates
v4.0.3- Added
hosts - Added
inspect - Added
remote_exec - Added
remote_glob - Added
remote_grep - Added
remote_patch - Added
remote_read - Added
remote_shell - Added
remote_transfer - Added
remote_tunnel
18 tool updates
v4.0.1- Added
local_exec - Added
policy_check - Removed
portal_audit - Removed
portal_check - Removed
portal_close_shell - Removed
portal_exec - Removed
portal_glob - Removed
portal_grep - Removed
portal_host - Removed
portal_job - Removed
portal_local_exec - Removed
portal_patch - Removed
portal_read - Removed
portal_shell - Removed
portal_transfer - Removed
portal_tunnel - Added
remote_close - Added
remote_job
14 tool updates
v3.3.2- First observed
portal_audit - First observed
portal_check - First observed
portal_close_shell - First observed
portal_exec - First observed
portal_glob - First observed
portal_grep - First observed
portal_host - First observed
portal_job - First observed
portal_local_exec - First observed
portal_patch - First observed
portal_read - First observed
portal_shell - First observed
portal_transfer - First observed
portal_tunnel
TDQS
Scored across 14 tools
Most tools target distinct resources/actions (read, patch, grep, glob, transfer, tunnel, hosts), and the exec family (remote_exec, remote_shell, remote_job, local_exec) is explicitly differentiated with strong usage guidance. There is still inherent overlap in the command-execution cluster, but the descriptions resolve it well.
Predominantly consistent snake_case, with a 'remote_' prefix on most tools (remote_exec, remote_shell, remote_job, remote_read, etc.). A few tools break the prefix (hosts, inspect, policy_check, local_exec) but remain readable and snake_case throughout.
14 tools is well within the ideal 3-15 range and each tool earns its place across file, execution, host, tunnel, and policy concerns. No filler or redundant tools.
The surface covers the remote-management lifecycle well: files (read/patch/grep/glob/transfer), execution (exec/shell/job/local), host registry, tunnels, policy, and introspection. Minor gaps exist — no dedicated file delete/mkdir and no host edit/update — but these are workable via remote_exec.
Maintenance
Related MCP Connectors
Termalin's hosted MCP server. Claude, ChatGPT or any MCP client runs commands, moves files over SFTP, queries databases and reads server metrics and monitoring alerts on your servers, keylessly through Termalin Connectors: no SSH key is handed over and no inbound port is opened. Per-host command policy and an audit log. Needs a termal.in Pro or Team account (new accounts start with a 14-day Pro trial).
Hosted MCP server and Agent Skills for specs, engineering threads, and project memory.
Remote MCP server exposing your Node Command fleet (files, shell, processes, browser) to AI hosts.
1MCP server for your apps' tools and custom tools, plus hosted AI agents and approval-gated workflows
Related MCP Servers
- AlicenseAqualityDmaintenanceZero-config SSH/SFTP MCP server that lets an LLM client open temporary SSH/SFTP sessions to remote hosts, run commands, and upload/download files without holding any pre-baked credentials.178 npm2MIT
- AlicenseAqualityAmaintenanceMCP server giving AI agents full SSH access with persistent sessions, structured command output, SFTP file transfer, and port forwarding.1811MIT
- AlicenseAqualityDmaintenanceAn MCP server that gives AI agents SSH access to remote machines through your local OpenSSH client, enabling remote command execution, file transfer, persistent shell sessions, and port forwarding.17169 PyPI25MIT
- AlicenseNot gradedqualityBmaintenanceMCP server enabling AI assistants to securely operate remote servers via persistent SSH sessions, with tools for command execution, file transfer, directory listing, and system monitoring.4MIT