Releases: developerz-ai/mcp-ssh
Release list
v1.2.4
job poll is now newest-first — latest output is the first page
Polling a long-running job used to be forward-paginated (cursor 0 = oldest), so seeing what a job is doing now meant paging to the end or computing cursor = total − limit. Fixed:
job(action="poll")with no cursor returns the most recent page. Pass the response'snext_cursorto page back through history.- Lines stay chronological within a page, so stack traces / build logs read top-to-bottom — only the page window moves newest-first.
file readis unchanged (forward, top-to-bottom — a file is read start-to-end).
Internals: new paginate_tail / read_page_tail (byte budget trimmed from the oldest end so the newest line is always kept); saved-tail fallback paginates the same way; tool description, cursor doc, and the overflow/page-back hints updated.
Install: sudo dpkg -i mcp-ssh_1.2.4-1_amd64.deb
v1.2.3
Admin CLI: jobs, job kill, sessions
Local ops surface over the same SQLite state the server uses — for a shell on the host:
mcp-ssh jobs [--all]— list running jobs (or all, newest-first, capped at 200).mcp-ssh job kill <id>— SIGTERM then SIGKILL the job's process group.mcp-ssh sessions— summarise durable OAuth tokens (active/expired counts + next expiry). Never prints token material.
Design
- New
src/admin.rs. Commands read/act on SQLite directly and never construct aJobStore— that would start the reaper + startup reconcile and flip the live server'srunningrows tofailed. - Jobs now persist their
pgid(new nullable column + idempotent migration) so the CLI can signal a job's group even after a server restart. Reuses the reaper's TERM→KILL escalation. config::db_path()resolves just the DB path, so the admin commands work without auth credentials configured.
No changes to the MCP tool surface. Drop-in upgrade.
Install: sudo dpkg -i mcp-ssh_1.2.3-1_amd64.deb
v1.2.2
Fix: clients no longer fail job/file with invalid type ... expected string or map
The action enum was emitted as a JSON-Schema $ref into $defs. Clients (Claude Desktop, codex, n8n) drop $defs, so the $ref resolved to nothing — the model couldn't see action is a string enum and sent a non-string placeholder (null, then true), failing deserialization before dispatch.
#[schemars(inline)]onJobAction/FileAction→ the enum is now inlined on the property ({"type":"string","enum":[…]}), no$ref/$defs. Documented fix (MCP python-sdk #1373).lenient_actioncoerces any non-stringaction— and an omitted key — to the read-onlylistdefault instead of dead-ending the call.filestays strict (destructive actions, no safe default).
Supersedes v1.2.1 (null-only tolerance). MCP clients must reconnect/restart to drop the cached old tool schema.
Install: sudo dpkg -i mcp-ssh_1.2.2-1_amd64.deb
v1.2.1
chore(release): v1.2.1 — null-tolerant job action deb Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v1.2.0
feat: SQLite-backed durable state + refresh tokens (v1.2.0) Durable state moves to a single SQLite database (rusqlite, bundled — still a single binary) at /var/lib/mcp-ssh/mcp-ssh.db, in a hybrid model: SQLite holds the low-frequency structured state, while high-frequency live job output keeps streaming to per-job log files. OAuth (src/oauth, src/db): - access + refresh tokens persisted in SQLite, so logins survive a restart (the agent self-updates by restarting its own service — previously that logged every client out). Access tokens 24h, refresh tokens 1 year, rotated on use (RFC 6749 §10.4). grant_type=refresh_token added + advertised; the /token response returns a refresh_token. Many tokens coexist → multiple clients (desktop, mobile, CLI) stay authenticated independently. Jobs (src/jobs): - job metadata + a bounded output tail persisted to SQLite, so job(list) and poll survive restarts; live output still streams to log files under /var/lib/mcp-ssh/logs/jobs. poll falls back to the saved tail when the live log is gone. Reaper runs on startup AND hourly: drops >24h jobs (rows + files), trims finished logs to a tail (5000 lines <3h, 500 after), mtime-ages orphan files, and marks jobs stuck 'running' across a restart as failed. - job ids/log filenames now use '-' for the time (job-23-30-07), not ':' — colon-free is portable everywhere (Windows/scp/globbing). - fix: poll() self-deadlocked on a MutexGuard held to the end of an `if let` block (temporary-in-scrutinee); bind the lookup to a local first. config: db_path (MCP_SSH_DB) decoupled from the job-log dir. Built in parallel by three agents (oauth, jobs, docs) + integration here. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v1.1.5
fix: file write/append/move create parent dirs (mkdir -p) Live QA surfaced it: writing or moving a file under a path whose parent doesn't exist yet failed with a bare ENOENT the agent then had to diagnose. `write`, `append`, and `move` now create missing parent directories first, so writing a new file under a fresh path just works. Adds a directory-redirect test for `read` while here. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v1.1.4
feat: tiered job-log compaction so disk stays bounded over long uptime The server runs for weeks; finished jobs' logs can't grow forever. The hourly reaper now compacts each finished job's log to a trailing tail — enough to debug a failure from a phone, not enough to fill the disk: - running job → full log, never touched (still being appended) - finished <3h → last 5000 lines - finished ≥3h → last 500 lines - >24h → purged (unchanged) Trimming is idempotent (strips a prior marker before re-measuring, shrink-only) so hourly passes never erode the tail, and swaps atomically (temp + rename) so a concurrent poll never reads a half-written log. A marker line records how many lines were dropped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v1.1.3
feat: job titles, bounded output everywhere, inline job id + prompt docs
Job titles: `bash` takes an optional `title`; the job id becomes
`<title>-HH:MM:SS` (seconds added) so the agent can tell its own jobs apart
in `job(list)`. Title is normalized to one `[A-Za-z0-9_-]+` path component so
it can't break the `<id>.log` filename or escape the job dir; blank/garbage
falls back to the neutral `job` label. Never derived from `cmd` (secret-safe).
Bounded output: pagination now caps BOTH line count and bytes (64 KB/page,
8 KB/line) in the shared `paginate`, used by `job(poll)`, inline `bash`, and
`file(read)` — a single 10 MB line or a fat page can no longer flood context.
`next_cursor` tracks lines actually returned so byte-capping never desyncs the
cursor. `file(list/grep)` output is byte-capped with a marker. `file(read)`
now saturates its slice and redirects directories to `list`.
Inline job id: a fast command whose output overflows the first page now
returns its job id + cursor so the agent can poll the rest instead of losing
it (previously inline dropped the id).
Docs: docs/prompts/{system-prompt,skill}.md — ready-to-paste prompt + Claude
Code skill for driving the server, referenced from README.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v1.1.2
feat: agent self-management — sudo (root) by default The agent is meant to run its own box: install its own updates, restart itself, manage services. That needs sudo, which is setuid and blocked by NoNewPrivileges=true. Flip the shipped unit to NoNewPrivileges=false and have the installer grant the run user NOPASSWD:ALL in /etc/sudoers.d/mcp-ssh (validated with visudo). The auth-gated MCP shell is now root-capable by design; README + docs/deploy.md document the blast radius and the lock-down path (remove sudoers + set NoNewPrivileges=true) for operators who don't want it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v1.1.1
fix: render job action as flat enum so Claude Desktop can call it
A `///`-documented unit enum makes schemars emit `oneOf` of
`{const, description}`. Claude Desktop mishandles that form and sends an
`action` value serde can't deserialize, so every `job` call fails before
dispatch — visible in the server logs as `tool="bash"` dispatches only,
never `tool="job"`. `bash` (no enum) and `file` (bare `FileAction`, flat
`enum`) were unaffected.
Drop the per-variant doc comments so `JobAction` renders a flat string
`enum` like `FileAction`; the per-action docs already live in the tool and
field descriptions. Regression test locks both action enums to flat `enum`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>