Skip to content

Commit b333600

Browse files
authored
Merge pull request #11 from hunchom/ssh-mcp-v4-redesign
feat: v4 redesign — 13 fat verb-tools, token-efficient output, adoption layer
2 parents 9b7d519 + 8380db2 commit b333600

108 files changed

Lines changed: 19769 additions & 2598 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/hooks/ssh-bash-nudge.mjs

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
#!/usr/bin/env node
2+
/**
3+
* PreToolUse hook for the Bash tool. Detects a simple ssh/scp/rsync invocation
4+
* against a configured server and prints a soft, non-blocking nudge toward the
5+
* matching ssh_* MCP tool. Best-effort: simple shapes nudged, complex command
6+
* lines passed through. Fail-open -- any error exits 0 with no nudge.
7+
*
8+
* Wired in .claude/settings.json under hooks.PreToolUse, matcher "Bash".
9+
*/
10+
import { readFileSync } from 'fs';
11+
import { fileURLToPath } from 'url';
12+
13+
// Shell metacharacters => the command line is not a simple invocation. Bail.
14+
const COMPLEX = /[|&;<>`]|\$\(/;
15+
16+
/** Configured server names from the project .env (best-effort, never throws). */
17+
export function configuredServers(envPath) {
18+
try {
19+
const text = readFileSync(envPath, 'utf8');
20+
const names = new Set();
21+
for (const line of text.split('\n')) {
22+
// SSH_SERVER_<NAME>_HOST=... -- <NAME> is the server identifier.
23+
const m = /^\s*SSH_SERVER_([A-Za-z0-9]+)_HOST\s*=/.exec(line);
24+
if (m) names.add(m[1].toLowerCase());
25+
}
26+
return [...names];
27+
} catch {
28+
return [];
29+
}
30+
}
31+
32+
/** Strip a leading user@ and return the bare host token, lowercased. */
33+
function bareHost(token) {
34+
const at = token.lastIndexOf('@');
35+
return (at === -1 ? token : token.slice(at + 1)).toLowerCase();
36+
}
37+
38+
/**
39+
* Inspect a Bash command string. Returns { tool, message } when it is a simple
40+
* ssh/scp/rsync call against a configured server, else null. Never throws.
41+
*/
42+
export function detectSshNudge(command, servers) {
43+
try {
44+
if (!command || typeof command !== 'string') return null;
45+
if (!Array.isArray(servers) || servers.length === 0) return null;
46+
if (COMPLEX.test(command)) return null;
47+
48+
const set = new Set(servers.map((s) => String(s).toLowerCase()));
49+
const tokens = command.trim().split(/\s+/);
50+
const head = tokens[0];
51+
52+
if (head === 'ssh') {
53+
// First token after the flags that is not a flag or a flag-value is the host.
54+
for (let i = 1; i < tokens.length; i++) {
55+
const t = tokens[i];
56+
if (t === '-p' || t === '-i' || t === '-l' || t === '-o' || t === '-F') {
57+
i++; // skip this flag's value
58+
continue;
59+
}
60+
if (t.startsWith('-')) continue;
61+
return set.has(bareHost(t))
62+
? { tool: 'ssh_run', message: nudgeText(bareHost(t), 'ssh_run', 'ssh') }
63+
: null;
64+
}
65+
return null;
66+
}
67+
68+
if (head === 'scp' || head === 'rsync') {
69+
// Any non-flag token of the form host:path against a configured server.
70+
for (let i = 1; i < tokens.length; i++) {
71+
const t = tokens[i];
72+
if (t.startsWith('-')) continue;
73+
const colon = t.indexOf(':');
74+
if (colon > 0 && set.has(bareHost(t.slice(0, colon)))) {
75+
const host = bareHost(t.slice(0, colon));
76+
return { tool: 'ssh_file', message: nudgeText(host, 'ssh_file', head) };
77+
}
78+
}
79+
return null;
80+
}
81+
82+
return null;
83+
} catch {
84+
return null;
85+
}
86+
}
87+
88+
/** The soft nudge text shown in the PreToolUse hook output. */
89+
function nudgeText(host, tool, rawCmd) {
90+
return `[ssh-manager] '${host}' is a configured server. Consider the `
91+
+ `${tool} MCP tool instead of raw \`${rawCmd}\` -- pooled connection, `
92+
+ `bounded output, structured result. (This is a hint, not a block.)`;
93+
}
94+
95+
// --- CLI shell: invoked by Claude Code as a PreToolUse hook --------------
96+
// Reads the hook JSON payload on stdin; prints a nudge on stdout if one
97+
// applies; always exits 0 so the Bash call is never blocked.
98+
function main() {
99+
let raw = '';
100+
try {
101+
raw = readFileSync(0, 'utf8');
102+
} catch {
103+
process.exit(0); // no stdin -> nothing to inspect
104+
}
105+
106+
let payload;
107+
try {
108+
payload = JSON.parse(raw);
109+
} catch {
110+
process.exit(0); // unparseable payload -> fail open
111+
}
112+
113+
const command = payload && payload.tool_input && payload.tool_input.command;
114+
const envPath = fileURLToPath(new URL('../../.env', import.meta.url));
115+
const nudge = detectSshNudge(command, configuredServers(envPath));
116+
if (nudge) console.log(nudge.message);
117+
process.exit(0);
118+
}
119+
120+
// Run main() only when executed directly, never when imported by a test.
121+
if (import.meta.url === `file://${process.argv[1]}`) {
122+
main();
123+
}

