jumpserver-mcp
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., "@jumpserver-mcpList the authorized assets I can access via JumpServer"
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.
jumpserver-mcp
JumpServer (KoKo) bastion connector for WorkBuddy as an MCP stdio server. Ported from dsh-jumpserver (v0.3.1) core. From v0.4.0 it is no longer just "AI safely executes JumpServer commands" — it is an AI infrastructure investigation / fault-analysis platform: fixed read-only probes, structured host inventory, evidence-annotated topology, streaming job model, and an embedded ops console (8 tabs). v0.4.1 adds the fleet layer: bounded-concurrency surveys, config-driven runbooks, multi-host diffing, baseline drift detection, an assets tab, and console access tokens. v0.4.2 adds assertions (runbooks return a verdict), scope checks at session entry, and console-token expiry + rotation. v0.4.3 is a correctness/security release: profile-step assertions aggregate instead of false-failing, jumpserver_job_start runs through the shared permission gate, commandStatus separates "exchange completed" from "command succeeded", the console token is no longer written to disk, maxSessions is actually enforced, and target scope matches exactly unless it declares otherwise. v0.4.4 makes the job stop path correct (one Ctrl+C, PTY held through recovery, maxDuration via stop()) and adds deferred approval for job start. v0.4.5 is the job-lifecycle / isolation release: one interrupt entry point (no more double Ctrl+C), concurrent-safe and idempotent stop(), PTY ownership bound to the session lifecycle (no ghost SESSION_BUSY after a reconnect), a real cursor for jumpserver_job_read, per-conversation job isolation, a corrected sessionScope, and one shared commandStatus → error mapping. v0.5.0 is the distribution / configuration release: the connection four-tuple (host / port / username / password) can come from the WorkBuddy connector form as environment variables (ENV > config.json > default), config.json became OPTIONAL and now carries policy only, the SSH handshake verifies the bastion's host key (TOFU + optional pinned fingerprint), the console gained a 设置 tab showing where every value came from, CI covers Windows × Node 20/22, and the command-classifier corpus was extended to the operator's real command surface. v0.5.1 is the console-stability / distribution-tooling release: the console address is fixed at http://127.0.0.1:8765/ with the access token injected into the page rather than the URL, so it is short, bookmarkable and identical in every process — a second MCP process now adopts that address instead of falling back to a random port whose URL died with it, an already-open tab self-heals after a restart, the handover hint repeats until the page has actually been fetched once, and the listener is released exactly once on shutdown. It also adds the connector package toolchain (validate:connector incl. a de-identification scan, pack:connector) and a local-market verification path so the install-by-form flow can be exercised before submitting for review.
Tools (23)
Tool | Purpose |
| Session state / gateway / current target / permission mode / runtime version / conversation scope; live console URL + token expiry. Strictly read-only |
| Invalidate the console access token. In stable mode (default) the address is unchanged — the open page picks up a fresh token on its next reload; set |
| Establish the persistent SSH/PTY session, wait for the KoKo menu |
| Enter an asset through the KoKo menu (verified by probe) |
| List authorized assets ( |
| Execute one simple command on the currently-entered asset |
| Auto-navigate + verify + execute (one target, one command) |
| Multi-target multi-command batch (target affinity) |
| Leave the current asset, return to the KoKo menu |
| Close the session, release resources |
| Terminal mirror: read the recent PTY event stream |
| Read recent audit entries (in-chat; time displayed in the configured zone) |
| Fixed read-only profile probes -> structured HostInventory |
| Build an evidence-annotated relationship graph between hosts |
| Run a NAMED runbook (config |
| Run one command/profile on N hosts and report the DIFF (groups + outliers) |
| Snapshot a named baseline to |
| Re-inspect and report DRIFT vs a saved baseline |
| Ctrl+C the remote shell NOW, re-verify, out-of-band (does not wait for the running op) |
| Start a streaming job ( |
| Cursor read of a job: what arrived since the last read + |
| Ctrl+C the job, release the shell |
| List this conversation's jobs |
When requireArm: true, jumpserver_arm / jumpserver_disarm (30-minute authorization window) are also registered.
Related MCP server: SSH MCP Server
Architecture highlights
Explicit state machine (V0.1): illegal transitions always collapse to
UNKNOWN; never guess.Command classifier (V0.3.1, 5 risks: READ / PRIVILEGED_READ / UNKNOWN / MODIFY / DANGEROUS); DANGEROUS always requires human approval, even in
FULL_ACCESS.Abort fix (V0.4.0 P0): MCP cancellation /
jumpserver_interrupt/ the console's "Interrupt current command" button all send Ctrl+C to the remote shell and re-run the probe — the connector never declares the asset usable while a foreground job is still running.Structured inspection (V0.4.0 P0):
jumpserver_inspectruns fixed read-only profiles (basic / network / process / service / web / java / database / container / full). A probe that regresses to a non-READ rule is skipped, not silently downgraded.jumpserver_topologybuilds a graph with per-edgeconfidence(HIGH/MEDIUM/LOW) andevidence(nginx upstream /ESTABLISHED …//etc/hosts/ same upstream cluster).Streaming jobs (V0.4.0):
jumpserver_job_*coverstail -f/journalctl -f/tcpdumpstyle commands that never end on their own. Output is harvested from theTerminalObserverstream,maxDurationenforces an upper bound.Job lifecycle & isolation (V0.4.5): one PTY owner, always.
jumpserver_interruptand the console's 中断 button go through a single entry point, so a streaming job is interrupted by exactly ONE Ctrl+C (V0.4.4 sentmanager.interrupt()and thenjobs.stop()= two).JobStore.stop()is idempotent and concurrent-safe: amaxDurationstop racing a manualjumpserver_job_stopshares one probe and one verdict, so a failed verify can no longer be masked as STOPPED. PTY ownership is bound to the session lifecycle —close(), transport loss and a reconnect all release it, so a reconnected conversation is not permanentlySESSION_BUSY; a job whose shell is gone is reportedLOSTeven when the reconnect completed within one pump interval. Jobs are scoped to their conversation (list / read / stop / the RUNNING cap), so a multiplexing host cannot let one conversation read or stop another's job.jumpserver_job_readis a real cursor read (nextSeq→sinceSeq), not a tail of the buffer.Incremental audit (V0.4.0): async append + bounded memory ring (5000) + file-offset incremental tail. The console no longer
readFileSyncs the whole JSONL every second.Audit timezone (V0.4.0): JSONL stays UTC; display uses
timeZone(defaultAsia/Shanghai).Target scope (V0.4.0):
allowedTargets/deniedTargets; deny always wins; every navigation and everyexecagainst an already-entered asset is checked. From V0.4.2 the check runs at the ENTRY ofjumpserver_connect/jumpserver_entertoo, so a denied target never gets an SSH/PTY session opened at all. From V0.4.3 entries declare their intent — exact (192.168.79.10), prefix (trailing./-/:), glob (oa-*) or CIDR (192.168.79.0/24); a bare entry is EXACT, so192.168.79.10no longer also matches192.168.79.100.Version single source (V0.4.0):
package.jsonis the only truth; runtimePLUGIN_VERSIONreads from there.npm versionand the runtime can no longer drift.Bounded-concurrency surveys (V0.4.1):
batchConcurrency(default 1 = strictly sequential) lets inspect / topology / compare / profile_run visit several targets at once. Result order always matches input order; one target's failure never aborts its siblings. One conversation still owns ONE bastion PTY — raisingbatchConcurrencyinterleaves target turns on that single session, it does not open N sessions. A process-wideSemaphore(maxSessions, default 4) caps how many targets are entered simultaneously, which only binds whenbatchConcurrency > 1.Runbooks (V0.4.1):
jumpserver_profile_runexecutes a named, reviewed recipe from configrunbooks. Each step is an inspect profile or an explicit READ command; a non-READ step (or a typo'd profile) is SKIPPED and reported — never silently executed.Runbook assertions (V0.4.2): a step may carry
expect(contains/notContains/matches/exitCode/notEmpty/minLines/message, all AND). The runbook then reports PASS/FAIL per step, per target, and overall — turning a survey into a verdict. An unreachable target auto-fails any assertion it carried. V0.4.3 fixes how a profile step is judged: aprofilestep is ONE logical step reported once (its per-probe detail stays incommands[]/probes[]), and itsexpectis evaluated against the AGGREGATE of every probe — soexpect: { contains: "LISTEN" }no longer FAILs a healthy host just because only thess -lntpprobe prints it. To assert one specific probe useexpect: { probe: "listen", contains: "LISTEN" }; a probe name that does not exist is a config error and skips the step.Command outcome (V0.4.3):
executionStatesays whether the transport exchange completed;commandStatussays what happened to the COMMAND —SUCCESS/EXIT_NONZERO/TIMEOUT/INTERRUPTED/CONNECTION_LOST/UNKNOWN. A completed exchange with a non-zero exit isok: false, and the audit/console report it as a failure (jps -lvexiting 127 readsREAD / EXIT_NONZERO, neverREAD / COMPLETED). V0.4.5 decodes it through ONE shared mapping, soTIMEOUT/CONNECTION_LOST/UNKNOWNkeep their own error code in exec, batch AND compare (V0.4.4 reported every compare failure asCOMMAND_EXIT_NONZERO).Conversation scope (V0.4.3): the scope id is resolved as transport
sessionId→JUMPSERVER_MCP_SESSION→ process isolation. WorkBuddy's stdio model spawns one server per conversation, so process isolation IS the boundary;jumpserver_statusreports which mode is active. A host that multiplexes conversations over one process MUST supply a sessionId, otherwise they share one bastion session — the startup log says so explicitly rather than pretending otherwise. V0.4.5 reports the scope the CALLER is actually on:transportonly when the transport itself supplied a sessionId, otherwise the runtime mode (env/process) — V0.4.4 asked whether the resolved scope ID was non-empty, which is always true, so a plain stdio conversation claimedtransport.Multi-host diff (V0.4.1):
jumpserver_compareruns the SAME read-only command/profile on every target and groups them by signature, reporting each outlier's missing/extra lines versus the majority. Order-insensitive, soss/psline ordering does not create false diffs.Baseline drift (V0.4.1):
jumpserver_baseline_capturepersists a compact state snapshot todata/baselines/<name>.json;jumpserver_baseline_comparere-inspects and reports real drift (ports, disks, services, roles, kernel, cores; load/memory jitter is suppressed). An unreachable host is reported as unreachable, never as "unchanged".SSH host-key verification (V0.5.0): the handshake no longer trusts whoever answers. With
hostFingerprintset, only a byte-identical host key is accepted; without it the first contact is recorded indata/known_hosts.json(TOFU) and every later contact must match. A contradiction is refused asHOST_KEY_MISMATCHcarrying both fingerprints — never auto-re-trusted. The oldalgorithms.serverHostKeyoverride (RSA variants only) is gone, so an ed25519 / ecdsa host key negotiates again.Connection provenance (V0.5.0): every resolved connection value carries the layer it came from (
env/config/default), reported in the console's 设置 tab and in the startup log — "which config am I actually running with" is answerable without opening a file.Console access token (V0.4.1): the embedded console binds to 127.0.0.1 but every request must present a per-process random token (
/?token=…or theX-Console-Tokenheader) — otherwise 403. The token is handed over only in the model's console URL.Token expiry + rotation (V0.4.2): the console token now expires (default 12h;
auditViewer.tokenTtlMinutes, 0 = never). A stale token gets 403 plus anx-console-token-expired: 1marker, so the page shows a "token expired — ask the model to reopen the console" overlay instead of retrying forever.jumpserver_statusreturns the liveconsoleUrl+ expiry, andjumpserver_console_rotate_tokenmints a new token on demand (the old link dies immediately).
Configuration
V0.5.0 splits configuration into two layers.
Connection — owned by the WorkBuddy connector form. Installing the connector pops a form (address / port / username / password); the values are stored on this machine and injected into the stdio process as environment variables, so there is nothing to hand-edit. Resolution order:
ENV > config.json > built-in defaultEnv var | Default | Description |
| — | Bastion gateway host (falls back to |
|
| Bastion SSH port |
| — | Bastion login (falls back to |
| — | Password, resolved per connect; never written to |
|
| Config file path |
| — | Pin one session scope per process |
Policy — still in config.json. Permission mode, target scope, concurrency, asset groups, runbooks and console settings live here. config.json is now OPTIONAL: when the connector form supplies the connection, the file may be absent and every policy field falls back to its default. Copy config.example.json to config.json only when you need those fields.
Field | Default | Description |
| see ENV | Bastion SSH gateway (overridden by ENV) |
| — | Pinned SSH host key, e.g. |
|
| TOFU store used when no fingerprint is pinned |
|
| Env-var name holding the password |
|
|
|
|
| Display zone for audit timestamps (storage is still UTC) |
|
| Allow-list; each entry is exact ( |
|
| Deny-list (checked first, always wins); same exact / prefix / glob / CIDR syntax |
|
| Targets interleaved per batch/inspect/compare call (1..8; 1 = sequential). One conversation = one PTY; this does not multiply sessions |
|
| Cap on targets entered simultaneously (1..16); only binds when |
|
| Named group -> keywords, used by |
|
| Named runbook -> |
|
| JSONL audit sink |
|
| Embedded console |
|
| Console access-token lifetime in minutes (0 = never expires) |
|
| Require |
Register with WorkBuddy
Two paths. They differ in who has to edit a file.
Connector package — for other people. Install from connector/. connector/token-schema.json declares the four connection fields, so a user fills a form once and never touches a config file. connector/mcp.json pins no machine paths — it declares a Node runtime and runs the published package:
{
"mcpServers": {
"jumpserver": {
"type": "stdio",
"command": "npx",
"args": ["-y", "jumpserver-mcp"],
"runtime": { "type": "node", "version": "20" },
"env": {
"JUMPSERVER_HOST": "${JUMPSERVER_HOST}",
"JUMPSERVER_PORT": "${JUMPSERVER_PORT}",
"JUMPSERVER_USERNAME": "${JUMPSERVER_USERNAME}",
"JUMPSERVER_PASSWORD": "${JUMPSERVER_PASSWORD}"
}
}
}
}Two prerequisites must be met before this path works on anyone else's machine:
The package has to be on npm.
mcp.jsonlaunchesnpx -y jumpserver-mcp; untilnpm publishhas run, that name resolves to nothing and the server dies at startup. Confirm withnpm view jumpserver-mcp version.The connector has to pass review. WorkBuddy provides no local install entry for
auth_mode: "token"connectors — zipconnector/and submit it to the WorkBuddy team; it shows up in the connector market once approved.npm run validate:connectorchecks the package against the submission rules (required manifest fields,${VAR}placeholders that must match a form field key exactly,minWorkbuddyVersionhigh enough for the features used, no hardcoded credentials, no machine-specific paths or internal addresses) and exits non-zero on any failure. To exercise the install-by-form flow on your own machine before review, see Verifying the connector locally.
Local checkout — while developing. Point command / args at node <repo>/lib/server.js and put the connection values directly in env. Works immediately, but never ship this form: an absolute path only works on the machine it was written on.
Embedded ops console
Once the MCP process starts it serves a local ops page at a stable address — http://127.0.0.1:8765/ by default (V0.5.1). /api/* still requires the per-process access token, but the server injects it into the page instead of the URL, so the address is short, bookmarkable and byte-identical in every process. Consequences:
No ephemeral fallback. If another MCP process already holds the port, this one adopts the same address (instead of inventing a random port whose URL dies with it) and takes the listener over the moment the owner exits.
The page self-heals. A restarted process re-binds the same address and an already-open tab resumes polling by itself; the 工作台已失效 overlay clears on the first successful poll.
Token expiry is transparent. A 403 marked expired makes the page reload once to pick up a freshly injected token.
Cross-origin requests are refused, so a web page in the local browser cannot read the audit trail or drive
/api/interrupt.
The model receives the URL in a tool response and must open it with present_files (WorkBuddy's built-in preview panel) — do not use the system browser. The hint repeats on every response until the page has actually been fetched once, because a single first-response handover got parked until the end of the task in practice.
Set auditViewer.stableUrl: false to restore the v0.5.0 form: 127.0.0.1:<random>/?token=…, one console per conversation, invalidated when the conversation ends.
Tabs:
Live terminal — real-time PTY mirror, input echo, state-transition markers; "Interrupt current command (Ctrl+C)" / Clear / Auto-scroll.
Assets — last
jumpserver_assetslisting with search, group/platform/status filters, and name/IP/platform/node/role/status columns; roles come from the last topology, status from the last 30 minutes of audit activity. Also lists saved baselines.Topology — last
jumpserver_topologyresult: node cards + edge list (with confidence + evidence) + per-node detail (roles, OS, ports, IPs, in/out edges, memory, load).Jobs — streaming jobs (
jsjob_xxxx) of this conversation: progress bar, Stop button, output tail.Audit log —
AuditStorememory ring (5000) in the configured timezone; supportscallId/batchId; JSONL / CSV export.Stats — risk / target / operation / 14-day activity bars.
Sessions — active sessions + Interrupt / Disconnect buttons.
Settings (V0.5.0) — the live connection and where every value came from (WorkBuddy environment /
config.json/ default), the policy knobs that onlyconfig.jsoncan set, and the recordedknown_hostsentries. The password appears as "configured / not configured" — its value is never rendered.
Approval flow
READ_ONLY auto-runs reads. MODIFY / DANGEROUS / UNKNOWN return COMMAND_APPROVAL_REQUIRED with a redacted reason on the first attempt; the model shows it to the user and retries the same call with confirm: true only after explicit agreement. requireArm: true adds the 30-minute jumpserver_arm / jumpserver_disarm window on top.
allowedTargets / deniedTargets are an independent gate: even with a human-approved READ, a target outside the allow-list still gets TARGET_DENIED.
Development
npm install
npm run typecheck # tsc --noEmit
npm run build # tsc -p tsconfig.json
npm test # builds + runs tests/**/*.test.mjs (node:test)
npm run smoke # MCP stdio JSON-RPC smokeConnector package toolchain (V0.5.1):
npm run validate:connector # submission rules + de-identification scan; non-zero on failure
npm run pack:connector # validate, then build .workbuddy/artifacts/jumpserver-connector-v<version>.zipTest coverage (tests/):
security/command-classifier.test.mjs— the classifier matrix (incl. every fixed case from the review).security/permission-gate.test.mjs— 3 modes x 5 risks + target allow/deny.security/target-scope.test.mjs— deny beats allow, exact / prefix / glob / CIDR matching, blank targets never gated.security/job-start-gate.test.mjs— the streaming-job tool shares the permission gate: READ_ONLY cannot be bypassed with confirm, AUTO asks, FULL_ACCESS still gates DANGEROUS, an unverified target is refused.jumpserver/state-machine.test.mjs— legal/illegal transitions.jumpserver/session-abort.test.mjs— AbortSignal and out-of-band interrupt() must Ctrl+C + re-verify.jumpserver/asset-list.test.mjs—pparse / footer / filter / groups.jumpserver/host-parse.test.mjs— every parser.jumpserver/topology.test.mjs— edge construction and evidence.inspect/profile-safety.test.mjs— every profile command is still READ.jumpserver/concurrency.test.mjs— bounded concurrency order/limit, abort,Semaphore.jumpserver/session-gate.test.mjs— themaxSessionsgate really caps concurrent holders.jumpserver/runbook.test.mjs— non-READ / unknown-profile steps are skipped;evaluateExpectPASS/FAIL matrix;runRunbookintegration (profile step reported ONCE, aggregated assertion,expect.probe).jumpserver/compare.test.mjs— order-insensitive grouping, count-aware (multiset) outliers, failed hosts excluded.runtime/baseline.test.mjs— baseline save/load, name safety, drift diff, jitter suppression.runtime/console-token.test.mjs— 403 without token, 200 with token / header, TTL expiry marker, rotation, and the discovery file never leaking the token.runtime/command-status.test.mjs— exit 0 vs exit 127 (jps -lvmust not be ok=true), timeout, connection lost.runtime/conversation-lifecycle.test.mjs— two conversations never share a bundle/grant; transport sessionId wins; a shared process is reported honestly.runtime/job-lifecycle-v045.test.mjs— one Ctrl+C per interrupt (job vs bare shell), concurrentstop()shares one verdict, PTY ownership released on close / transport loss / reconnect, per-conversation job isolation + cap, cursorjob_read(nextSeq/sinceSeq/droppedChars).runtime/session-scope.test.mjs—jumpserver_status.sessionScopeprojection: a stdio conversation (or theANONYMOUS_SESSION/ env fallback) must never reporttransport.runtime/job-stop-correctness.test.mjs— one Ctrl+C, PTY held through recovery,maxDurationthroughstop().runtime/compare-command-status.test.mjs— a failed probe is excluded from the diff and keeps its own error code.runtime/terminal-markers.test.mjs— connector-internal markers are hidden from the live terminal view.runtime/audit-store.test.mjs— async append, ring cap, incremental tail.runtime/time.test.mjs— timezone formatting.config/config-env.test.mjs—ENV > config.json > defaultresolution, per-value provenance,config.jsonoptional, blank ENV never overriding a real value.jumpserver/host-key.test.mjs— pinned fingerprint accepted / rejected, TOFU first-contact record, a contradiction surfaces asHOST_KEY_MISMATCHinstead of re-trusting.runtime/console-stable-url.test.mjs— the console address is byte-identical across processes, a second process adopts the port instead of falling back, the page self-heals after a restart,/api/*stays token-gated, cross-origin refused.runtime/console-exit-listener.test.mjs— the console listener is released exactly once and survives a restart (a leakedexitlistener used to keep the port bound).security/command-classifier-corpus/*.test.mjs— a wider classifier corpus (SAP / databases, containers / services, filesystem / process / storage, network / dangerous) guarding the READ / MODIFY / DANGEROUS boundary.
CI: .github/workflows/ci.yml — typecheck → build → validate:connector → smoke → test, on Ubuntu and Windows × Node 20/22.
Verifying the connector locally
A auth_mode: "token" connector has no local install button — it reaches the market only after review. To exercise the form-and-install path on your own machine first, point WorkBuddy's connector market at a locally served zip:
npm run market:build # inject connector/ into a copy of the official market index
npm run market:serve # serve it on 127.0.0.1
npm run market:enable # write ~/.workbuddy/connectors/connector-marketplace.json
# restart WorkBuddy, then install "JumpServer" from the connector list
npm run market:status # what is currently overridden
npm run market:disable # restore the official market (also removes the override file)market:enable backs up whatever it overwrites and market:disable restores it. Do not put files directly into ~/.workbuddy/connectors/connectors-marketplace/ — the client rebuilds that directory on every sync (every 10 minutes) and will delete them.
This server cannot be deployed
Maintenance
Related MCP Connectors
Scoped, audited SSH exec, sessions, and SFTP on your saved servers without exposing credentials
Operate Linux, macOS and Windows from your LLM. Every action runs through an auditable allowlist.
Remote shell and detached long-running jobs on your own machines — no SSH, open ports or VPN.
- emisarOAuthdev.emisar
Let AI operate servers without SSH. Choose actions, approve risky changes, and audit every step.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceEnables secure remote access operations through SSH, SFTP, rsync, VPN, and tunneling with enterprise-grade policy enforcement and audit logging. Provides AI assistants with secure, policy-driven access to remote systems while maintaining comprehensive audit trails and zero-trust security.1Apache 2.0
- AlicenseNot gradedqualityDmaintenanceEnables SSH connections and remote command execution with persistent session management and real-time browser-based terminal monitoring. Supports multiple simultaneous SSH sessions with command history tracking and live WebSocket streaming of terminal output.6MIT
- AlicenseBqualityAmaintenanceProvides policy-driven, auditable SSH access to server fleets for AI assistants with zero-trust security controls, command whitelisting, and comprehensive audit logging to safely manage infrastructure.1327Apache 2.0
- AlicenseNot gradedqualityAmaintenanceEnables AI assistants to securely execute SSH commands on remote servers with connection pooling, session isolation, and a web audit panel.3MIT