@akms/mcp-ssh 0.1.8 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  All notable changes to `@akms/mcp-ssh`. Dates are `YYYY-MM-DD`.
4
4
 
5
+ ## 0.1.9 — 2026-10-19
6
+
7
+ ### Changed
8
+
9
+ - Load SSH and its crypto runtime on the first connection instead of during idle MCP startup or local configuration work. Direct, proxy and jump-host routes share the loaded client constructor.
10
+ - Preserve shared connection attempts and reject a connection closed while the module was loading, before opening a proxy or network transport.
11
+ - Document idle-memory behavior and direct Node launch as an alternative to a persistent npm wrapper.
12
+
5
13
  ## 0.1.8 — 2026-10-19
6
14
 
7
15
  ### Added
package/README.md CHANGED
@@ -276,6 +276,14 @@ Rules are enforced inside `SshConnection.exec` and the SFTP operations — the l
276
276
 
277
277
  The file tools run with this server's own privileges on your machine, so `localPath` is guarded in both directions — but narrowly, covering **credentials** (`~/.ssh`, `authorized_keys`, private keys, `.pem`/`.key`, `~/.aws`, `~/.gnupg`, `~/.kube`, `~/.docker`, `.npmrc`, `.netrc`) and **files that define the guards** (`.claude*` / `.mcp.json` / `claude_desktop_config.json` — where this server's `SSH_*` variables live — plus autostart locations). `.env` uploads, `.bashrc` backups and cron fetches are normal deployment work and are allowed. Symlinks are resolved before matching, so a link into `~/.ssh` does not slip past.
278
278
 
279
+ ## Memory and process lifetime
280
+
281
+ SSH and its crypto runtime load on the first connection instead of during idle MCP startup or local configuration work. The same loaded client supports direct, proxy and jump-host routes. Concurrent callers share the connection attempt, and disconnecting during module loading prevents a later transport from opening. The runtime stays loaded after first use; disconnecting a session closes its transport rather than unloading the module.
282
+
283
+ Each configured host in each MCP client runs its own process. An `npx` registration also keeps an npm wrapper running. For a stable local/global installation, an entry can instead use the absolute Node executable as `command` and the installed package's absolute `lib/cli.js` path as its sole argument, retaining its existing `env`. Avoid paths inside npm's temporary execution cache. The configuration form continues to generate portable `npx` registrations and may restore that launch style when saving an edit.
284
+
285
+ Restart MCP connections after updating to apply the new code. Compare process counts and Windows Private Bytes separately from resident memory (RSS), including npm wrappers in the total.
286
+
279
287
  ## 📄 Logging
280
288
 
281
289
  Everything goes to **stderr** (stdout is the MCP protocol channel). `SSH_MCP_LOG_LEVEL` picks the level — `silent` / `debug` / `info` / `warn` / `error`, default `info`. Guard rejections are logged at `warn` with the full command, and that log is the audit trail for what the agent tried; full command text is otherwise only logged at `debug`, while `password`, `passphrase`, `privateKey` and `token` meta keys are always masked.
package/lib/cli.cjs CHANGED
@@ -9,7 +9,6 @@ var child_process = require('child_process');
9
9
  var crypto = require('crypto');
10
10
  var stream = require('stream');
11
11
  var string_decoder = require('string_decoder');
12
- var ssh2 = require('ssh2');
13
12
  var timers = require('timers');
14
13
  var util = require('util');
15
14
  var smolToml = require('smol-toml');
@@ -21,7 +20,6 @@ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
21
20
  var fs7__default = /*#__PURE__*/_interopDefault(fs7);
22
21
  var os5__default = /*#__PURE__*/_interopDefault(os5);
23
22
  var path__default = /*#__PURE__*/_interopDefault(path);
24
- var ssh2__default = /*#__PURE__*/_interopDefault(ssh2);
25
23
 
26
24
  // src/_defs/defaults.ts
27
25
  var LOG_LEVEL_ENV = "SSH_MCP_LOG_LEVEL";
@@ -38,7 +36,7 @@ var CWD_MARKER = "__AKMS_SSH_CWD__";
38
36
  var MAX_DIRECTORY_ENTRIES = 1e3;
39
37
  var SERVER_NAME = "akms-mcp-ssh";
40
38
  var PACKAGE_NAME = "@akms/mcp-ssh";
