jms mcp
by MiFaZhan
README.md
# jms-client
[](https://github.com/MiFaZhan/jms-client/releases/latest)
[](LICENSE)
[](go.mod)
**A command-line client and MCP server for [JumpServer v4](https://docs.jumpserver.org/zh/v4/) bastion hosts.**
One self-contained binary that reaches the assets behind your bastion the same way the web terminal does — no VPN client, no SSH config, no per-host credentials.
Use it on its own to list assets, run commands, open shells and move files. Run `jms mcp` and an AI assistant (Claude Desktop, Claude Code, Codex, Cursor, pi, …) can do the same things through pooled, audited connections.
```console
$ jms exec web-01 "systemctl is-active nginx"
active
```
**English** · [简体中文](README.zh-CN.md)
**Docs:** [Command reference](docs/command-reference.md) ·
[Addresses and proxies](docs/networking.md) ·
[Troubleshooting](docs/troubleshooting.md) ·
[Design](DESIGN.md)
---
## Why this exists
Reaching a bastion usually means a browser tab, or a `~/.ssh/config` you maintain by hand. Neither composes with a shell pipeline, a build script, or an AI agent.
`jms-client` talks to the same two JumpServer APIs the web console uses (Core's REST API and KoKo's terminal gateway), so:
- **Credentials never sit in a config file.** The password and TOTP secret go to your OS credential store. The config file holds addresses and usernames only, so it is safe to sync, back up, or commit.
- **One login serves many commands.** Connections are pooled per server/asset/account/protocol and reused.
- **The same config works inside and outside the office.** Give a server both an internal and an external address and the client picks whichever answers.
- **Everything is logged locally.** Every command an AI agent runs is written to a JSONL audit stream you can read, follow, or join interactively.
## Features
| | |
|---|---|
| **Single static binary** | No runtime dependencies. Linux, macOS, Windows on x86_64 and arm64. |
| **Dual-address failover** | Internal/external addresses probed and selected automatically, with a remembered last-good address for fast cold starts. |
| **Connection pooling** | Login sessions and terminals are reused per `server/asset/account/protocol`. Idle connections are reaped; broken ones are rebuilt. |
| **Credentials in the OS store** | Windows Credential Manager, macOS Keychain, Linux Secret Service. Environment-variable fallback for containers and CI. |
| **Asset management** | Create, update, delete and list assets, and grant or revoke access — all from the CLI or MCP, without leaving the terminal for the web console. |
| **MCP server** | Fifteen tools over stdio for AI clients, sharing one pooled process. |
| **Local audit stream** | Structured JSONL per process; `jms tail` to follow it, `jms attach` to watch and join. |
| **Two terminal backends** | WebSocket (default, same path as the web console) and SSH, with automatic selection. |
## Requirements
- **JumpServer v4** with the **KoKo** component reachable from your machine. JumpServer v3 and earlier are not supported.
- An account on that JumpServer with the assets you want to reach already authorized.
- The WebSocket backend requires nothing beyond a reachable HTTP(S) address. The SFTP and SSH features additionally require KoKo's SSH port (default `2222`) to be open on the network path — many deployments only expose the web port, so check before relying on them.
## Install
### Prebuilt binaries (recommended)
Download the archive for your platform from [**Releases**](https://github.com/MiFaZhan/jms-client/releases/latest) and put the `jms` binary (`jms.exe` on Windows) on your `PATH`.
| Platform | File |
|---|---|
| Windows x86_64 | `jms-client_<version>_windows_amd64.zip` |
| Windows arm64 | `jms-client_<version>_windows_arm64.zip` |
| macOS Apple Silicon | `jms-client_<version>_darwin_arm64.tar.gz` |
| macOS Intel | `jms-client_<version>_darwin_amd64.tar.gz` |
| Linux x86_64 | `jms-client_<version>_linux_amd64.tar.gz` |
| Linux arm64 | `jms-client_<version>_linux_arm64.tar.gz` |
`checksums.txt` in the same release lets you verify the download:
```sh
sha256sum -c --ignore-missing checksums.txt # Linux
shasum -a 256 -c --ignore-missing checksums.txt # macOS
```
### Scoop (Windows)
```powershell
scoop bucket add mifazhan https://github.com/MiFaZhan/scoop-bucket
scoop install jms-client
```
Upgrade later with `scoop update jms-client`.
### Go
Requires Go 1.25 or newer:
```sh
go install github.com/MiFaZhan/jms-client/cmd/jms@latest
```
### From source
```sh
git clone https://github.com/MiFaZhan/jms-client
cd jms-client
go build -o jms ./cmd/jms
```
### For MCP clients only
If all you want is the AI integration and you would rather not manage a binary yourself, download **`jms-client.mcpb`** from [Releases](https://github.com/MiFaZhan/jms-client/releases/latest) and open it with your MCP client (Claude Desktop and other MCPB-aware clients handle this by double-click). That single bundle contains the binaries for all six platform/architecture combinations; a bundled launcher picks the right one at startup.
## Quick start
### 1. Store your server
```console
$ jms config add bastion
Internal URL (http(s)://..., empty to skip): https://jumpserver.corp.example.com/
External URL (http(s)://..., empty to skip):
Username: alice
Password:
Confirm password:
TOTP secret (base32, empty to enter the code each time):
Server "bastion" saved to /home/alice/.config/jms/config.toml
Server "bastion" is now the default.
```
Either address may be left empty, but at least one is required. The credentials are validated against a reachable address **before** anything is written, so a mistyped password changes nothing on disk. The first server you add becomes the default.
### 2. Check it
```console
$ jms config test
Server: bastion (default)
Endpoint URL Reachable Probe Login
external https://jumpserver.corp.example.com/ yes 12ms ok
Credentials: keyring
```
`config test` probes every configured address and attempts a real login on each reachable one, so a network problem is reported separately from a rejected password, and you can see which addresses actually work. An address can pass the TCP probe and still fail the login — a VPN or proxy can accept the connection with nothing useful behind it.
### 3. Use it
```console
$ jms ls # assets you are authorized for
$ jms ls -q web # search by keyword
$ jms exec web-01 "uptime" # one command, exit code passes through
$ jms login web-01 # interactive shell (Ctrl+] to exit)
$ jms sftp web-01:/var/log/app.log . # download
$ jms sftp ./config.yml web-01:/etc/app/ # upload
```
## Connecting an AI assistant (MCP)
`jms mcp` runs a [Model Context Protocol](https://modelcontextprotocol.io) server over stdio. Any MCP-capable client can then list assets, run commands and transfer files through the same pooled connections your shell uses.
```json
{
"mcpServers": {
"jms": {
"command": "jms",
"args": ["mcp"]
}
}
}
```
Print this snippet at any time with:
```sh
jms mcp --print-config
```
Because the MCP process has no command-line flags (your client launches one long-lived process and cannot add arguments per call), the endpoint, backend and command timeout are steered with environment variables in that client entry:
```json
{
"mcpServers": {
"jms": {
"command": "jms",
"args": ["mcp"],
"env": {
"JMS_MCP_BACKEND": "ws",
"JMS_MCP_EXEC_TIMEOUT": "10m"
}
}
}
}
```
### Tools
| Tool | Purpose | Required arguments |
|---|---|---|
| `jms_ls` | List or search authorized assets | — |
| `jms_resolve_asset` | Resolve an asset to its address, account and protocol | `asset` |
| `jms_exec` | Run one command and return its output | `asset`, `command` |
| `jms_sftp_upload` | Upload a local file to an asset | `src`, `asset`, `dst` |
| `jms_sftp_download` | Download a file from an asset | `asset`, `src`, `dst` |
| `jms_sftp_relay` | Stream a file directly between two assets | `src_spec`, `dst_spec` |
| `jms_config_list` | List configured servers and their credential source | — |
| `jms_asset_list` | List every asset on the server (admin view) | — |
| `jms_asset_show` | Show one asset in detail | `target` |
| `jms_asset_create` | Create a host asset | `name`, `address` |
| `jms_asset_update` | Rename, re-address or re-comment an asset | `target` |
| `jms_asset_delete` | Delete an asset | `target` |
| `jms_asset_grant` | Grant users access to an asset | `target` |
| `jms_asset_permissions` | List permissions, optionally for one asset | — |
| `jms_asset_revoke` | Revoke a permission | `target` |
An eighth tool, `jms_pool_status`, is registered when `JMS_MCP_DEBUG=1` and reports the shape of the connection pool. It is for diagnosing the server, not for daily use.
Most tools accept optional `server`, `config_path`, `account`, `protocol` and `timeout` arguments. Remote specs use the form `<asset>[@<server>]:<path>`.
Two behaviours worth knowing when you write prompts for an agent:
- **A non-zero exit code is not reported as a failure.** `jms_exec` returns the command's output; the remote exit status is recorded in the audit log but not surfaced in the tool result. If your agent must branch on success, make the command say so (`… && echo OK || echo FAIL`).
- **Commands are never replayed.** If a call fails after the command was already delivered to the host, the client reports the error instead of retrying, because `rm`, `reboot` and friends are not idempotent. A dropped connection triggers one recovery attempt through a fresh login, but only for a command that produced no output at all.
## Commands at a glance
| Command | What it does |
|---|---|
| `jms config add \| list \| remove \| set-default \| test` | Manage server entries and their credentials. |
| `jms ls [server] [-q keyword]` | List or search authorized assets. |
| `jms exec <target> <command...>` | Run one command on an asset. |
| `jms login <target>` | Open an interactive shell. |
| `jms sftp <src> <dst>` | Upload or download a file. |
| `jms ssh-pipe <target> [command...]` | Bridge stdio to an asset (for `rsync -e`). |
| `jms tail [-f]` | Follow the local audit stream. |
| `jms attach` | Watch a running host, optionally joining its session. |
| `jms log show` | Query the audit log. |
| `jms mcp` | Run the stdio MCP server. |
| `jms asset list \| show \| create \| update \| delete` | Manage assets on the bastion. |
| `jms asset grant \| permissions \| revoke` | Manage who may reach an asset. |
### Provisioning assets
JumpServer separates "an asset exists" from "a user may reach it": creating
an asset records it, but only an `AssetPermission` exposes it to a user.
That is why a new asset appears in `jms asset list` (the admin view) and
not in `jms ls` (the per-user view) until it has been granted.
`--grant-me` on create does both steps in one command:
```bash
jms asset create "host-preview-01" 192.168.100.252 \
--account yunze --username yunze --password '…' --grant-me
```
Grants can also be managed separately:
```bash
jms asset grant "host-preview-01" --self # or --account @ALL --protocol all
jms asset permissions "host-preview-01" # what grants this asset
jms asset revoke "grant-host-preview-01" --yes
```
Deleting an asset does not delete the permissions that referenced it; the
two are separate objects, so `jms asset revoke` is the command that ends
access.
Target syntax is `<asset>@<server>`; omitting `@<server>` uses the default server.
**Full reference:** [docs/command-reference.md](docs/command-reference.md) ·
[简体中文](docs/command-reference.zh-CN.md)
## Configuration
Config lives at:
| Platform | Path |
|---|---|
| Windows | `%AppData%\jms\config.toml` |
| Linux / macOS | `~/.config/jms/config.toml` (or `$XDG_CONFIG_HOME/jms/config.toml`) |
Override with the `--config` flag or the `JMS_CONFIG` environment variable. Relocating the file also relocates the audit directory, because both live beside it.
```toml
version = 2
default_server = "bastion"
# Optional global proxy policy. Omitted means: connect directly.
# proxy = "socks5://127.0.0.1:1080"
[servers.bastion]
internal = "http://192.168.1.10:2280/" # internal address (optional)
external = "https://bastion.example.com/" # external address (optional)
username = "alice"
# prefer = "external" # try this address first; default is internal
# pin = "internal" # use only this address: no probing, no failover
# ssh_port = 2222 # KoKo SSH port if not the default
# proxy = "direct" # force a direct connection for this server
[servers.staging]
external = "https://jumpserver.staging.example.com/"
username = "alice"
prefer = "external"
```
Every field except the username is optional, but at least one address is required.
When a server has both addresses, the client probes them in turn and logs into the first that answers, remembering the last-good one for ten minutes. The client **connects directly by default** and ignores `HTTP_PROXY` and friends — a proxy must be configured explicitly.
**Details:** [docs/networking.md](docs/networking.md) ·
[简体中文](docs/networking.zh-CN.md)
### Credentials
| Data | Where it lives |
|---|---|
| Aliases, addresses, username, preferences | `config.toml`, plain text (0600) |
| **password**, **TOTP secret** | The OS credential store, under the service name `jms-client` |
The config file is written atomically and never contains a credential, which is what makes it safe to sync between machines. Acknowledge the boundary this implies: it protects against file leakage and copying a config to another machine, not against a malicious process running as the same user.
For headless environments (containers, CI, WSL without a Secret Service) there are environment-variable fallbacks. **The environment takes precedence over the credential store.**
| Variable | Meaning |
|---|---|
| `JMS_PASSWORD_<ALIAS>` | Password for that server alias. |
| `JMS_OTP_<ALIAS>` | TOTP secret for that server alias. |
Aliases are upper-cased and `-` becomes `_`, so `bastion-eu` becomes `JMS_PASSWORD_BASTION_EU`. An exported-but-empty variable counts as unset rather than as an empty password. If neither source has a credential, the command fails with a message naming the variable to set — it never falls back to storing a plaintext password.
A locked credential store (the usual state of a GNOME Keyring with no desktop session) fails in milliseconds with a message naming the environment variable to set — see [troubleshooting](docs/troubleshooting.md).
### Environment variables
| Variable | Effect |
|---|---|
| `JMS_CONFIG` | Path to the configuration file. |
| `JMS_PASSWORD_<ALIAS>`, `JMS_OTP_<ALIAS>` | Credential fallback (see above). |
| `JMS_ACTOR` | Name recorded in the audit log; use it to tell several clients on one machine apart. |
| `JMS_AUDIT` | Set to `off` to disable the audit log. |
| `JMS_AUDIT_DIR` | Relocate the audit directory. |
| `JMS_AUDIT_FULL` | Set to `1` to record untruncated output previews. |
| `JMS_AUDIT_PREVIEW_BYTES` | Truncation limit for output previews (default 4096). |
| `JMS_MCP_ENDPOINT` | MCP only: force `internal` or `external`. |
| `JMS_MCP_BACKEND` | MCP only: force `ssh`, `ws` or `auto`. |
| `JMS_MCP_EXEC_TIMEOUT` | MCP only: default `jms_exec` timeout, e.g. `10m` or `600`. |
| `JMS_MCP_DEBUG` | MCP only: set to `1` to register `jms_pool_status`. |
| `JMS_WS_DUMP` | Dump raw WebSocket frames, for protocol debugging. |
## Terminal backends
JumpServer's KoKo component offers two terminal channels, and `jms-client` speaks both.
| Backend | Channel | Notes |
|---|---|---|
| `ws` (default) | `/koko/ws/terminal/` | The same path the web console uses. Works wherever the web console works. 30s default command timeout. |
| `ssh` | KoKo SSH port (default 2222) | Native exit statuses per command, no output-marker parsing. Requires the port to be reachable, and SFTP depends on it. |
Select it with `--backend ssh`, or `--backend auto` to try SSH and fall back to WebSocket. The backend that last succeeded for an address is remembered in `state.json`, so a cold start goes straight to it instead of waiting out a handshake on a blocked port. In MCP, the remembered backend also lets the process recover from a stale connection without a restart.
Tools that need SFTP — `jms sftp`, `jms_sftp_upload`, `jms_sftp_download`, `jms_sftp_relay` — require KoKo's SSH port to be open. If file transfers hang or fail while `exec` works fine, that port is the first thing to check.
## Audit log
Every command is recorded as one JSON object per line:
```json
{"ts":"2026-10-05T14:32:01.123+08:00","pid":12345,"actor":"mcp:pi","kind":"exec.end","server":"prod","asset":"web-01","backend":"ssh","endpoint":"external","command":"systemctl status nginx","exit_code":0,"duration_ms":118,"output_bytes":2140,"preview":"● nginx.service ...","pool":"hit","idle_ms":68000}
```
Files are written to `<config dir>/audit/audit-YYYYMMDD-<pid>.jsonl`, one file per process, with `0600` permissions on both the directory and the files. Per-process files avoid the Windows problem of several processes appending to one locked file, and they let a long-running host and a one-shot CLI invocation coexist.
Event kinds: `exec.start`, `exec.end`, `pool.cold`, `pool.hit`, `pool.evict`, `pool.reap`, `session.login`, `session.relogin`, `endpoint.select`, `error`.
Output previews are truncated to 4096 bytes by default (`JMS_AUDIT_PREVIEW_BYTES`, or `JMS_AUDIT_FULL=1` for everything). Nothing in the log interpolates a credential.
## Limitations
- **JumpServer v4 only.** The v3 authentication and terminal APIs differ.
- **`jms sftp` does not do recursive copies or asset-to-asset relay.** Use the MCP `jms_sftp_relay` tool for the latter, or relay through your local machine.
- **32-bit platforms are not built.** Only x86_64 and arm64.
- **The audit log is local.** Nothing is forwarded to a central collector; ship the files yourself if you need to aggregate.
- **A jump-host chain (proxy/jump through another asset) is not implemented.**
**When something goes wrong:** [docs/troubleshooting.md](docs/troubleshooting.md) ·
[简体中文](docs/troubleshooting.zh-CN.md)
## Development
```sh
go build ./... # compile
go test ./... # unit and protocol-regression tests
go vet ./...
gofmt -l . # must print nothing
```
Integration tests connect to a real JumpServer and are gated behind an environment variable; without it they skip, so a fresh clone is green:
```sh
JMS_TEST_SERVER=bastion go test ./...
```
Architecture, the reverse-engineered KoKo protocol details, pool design and failure-handling rules live in [DESIGN.md](DESIGN.md) (written in Chinese).
Releases are cut from GitHub Actions:
```sh
gh workflow run release --repo MiFaZhan/jms-client -f tag=v0.1.7
```
## License
MIT © MiFaZhan. See [LICENSE](LICENSE).
The design and the KoKo protocol findings build on [GCS-ZHN/jms-cli](https://github.com/GCS-ZHN/jms-cli).
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo recent community issues