.claude/settings.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"hooks": {
3+
"PreToolUse": [
4+
{
5+
"matcher": "Bash",
6+
"hooks": [
7+
{
8+
"type": "command",
9+
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/ssh-bash-nudge.mjs\""
10+
}
11+
]
12+
}
13+
]
14+
}
15+
}

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<!-- gitnexus:start -->
22
# GitNexus — Code Intelligence
33

4-
This project is indexed by GitNexus as **claude-code-ssh** (1340 symbols, 3668 relationships, 111 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
4+
This project is indexed by GitNexus as **claude-code-ssh** (1341 symbols, 3675 relationships, 111 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
55

66
> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.
77

CLAUDE.md

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ This file provides guidance to Claude Code when working on this repository.
66

77
**claude-code-ssh** is an MCP server that gives Claude Code direct SSH access to a configured fleet of servers. The goal: Claude stops being a read-only assistant and becomes a hands-on operator — reading logs, editing configs, running backups, deploying, debugging — without a human typing commands between them.
88

9-
51 tools, 7 groups, opt-in per user. Connection pooling, streaming exec, head+tail output truncation, ASCII-only rendering.
9+
13 fat verb-tools, each covering one domain via an `action` enum. Always loaded (un-deferred). Connection pooling, streaming exec, head+tail output truncation, command-output compression, ASCII-only rendering.
1010

1111
## Architecture
1212

13-
- **`src/index.js`** — MCP server entry, registers all 51 tools via `registerToolConditional()`
13+
- **`src/index.js`** — MCP server entry, registers the 13 v4 tools via `registerToolConditional()`; descriptions sourced from `src/tool-descriptions.js`
1414
- **`src/tools/*.js`** — 17 modular handler files, one per logical tool area (exec, files, backup, db, etc.)
1515
- **`src/tool-registry.js`** — tool metadata + group membership (core, sessions, monitoring, backup, database, advanced, gamechanger)
1616
- **`src/tool-config-manager.js`** — per-user enablement via `~/.ssh-manager/tools-config.json`
@@ -58,14 +58,14 @@ ssh-manager tools export-claude # Export auto-approval config
5858

5959
**Tool Groups**: core (5), sessions (4), monitoring (6), backup (4), database (4), advanced (14)
6060

61-
**Modes**: all (37 tools, ~43.5k tokens), minimal (5 tools, ~3.5k tokens), custom (variable)
61+
**Modes**: v4 surface is always loaded (13 tools, ~5k tokens); the per-group mode system is deprecated.
6262

6363
See [docs/TOOL_MANAGEMENT.md](docs/TOOL_MANAGEMENT.md) for complete guide.
6464

6565
### Development and Testing
6666
```bash
6767
npm start # Start MCP server (requires stdin)
68-
npm test # Run 551 tests across 26 suites
68+
npm test # Run 1028 tests
6969
./scripts/validate.sh # Syntax + startup check
7070
node --check src/index.js # JavaScript syntax only
7171
```
@@ -204,10 +204,25 @@ claude mcp add ssh-manager node /absolute/path/to/claude-code-ssh/src/index.js
204204

205205
Configuration is stored in `~/.config/claude-code/claude_code_config.json`
206206

207+
## Using the SSH Tools
208+
209+
**For any server configured in this MCP server, use the `ssh_*` MCP tools — not raw `ssh`, `scp`, or `rsync` through the Bash tool.**
210+
211+
The 13 v4 tools (`ssh_run`, `ssh_file`, `ssh_find`, `ssh_logs`, `ssh_service`, `ssh_health`, `ssh_db`, `ssh_backup`, `ssh_session`, `ssh_net`, `ssh_docker`, `ssh_fleet`, `ssh_plan`) are not a read-only convenience layer — they are the intended way to operate the fleet. Reach for them first.
212+
213+
Why they beat raw `ssh` in Bash:
214+
215+
- **Connection pooling** — the MCP server holds persistent SSH connections, so there is no per-call handshake. Raw `ssh` in Bash reconnects every single time.
216+
- **Bounded output** — results are compressed and head+tail truncated, so a noisy command (`journalctl`, `ps`, a 100k-line log) will not flood the context window. Raw `ssh` dumps everything.
217+
- **Credential handling** — passwords and sudo passwords are passed via stdin or env, never leaked on the argv of a `ps`-visible process. Raw `ssh` with an inline password is exposed.
218+
- **Structured results** — per-segment exit codes for command chains, typed service/health snapshots, SFTP transfers with sha256 verification. Raw `ssh` gives an unstructured terminal dump.
219+
220+
Raw `ssh` through Bash is acceptable only for a host that is **not** in the MCP configuration. Run `ssh_fleet action: servers` to see which servers are configured.
221+
207222
<!-- gitnexus:start -->
208223
# GitNexus — Code Intelligence
209224

210-
This project is indexed by GitNexus as **claude-code-ssh** (1340 symbols, 3668 relationships, 111 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
225+
This project is indexed by GitNexus as **claude-code-ssh** (1341 symbols, 3675 relationships, 111 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
211226

212227
> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.
213228

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ flowchart LR
103103
C[Claude Code]
104104
end
105105
subgraph mcp["claude-code-ssh (MCP server)"]
106-
T[51 typed tools]
106+
T[13 verb-tools]
107107
P[ssh2 connection pool]
108108
O[head+tail output]
109109
T --> P
@@ -121,9 +121,9 @@ flowchart LR
121121
B --> H1
122122
```
123123

124-
- **51 typed tools across 7 groups**shell, files, databases, backups, deploys, tunnels, sessions. Claude picks; you never enumerate.
124+
- **13 fat verb-tools**one per domain (run, files, logs, db, docker, services, ...), each with an action enum. Claude picks; you never enumerate.
125125
- **Pooled connections** — 30-minute idle timeout. Reconnects cost zero.
126-
- **Opt-in per group**minimal mode (5 tools, ~3.5k tokens) to full mode (51 tools, ~43k tokens).
126+
- **Always loaded**the 13-tool schema is small enough (~5k tokens) to stay un-deferred. No per-group opt-in to manage.
127127

128128
## Install
129129

@@ -224,8 +224,8 @@ Claude already has a bash tool. Why this server?
224224
| Sudo password handling | argv / `echo pwd \| sudo -S` (leaks to `ps`) | stdin only, never argv |
225225
| DB query safety | Claude can send `DROP TABLE` | token-level SQL parser, SELECT only |
226226
| Host key verification | TOFU by default, no MITM check | SHA256 fingerprint match, strict mode available |
227-
| Tool surface | 1 generic shell exec | 51 typed tools with JSON schemas |
228-
| Context cost | unbounded per command | ~3.5k tokens minimal mode, ~43k full |
227+
| Tool surface | 1 generic shell exec | 13 verb-tools with JSON schemas |
228+
| Context cost | unbounded per command | ~5k tokens, always loaded |
229229

230230
The pitch isn't "Claude couldn't SSH before." The pitch is "Claude could SSH, but badly — and one bad command on prod is one too many."
231231

@@ -241,7 +241,7 @@ What this doesn't do, today, honestly:
241241
## Testing
242242

243243
```bash
244-
npm test # 551 tests across 26 suites
244+
npm test # 1031 tests
245245
```
246246

247247
## Layout

docs/TOOL_MANAGEMENT.md

Lines changed: 18 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,18 @@
22

33
## Overview
44

5-
claude-code-ssh provides **37 tools** organized into **6 functional groups**. You can enable or disable tool groups to customize your experience and reduce context usage in Claude Code.
6-
7-
### Why Manage Tools?
8-
9-
- **Reduce Context Usage**: By default, all 37 tools consume ~43.5k tokens in Claude Code. Minimal mode uses only ~3.5k tokens (92% reduction)
10-
- **Fewer Approval Prompts**: Only enabled tools require approval in Claude Code
11-
- **Faster Loading**: Less tools mean faster MCP server startup
12-
- **Cleaner Interface**: Only see the tools you actually use
13-
14-
## Quick Start
15-
16-
### View Current Configuration
17-
18-
```bash
19-
ssh-manager tools list
20-
```
21-
22-
### Interactive Configuration Wizard
23-
24-
```bash
25-
ssh-manager tools configure
26-
```
27-
28-
Choose from three modes:
29-
1. **All tools** (37 tools) - Full feature set, recommended for most users
30-
2. **Minimal** (5 tools) - Only core operations, maximum efficiency
31-
3. **Custom** - Pick which groups to enable
5+
> **v4 update:** the v4 surface is **13 fat verb-tools**, always loaded. The
6+
> per-group enable/disable model described below belonged to the v3 51-tool
7+
> surface and no longer applies — there are no tool *groups* in v4. The 13
8+
> tools serialize to roughly 5k schema tokens, small enough that Claude Code
9+
> keeps them loaded without `ToolSearch`. This guide is retained for historical
10+
> reference; the `ssh-manager tools` CLI subcommands are deprecated.
11+
12+
claude-code-ssh provides **13 tools**, each a verb-tool covering one domain
13+
through an `action` enum (`ssh_run`, `ssh_file`, `ssh_find`, `ssh_logs`,
14+
`ssh_service`, `ssh_health`, `ssh_db`, `ssh_backup`, `ssh_session`, `ssh_net`,
15+
`ssh_docker`, `ssh_fleet`, `ssh_plan`). All 13 are registered unconditionally —
16+
there is nothing to enable or disable.
3217

3318
### Enable/Disable Specific Groups
3419

@@ -157,8 +142,8 @@ Advanced features for power users:
157142
}
158143
```
159144

160-
- **Enabled tools**: 37/37
161-
- **Context usage**: ~43.5k tokens
145+
- **Enabled tools**: the full v3 tool set
146+
- **Context usage**: the full v3 schema cost
162147
- **Best for**: Users who need all features
163148

164149
### Minimal Mode
@@ -198,7 +183,7 @@ Advanced features for power users:
198183
}
199184
```
200185

201-
- **Enabled tools**: Custom (5-37 tools)
186+
- **Enabled tools**: Custom (a hand-picked v3 subset)
202187
- **Context usage**: Varies based on selection
203188
- **Best for**: Tailoring to specific workflows
204189

@@ -270,7 +255,7 @@ ssh-manager tools enable monitoring
270255
ssh-manager tools configure # Choose "1) All tools"
271256
```
272257

273-
**Result**: 37 tools = ~43.5k tokens
258+
**Result**: the full v3 tool set loaded
274259

275260
### Scenario 3: Database Administrator
276261

@@ -431,7 +416,7 @@ Add comments to your config file to remember why you enabled specific groups:
431416

432417
### Q: Will existing users see any changes?
433418

434-
**A**: No. If no configuration file exists, all 37 tools are enabled by default (current behavior).
419+
**A**: No. Under the v3 model, with no configuration file every tool was enabled by default. The v4 surface is always fully loaded -- there is nothing to enable or disable.
435420

436421
### Q: Can I enable individual tools without enabling the whole group?
437422

@@ -451,7 +436,7 @@ Add comments to your config file to remember why you enabled specific groups:
451436

452437
### Q: How much does minimal mode actually save?
453438

454-
**A**: Minimal mode (5 tools) uses ~3.5k tokens vs all tools (37 tools) at ~43.5k tokens. That's a **92% reduction** or **~40k tokens saved**.
439+
**A**: Under the v3 model, minimal mode (5 tools) cut the schema cost sharply versus enabling the full tool set. This no longer applies -- the v4 surface is a flat 13-tool set, always loaded.
455440

456441
## Command Reference
457442

0 commit comments

Comments
 (0)