Skip to content

Releases: jaredshuai/mcp-ssh4agent

Release list

v4.0.0

Choose a tag to compare

@github-actions github-actions released this 04 Sep 10:14

[4.0.0] - 2026-09-04

Changed

  • BREAKING — full rebrand from ssh-manager / mcp-ssh-manager to ssh4agent / mcp-ssh4agent. Bin commands are now ssh4agent and mcp-ssh4agent; the MCP server registers as ssh4agent (re-add it in your agent — auto-approval patterns become mcp__ssh4agent__*); the config directory is ~/.ssh4agent (existing ~/.ssh-manager keeps loading until the new directory exists); SSH_MANAGER_* env vars are now SSH4AGENT_*; the log file is .ssh4agent.log. Remote-side paths changed with no migration (the package had no published release): alert configs live at /etc/ssh4agent-alerts.json, backups under /var/backups/ssh4agent, scheduled scripts at /usr/local/bin/ssh4agent-backup-* — re-run ssh_alert_setup and ssh_backup_schedule on hosts configured with earlier code.

  • BREAKING (contributors) — the codebase is now TypeScript, run natively by Node's type stripping. All src/, cli/, scripts/, debug/ modules are .ts with explicit .ts import extensions (erasable syntax only — no enums/namespaces/parameter properties); there is no tsx and no build step: node src/index.ts is the whole dev runtime, and node --check/npm run typecheck gate it in CI. The dev tree needs Node ≥ 23.6 (type stripping). Exception for publishing only: the npm artifact is compiled to JS under dist/ by prepack (Node refuses type stripping under node_modules), so consumers need Node ≥ 20.

  • BREAKING (contributors) — Lint/format tooling is Biome, replacing ESLint + Prettier. biome check gates pre-commit and CI; npm run lint:fix is the one-command fixer.

  • The tool registration funnel owns Policy, Audit, and the response envelope. Every tool handler now declares its policy at registration (gate: 'server' by default, with commandArg/serverFrom/when variants, and explicit gate: 'exempt' | 'manual' for the two deliberate exemptions) instead of hand-weaving applyServerPolicy + auditOk in 15 places: a newly registered mutating tool can no longer skip the per-server policy, and audit entries are written on both the success and failure paths — failure is derived from the isError flag or a thrown error, fixing 29 catch blocks that omitted isError and were audited as successes. Tool modules receive their infrastructure via a ToolContext (ADR-0002) and never import the entry point.

  • Tunnels own their dedicated connection; shutdown closes tunnels first. A tunnel no longer borrows a pooled connection it cannot account for — it holds its own, disposes it on close, and ssh_tunnel_create now refuses proxyJump/proxyCommand explicitly (it silently ignored them before) (ADR-0003).

  • One tool-enablement owner for CLI and server. src/tool-config-manager.ts (logger-free, pure fs + registry data) is the single source for tool groups, modes, and per-tool overrides; the CLI's drifted third copy of the list and its four observable behaviour gaps (mode transitions silently enabling all 37 tools, reset resurrecting legacy configs, dead per-tool overrides under all, hardcoded export-claude list) are gone (ADR-0004).

  • One config-reading path for CLI and MCP entry point. src/env-path.ts is the single .env fallback chain (SSH_ENV_PATH → SSH4AGENT_ENV (deprecated alias) → ~/.ssh4agent/.env → legacy → $PWD/.env → ~/.env → package root → default); server reads go through the shared parseEnvServersText parser; the CLI's five copies of server→ssh-argument resolution collapsed into resolveServerToSshArgs(). The two processes can no longer disagree about which file holds the servers.

  • Centralized shell quoting and dump/import command builders. Every password/path interpolated into a remote command goes through src/shell-quote.ts; the three drifted dump implementations (one of them unquoted — password injection) merged into src/dump-command-builder.ts.

