The connector asked for a sudo password on every privileged call and failed whenever nobody answered it, which is every unattended run. - Always probe `sudo -n` first, even for aliases set to sudo="prompt". `_sudo_prefix` used to elicit unconditionally, so a host granting this user NOPASSWD still opened an Agent Inbox prompt; with no human there it hit the client's 300s ELICITATION_DEADLINE, got back `cancel`, and surfaced as "sudo password required (user declined or timed out)". sudo refuses before running anything when it wants a password, so the probe is side-effect free. - Strip a leading `sudo` from `command` and turn it into sudo=true. Agents write `exec(command="sudo systemctl restart x")`: with sudo=false that ran a tty-less sudo, with sudo=true it nested `sudo -S ... sudo ...` whose inner prompt had no tty either. Handles -u/-n/-S/-E/-H/-i/-k/-p/--; an unknown flag leaves the command alone. sudo_user now implies sudo=true. - Run privileged commands as `sh -c '<command>'`, so `&&`, pipes and redirections are elevated too instead of only the first word. - Add SSH_MCP_SUDO_PASSWORD (optional, secret) for unattended runs. It is consulted only after `sudo -n` proved a password is needed, so on a NOPASSWD host it never lands in the command's own stdin. - Actionable errors for every sudo failure mode, and a `hint` on a nested sudo we could not peel off. General review of the same server: - Drain stdout and stderr together and make timeout_sec a real wall-clock deadline. Both streams share one SSH channel window, so reading stdout to EOF first stalled once a chatty stderr filled it. Command stdin is now closed after the optional password. - Queue messages that arrive while awaiting an elicitation reply instead of discarding them, so a concurrent tools/call is not lost. - Record the client's `elicitation` capability at initialize and fail fast when it is absent rather than blocking on a prompt nobody can answer. - Tolerate null/string integer arguments (depth, max_results, context_lines, timeout_sec). - Realign the version across both manifests: fragment.json said 5/1.0.4 while connector.json said 2/1.0.1, so the feed was permanently ahead of the installed version and offered an update forever. Also lands the pending docs work: CONNECTOR_MANIFEST_GUIDE.md as the single source of truth, docs/connector.manifest_guide.md retired to a pointer, CLAUDE.md audited against the repo, compile.py docstring fixed, and an opencode.json config.
124 lines
4.4 KiB
JSON
124 lines
4.4 KiB
JSON
{
|
|
"id": "ssh",
|
|
"name": "SSH Remote Access",
|
|
"type": "mcp_local",
|
|
"launch_command": "python3 ssh_mcp_server.py",
|
|
"transport": "stdio",
|
|
"requires": [
|
|
"PYTHON"
|
|
],
|
|
"dependencies": [
|
|
"paramiko>=3.4"
|
|
],
|
|
"env": [
|
|
{
|
|
"name": "SSH_MCP_POOL_TTL",
|
|
"label": "Connection pool TTL (seconds)",
|
|
"description": "How long an idle SSH connection stays open before being closed. Default: 300 (5 min).",
|
|
"required": false,
|
|
"secret": false,
|
|
"default": "300",
|
|
"example": "300"
|
|
},
|
|
{
|
|
"name": "SSH_MCP_COMMAND_TIMEOUT",
|
|
"label": "Command timeout (seconds)",
|
|
"description": "Max time a remote command can run before being killed. Default: 120.",
|
|
"required": false,
|
|
"secret": false,
|
|
"default": "120",
|
|
"example": "120"
|
|
},
|
|
{
|
|
"name": "SSH_MCP_CONNECT_TIMEOUT",
|
|
"label": "Connect timeout (seconds)",
|
|
"description": "Max time to wait for the SSH handshake. Default: 15.",
|
|
"required": false,
|
|
"secret": false,
|
|
"default": "15",
|
|
"example": "15"
|
|
},
|
|
{
|
|
"name": "SSH_MCP_LOGIN_PW_TTL",
|
|
"label": "Login password cache TTL (seconds)",
|
|
"description": "How long an elicited login password stays cached in RAM. Default: 300.",
|
|
"required": false,
|
|
"secret": false,
|
|
"default": "300",
|
|
"example": "300"
|
|
},
|
|
{
|
|
"name": "SSH_MCP_SUDO_PW_TTL",
|
|
"label": "Sudo password cache TTL (seconds)",
|
|
"description": "How long an elicited sudo password stays cached in RAM. Default: 300.",
|
|
"required": false,
|
|
"secret": false,
|
|
"default": "300",
|
|
"example": "300"
|
|
},
|
|
{
|
|
"name": "SSH_MCP_KEY_PASSPHRASE",
|
|
"label": "SSH key passphrase (non-interactive override)",
|
|
"description": "Passphrase for an encrypted private key. Overrides elicitation. Use only in automated environments.",
|
|
"required": false,
|
|
"secret": true,
|
|
"example": ""
|
|
},
|
|
{
|
|
"name": "SSH_MCP_SUDO_PASSWORD",
|
|
"label": "Sudo password (non-interactive override)",
|
|
"description": "Sudo password used when the remote host demands one and nobody can answer the Agent Inbox prompt (unattended/scheduled runs). Leave empty to always ask interactively. Only used after 'sudo -n' has proved the host really requires a password.",
|
|
"required": false,
|
|
"secret": true,
|
|
"example": ""
|
|
}
|
|
],
|
|
"setup_instructions": [
|
|
"Install dependencies: pip install -r requirements.txt",
|
|
"No OAuth or API key needed: host authentication is managed at runtime via add_alias.",
|
|
"Add an alias: mcp__ssh__add_alias(alias=\"my-server\", hostname=\"...\", username=\"...\", auth=\"key\")",
|
|
"The server stores aliases in ~/.ssh_aliases.json (auto-managed, never hand-edit).",
|
|
"Optional: tune TTLs via SSH_MCP_* environment variables (see env[] above)."
|
|
],
|
|
"docs": [
|
|
{
|
|
"lang": "en",
|
|
"description": "Full SSH remote server access: read/write files, grep, execute commands, manage systemd services, upload/download files and directories, and probe system info \u2014 all over SFTP and SSH with the same output format as Skald's native tools. Aliases manage host config (hostname, port, user, auth method, sudo policy). Login passwords and sudo passwords are elicited on demand and kept only in RAM with configurable TTL. No OAuth, no API keys, no secrets on disk.",
|
|
"llm_short_description": "SSH remote access \u2014 connect to remote servers via alias system. Read, write, list, grep, edit files and execute commands on remote hosts over SSH."
|
|
}
|
|
],
|
|
"mcp_config": {
|
|
"command": "python3",
|
|
"args": [
|
|
"ssh_mcp_server.py"
|
|
],
|
|
"env": {
|
|
"SSH_MCP_POOL_TTL": "{ENV:SSH_MCP_POOL_TTL}",
|
|
"SSH_MCP_COMMAND_TIMEOUT": "{ENV:SSH_MCP_COMMAND_TIMEOUT}",
|
|
"SSH_MCP_CONNECT_TIMEOUT": "{ENV:SSH_MCP_CONNECT_TIMEOUT}",
|
|
"SSH_MCP_LOGIN_PW_TTL": "{ENV:SSH_MCP_LOGIN_PW_TTL}",
|
|
"SSH_MCP_SUDO_PW_TTL": "{ENV:SSH_MCP_SUDO_PW_TTL}",
|
|
"SSH_MCP_KEY_PASSPHRASE": "{SECRET:SSH_MCP_KEY_PASSPHRASE}",
|
|
"SSH_MCP_SUDO_PASSWORD": "{SECRET:SSH_MCP_SUDO_PASSWORD}"
|
|
}
|
|
},
|
|
"homepage": "",
|
|
"icon_small": "icon_sm.svg",
|
|
"icon_large": "icon_lg.svg",
|
|
"scope": "user",
|
|
"tags": [
|
|
"ssh",
|
|
"mcp",
|
|
"local",
|
|
"remote",
|
|
"server",
|
|
"terminal",
|
|
"sysadmin"
|
|
],
|
|
"auth": {
|
|
"type": "none"
|
|
},
|
|
"version": 6,
|
|
"version_string": "1.1.0",
|
|
"version_release_date": "2026-09-03"
|
|
} |