@aaarc/handfree-ssh-mcp 1.0.2 → 1.0.3

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/README.md CHANGED
@@ -268,7 +268,7 @@ Rules (enforced at config load — bad configs fail fast):
268
268
  - **Independent policy.** The target authenticates with its OWN `username` / `password` / `privateKey`, and its own `whitelist` / `blacklist` / `safeDirectory` / `allowed*Directories` apply. The jump host is purely transport.
269
269
  - **Jump host is still a normal server.** You can run tools against `bastion` directly; its connection is separate from the tunneling one.
270
270
 
271
- **Hot-reload note:** `jumpHost` is connection-level, not policy-level. Editing it in `servers.yaml` while the server is running will NOT take effect the hot-reloader only refreshes whitelist / blacklist / `safeDirectory` / `allowed*Directories`. Restart the MCP server to pick up `jumpHost` changes.
271
+ **Hot-reload note:** `jumpHost` is a connection-level field, so it hot-reloads. Editing a server's `jumpHost` (or any hop's connection fields) in `servers.yaml` resets the affected connection on the fly the change takes effect on the next tool call, no restart needed. This is chain-aware: if an intermediate hop's host / port / credentials change, every target that tunnels through it is reset too.
272
272
 
273
273
  ### Connection lifecycle (connect / reconnect)
274
274
 
@@ -1,6 +1,6 @@
1
1
  export const SERVER_CONFIG = {
2
2
  name: "ssh-mcp-server",
3
- version: "1.0.2",
3
+ version: "1.0.3",
4
4
  };
5
5
  export const SERVER_INSTRUCTIONS = `This server provides SSH access to servers loaded from OpenSSH config (~/.ssh/config by default) and optional YAML config/policy overlays.
6
6
 
@@ -116,18 +116,52 @@ export class SSHConnectionManager {
116
116
  */
117
117
  replaceConfig(configs, enabledServers) {
118
118
  const previous = this.configs;
119
- const changedConnections = [];
119
+ // Pass 1: servers whose own connection fields changed (or that vanished).
120
+ const directlyChanged = new Set();
120
121
  for (const [name, oldConfig] of Object.entries(previous)) {
121
122
  const nextConfig = configs[name];
122
123
  if (!nextConfig || this.connectionFieldsChanged(oldConfig, nextConfig)) {
123
- this.closeClient(name, true);
124
- this.statusCache.delete(name);
125
- changedConnections.push(name);
124
+ directlyChanged.add(name);
126
125
  }
127
126
  }
127
+ // Pass 2: a target must ALSO reset if any hop in its jump chain directly
128
+ // changed — otherwise it keeps tunneling through a stale intermediate hop.
129
+ // Walk both the old and new chains so topology shifts (added/removed hops)
130
+ // are covered too.
131
+ const toReset = new Set(directlyChanged);
132
+ for (const name of Object.keys(previous)) {
133
+ if (toReset.has(name))
134
+ continue;
135
+ if (this.jumpChainTouchesChanged(name, previous, directlyChanged) ||
136
+ this.jumpChainTouchesChanged(name, configs, directlyChanged)) {
137
+ toReset.add(name);
138
+ }
139
+ }
140
+ for (const name of toReset) {
141
+ this.closeClient(name, true);
142
+ this.statusCache.delete(name);
143
+ }
128
144
  this.setConfig(configs, enabledServers);
129
145
  Logger.log(`Hot-reloaded SSH config: ${Object.keys(configs).length} server(s), ` +
130
- `${changedConnections.length} connection(s) reset`, "info");
146
+ `${toReset.size} connection(s) reset`, "info");
147
+ }
148
+ /**
149
+ * Walk `name`'s jump chain in `configMap` and report whether any hop is in the
150
+ * `changed` set. Guards against cycles defensively (config load rejects them).
151
+ * @private
152
+ */
153
+ jumpChainTouchesChanged(name, configMap, changed) {
154
+ const seen = new Set([name]);
155
+ let hop = configMap[name]?.jumpHost;
156
+ while (hop !== undefined) {
157
+ if (changed.has(hop))
158
+ return true;
159
+ if (seen.has(hop))
160
+ break;
161
+ seen.add(hop);
162
+ hop = configMap[hop]?.jumpHost;
163
+ }
164
+ return false;
131
165
  }
132
166
  /**
133
167
  * Set the root directory under which execute-command full-output logs are
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aaarc/handfree-ssh-mcp",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Handfree SSH MCP Server - A hands-free SSH automation tool via MCP protocol",
5
5
  "main": "build/index.js",
6
6
  "type": "module",