Fixed

  • Failed dump producers can no longer masquerade as successful backups — mysqldump … | gzip > out reported the pipe's last exit code, so a wrong password, missing database, or full disk left an empty/partial archive marked as success. Dumps are now two-step (dump to a temp file gated by &&, then compress, with a cleanup arm removing both half-products) — pure POSIX, valid under any remote login shell including BusyBox ash. Verified against real MySQL 8 / PostgreSQL 16 (Alpine) containers.

  • Corrupt archives can no longer masquerade as successful imports — the mirror bug: gunzip -c X | mysql exited 0 after consuming a truncated stream. Same two-step fix; the plain-input cat X | mysql fallback (which reported success for a missing file) became a direct < X redirection that fails in the shell. Verified against real MySQL / PostgreSQL containers: a truncated .gz fails before the client ever runs, leaving zero partial data.

  • MongoDB backup archive paths no longer drift across consumers — the dump produced <id>.tar.gz while the size check, reported location, and restore all looked for <id>.gz. The path now has one source (mongoArchivePath / getBackupArchivePath), the restore verifies the archive exists before touching the target, and pre-existing .tar.gz backups that were never referenced became restorable. ssh_db_dump reports the actual archive path for mongodb.

  • All user state now lives under ~/.ssh4agent/ — the active-profile pointer (.ssh4agent-profile, plus the pre-rebrand .ssh-manager-profile) was the last state file written to the install directory, which is read-only under a global npm install; every state file now routes through src/state-files.ts with one-time best-effort migration from the old location and 0600/0700 permission tightening on every read/write.

  • Aliases can no longer bypass per-server policy — server resolution went through two paths and only one applied the security modes; there is now a single resolveServer() path for everyone.

  • Audit entries no longer leak database credentials and failure reporting no longer marks errored calls as successes.

  • Connection pool hardening — in-flight connects are shared instead of racing, failed setups dispose the SSH client before rethrowing, ProxyJump dialing is alias-aware, connect timeouts are wired, and shutdown no longer races live remote forwards.

  • ssh4agent codex setup emits a working entry path for installed users (source vs installed package environments), and the documented-but-missing codex, monitor, and session CLI commands are implemented.

Added

  • Domain documentation for agents: CONTEXT.md glossary + docs/adr/ decision records (ToolContext injection, tunnel connection ownership, tool-config single owner, module-split decision for advanced.ts).

  • Interface tests without a network: connection pool, remote tunnels, policy funnel, config unification, tool-config unification, state files, and manager interfaces (session/backup/health) — all driven through fakes at the calling-convention level, where the real bugs used to hide.

[3.8.0] - 2026-08-14

Security

  • @modelcontextprotocol/sdk floor raised to ^1.30.0 — the declared range (^1.17.2) still allowed versions carrying three published advisories, one of them high: cross-client data leak through shared server/transport instance reuse, DNS rebinding protection off by default, and a ReDoS. It also dragged in vulnerable transitive express packages (body-parser, path-to-regexp, qs, ajv). npm audit --omit=dev now reports 0 vulnerabilities. Behaviour verified against the real MCP stdio protocol: identical to 1.17.5 (37 tools registered, same negotiation, clean shutdown).

Added

  • Optional group field on server config (#56 — contributed by @ice616, requested in #55)

    • A free-form label (SSH_SERVER_<NAME>_GROUP in .env, group = "..." in TOML) carried on the server entry itself, so a server's group membership can be read straight off its config when exporting to — or importing from — another tool, with no second file to ship. It round-trips through exportToToml/exportToEnv and is returned by ssh_list_servers.

    • The label is not just descriptive: it makes the group usable. ssh_execute_group and ssh_group_manage list now resolve members from the group field as well, so tagging servers is enough to run a command across them — .server-groups.json is no longer required to have a working group.

    • Membership is the union of both sources: the explicit list stored by ssh_group_manage (which keeps carrying strategy, delay and stop-on-error) plus every server tagged with that name. Group names are case-insensitive. Config-derived members are resolved at read time and never written to .server-groups.json, so they cannot go stale.

    • A group that exists only through the config is read-only for ssh_group_manage; attempting to edit it now returns an error pointing at the server config instead of a bare "not found".

    • The ssh-manager add-server wizard asks for an optional group.

  • Tests: new tests/test-server-groups.js (14 checks) covers config-derived groups, the union with stored groups, case-insensitivity, read-only enforcement, persistence isolation, the dynamic-group reload regression and group execution; tests/test-config-field-names.js gains group coverage across the .env loader, the TOML loader, the forbidden-field guard and the export round-trip.

  • Static type-checking over the existing JavaScript (npm run typecheck) — a tsconfig.json in checkJs/noEmit mode runs TypeScript as a linter over JSDoc annotations. Nothing is compiled and nothing changes for users: the package still ships plain JS straight from src/, with no build step ...

Read more