41
- var SERVER_VERSION = "0.1.8";
39
+ var SERVER_VERSION = "0.1.9";
42
40
 
43
41
  // src/_defs/command-rules.ts
44
42
  var CRITICAL_PATH_PATTERN = new RegExp(
@@ -1362,7 +1360,6 @@ var GuardRejectionError = class extends Error {
1362
1360
  };
1363
1361
 
1364
1362
  // src/modules/ssh/ssh-connection.ts
1365
- var { Client } = ssh2__default.default;
1366
1363
  var OUTPUT_HARD_CAP_MULTIPLIER = 4;
1367
1364
  var PRIVATE_KEY_CACHE = /* @__PURE__ */ new Map();
1368
1365
  var WINDOWS_AGENT_PIPE = "\\\\.\\pipe\\openssh-ssh-agent";
@@ -1504,7 +1501,7 @@ function openProxyCommand(profile, hop) {
1504
1501
  }
1505
1502
  };
1506
1503
  }
1507
- function openJumpTransport(profile, jump, proxy, onHostKeySeen) {
1504
+ function openJumpTransport(profile, jump, proxy, onHostKeySeen, Client) {
1508
1505
  const label = `${profile.name} jump host`;
1509
1506
  const endpoint = `${jump.username}@${jump.host}:${jump.port}`;
1510
1507
  const _logger = $logger.child({ context: "openJumpTransport", profile: profile.name, jump_host: jump.host });
@@ -1569,7 +1566,7 @@ function openJumpTransport(profile, jump, proxy, onHostKeySeen) {
1569
1566
  jumpClient.connect(jumpConfig);
1570
1567
  });
1571
1568
  }
1572
- async function openTransport(profile, onJumpHostKeySeen) {
1569
+ async function openTransport(profile, onJumpHostKeySeen, Client) {
1573
1570
  let proxy = null;
1574
1571
  if (profile.proxyCommand != null) {
1575
1572
  proxy = openProxyCommand(profile, profile.jump ?? profile);
@@ -1578,7 +1575,7 @@ async function openTransport(profile, onJumpHostKeySeen) {
1578
1575
  return proxy;
1579
1576
  }
1580
1577
  try {
1581
- const jumpTransport = await openJumpTransport(profile, profile.jump, proxy, onJumpHostKeySeen);
1578
+ const jumpTransport = await openJumpTransport(profile, profile.jump, proxy, onJumpHostKeySeen, Client);
1582
1579
  return jumpTransport;
1583
1580
  } catch (ex) {
1584
1581
  proxy?.close();
@@ -1642,6 +1639,11 @@ var SshConnection = class {
1642
1639
  get wasDropped() {
1643
1640
  return this.droppedAfterReady;
1644
1641
  }
1642
+ assertNotDisposed() {
1643
+ if (this.disposed == true) {
1644
+ throw new Error(`ssh connection for '${this.profile.name}' has been closed; open a new session`);
1645
+ }
1646
+ }
1645
1647
  /**
1646
1648
  * Opens the connection, or joins the in-flight attempt when one is already running.
1647
1649
  *
@@ -1652,9 +1654,7 @@ var SshConnection = class {
1652
1654
  * unreachable, the handshake times out, or the carrier cannot be opened.
1653
1655
  */
1654
1656
  async connect(onProgress) {
1655
- if (this.disposed == true) {
1656
- throw new Error(`ssh connection for '${this.profile.name}' has been closed; open a new session`);
1657
- }
1657
+ this.assertNotDisposed();
1658
1658
  if (this.client != null) {
1659
1659
  return;
1660
1660
  }
@@ -1673,12 +1673,14 @@ var SshConnection = class {
1673
1673
  this.observedFingerprint = fingerprint;
1674
1674
  });
1675
1675
  this.connecting = (async () => {
1676
+ const { default: { Client } } = await import('ssh2');
1677
+ this.assertNotDisposed();
1676
1678
  if (this.profile.jump != null || this.profile.proxyCommand != null) {
1677
1679
  onProgress?.("transport");
1678
1680
  }
1679
1681
  const transport = await openTransport(this.profile, (fingerprint) => {
1680
1682
  this.observedJumpFingerprint = fingerprint;
1681
- });
1683
+ }, Client);
1682
1684
  if (this.disposed == true) {
1683
1685
  transport?.close();
1684
1686
  throw new Error(`ssh connection for '${this.profile.name}' was closed during the handshake`);