mcp-ssh-pty 1.1.0 → 1.2.0

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
@@ -11,7 +11,7 @@ npm install -g mcp-ssh-pty
11
11
  ### Add to Claude Code
12
12
 
13
13
  ```bash
14
- claude mcp add ssh -- npx -y mcp-ssh-pty
14
+ claude mcp add --transport stdio ssh -- npx -y mcp-ssh-pty
15
15
  ```
16
16
 
17
17
  ## CLI Commands
@@ -28,7 +28,7 @@ mcp-ssh-pty list --all # Show both levels
28
28
  ### Add server
29
29
 
30
30
  ```bash
31
- # Interactive mode (will ask for config level)
31
+ # Interactive mode
32
32
  mcp-ssh-pty add
33
33
 
34
34
  # Save to project level
@@ -58,22 +58,6 @@ mcp-ssh-pty test my-server
58
58
  mcp-ssh-pty config
59
59
  ```
60
60
 
61
- ```
62
- ? 选择配置级别:
63
- ❯ 📁 项目级别 (已存在)
64
- 🌐 用户级别 (新建)
65
-
66
- ? 选择操作:
67
- ❯ 📋 查看所有服务器
68
- ➕ 添加服务器
69
- ✏️ 编辑服务器
70
- 🗑️ 删除服务器
71
- 🔌 测试连接
72
- 🔄 切换配置级别
73
- 📁 显示配置文件路径
74
- 🚪 退出
75
- ```
76
-
77
61
  ## Configuration
78
62
 
79
63
  ### Config file locations
@@ -84,8 +68,6 @@ mcp-ssh-pty config
84
68
  | User | `~/.claude/ssh-servers.json` | Low |
85
69
  | Custom | `SSH_MCP_CONFIG_PATH` env | Highest |
86
70
 
87
- Project level config overrides user level when exists.
88
-
89
71
  ### Config format
90
72
 
91
73
  ```json
@@ -104,13 +86,25 @@ Project level config overrides user level when exists.
104
86
 
105
87
  ## MCP Usage
106
88
 
107
- ### Connection
89
+ ### List Servers
108
90
 
109
91
  ```
110
92
  ssh({ action: "list" })
111
- ssh({ action: "connect", server: "my-server" })
112
- ssh({ action: "status" })
113
- ssh({ action: "disconnect" })
93
+ ```
94
+
95
+ Returns:
96
+ ```json
97
+ [
98
+ { "name": "local", "connected": false, "type": "built-in" },
99
+ { "name": "my-server", "connected": false, "type": "configured" }
100
+ ]
101
+ ```
102
+
103
+ ### Connect
104
+
105
+ ```
106
+ ssh({ action: "connect", server: "local" }) # Local shell
107
+ ssh({ action: "connect", server: "my-server" }) # Remote SSH
114
108
  ```
115
109
 
116
110
  ### Command Execution
@@ -144,6 +138,24 @@ ssh({ read: true })
144
138
  ssh({ signal: "SIGINT" }) # Ctrl+C
145
139
  ```
146
140
 
141
+ ### Disconnect
142
+
143
+ ```
144
+ ssh({ action: "disconnect" })
145
+ ```
146
+
147
+ ### Status
148
+
149
+ ```
150
+ ssh({ action: "status" })
151
+ ```
152
+
153
+ ## Built-in Servers
154
+
155
+ | Name | Description |
156
+ |------|-------------|
157
+ | `local` | Local shell (uses system default shell) |
158
+
147
159
  ## License
148
160
 
149
161
  MIT
package/dist/cli.js CHANGED
@@ -26,6 +26,9 @@ function formatScope(scope) {
26
26
  * list 命令
27
27
  */
28
28
  async function listServers(options) {
29
+ // 先显示内置服务器
30
+ console.log("=== 内置服务器 ===");
31
+ console.log(` • local (本地 shell)\n`);
29
32
  if (options.all) {
30
33
  // 显示两个级别的配置
31
34
  console.log("=== 项目级别 (local) ===");
@@ -64,14 +67,13 @@ async function listServers(options) {
64
67
  const scope = options.local ? "local" : options.global ? "global" : undefined;
65
68
  const configManager = getConfigManager(scope);
66
69
  const servers = configManager.listServers();
67
- console.log(`配置级别: ${formatScope(configManager.getScope())}`);
68
- console.log(`配置文件: ${configManager.getConfigPath()}\n`);
70
+ console.log(`=== ${formatScope(configManager.getScope())} ===`);
71
+ console.log(`路径: ${configManager.getConfigPath()}\n`);
69
72
  if (servers.length === 0) {
70
- console.log("没有配置任何服务器");
73
+ console.log("(空)");
71
74
  console.log("\n使用 'mcp-ssh-pty add' 添加服务器");
72
75
  return;
73
76
  }
74
- console.log("已配置的服务器:\n");
75
77
  servers.forEach((server, index) => {
76
78
  console.log(` ${index + 1}. ${formatServer(server)}`);
77
79
  });
package/dist/index.js CHANGED
@@ -32,15 +32,11 @@ async function startServer() {
32
32
  registerTools(server, sshManager, configManager);
33
33
  const transport = new StdioServerTransport();
34
34
  await server.connect(transport);
35
- console.error("SSH MCP Server 已启动");
36
- console.error(`配置文件路径: ${configManager.getConfigPath()}`);
37
35
  process.on("SIGINT", async () => {
38
- console.error("收到 SIGINT,正在关闭...");
39
36
  await sshManager.disconnect();
40
37
  process.exit(0);
41
38
  });
42
39
  process.on("SIGTERM", async () => {
43
- console.error("收到 SIGTERM,正在关闭...");
44
40
  await sshManager.disconnect();
45
41
  process.exit(0);
46
42
  });
@@ -2,19 +2,33 @@ import { Client } from "ssh2";
2
2
  import { ShellResult, ShellConfig } from "./types.js";
3
3
  export declare class ShellManager {
4
4
  private shell;
5
+ private localProcess;
5
6
  private outputBuffer;
6
7
  private outputLines;
7
8
  private lastOutputTime;
8
9
  private config;
10
+ private isLocal;
9
11
  constructor(config?: Partial<ShellConfig>);
10
12
  /**
11
- * 打开 PTY Shell(在 SSH 连接成功后调用)
13
+ * 打开远程 PTY Shell(在 SSH 连接成功后调用)
12
14
  */
13
15
  open(client: Client): Promise<void>;
16
+ /**
17
+ * 打开本地 Shell
18
+ */
19
+ openLocal(): Promise<void>;
20
+ /**
21
+ * 设置 stream 的事件监听
22
+ */
23
+ private setupStream;
14
24
  /**
15
25
  * 检查 shell 是否已打开
16
26
  */
17
27
  isOpen(): boolean;
28
+ /**
29
+ * 是否是本地 shell
30
+ */
31
+ isLocalShell(): boolean;
18
32
  /**
19
33
  * 发送命令,智能等待完成
20
34
  */
@@ -1,3 +1,4 @@
1
+ import { spawn } from "child_process";
1
2
  const DEFAULT_CONFIG = {
2
3
  quickTimeout: 2000,
3
4
  maxTimeout: 5000,
@@ -6,15 +7,17 @@ const DEFAULT_CONFIG = {
6
7
  };
7
8
  export class ShellManager {
8
9
  shell = null;
10
+ localProcess = null;
9
11
  outputBuffer = "";
10
12
  outputLines = [];
11
13
  lastOutputTime = 0;
12
14
  config;
15
+ isLocal = false;
13
16
  constructor(config) {
14
17
  this.config = { ...DEFAULT_CONFIG, ...config };
15
18
  }
16
19
  /**
17
- * 打开 PTY Shell(在 SSH 连接成功后调用)
20
+ * 打开远程 PTY Shell(在 SSH 连接成功后调用)
18
21
  */
19
22
  async open(client) {
20
23
  return new Promise((resolve, reject) => {
@@ -24,31 +27,8 @@ export class ShellManager {
24
27
  return;
25
28
  }
26
29
  this.shell = stream;
27
- this.outputBuffer = "";
28
- this.outputLines = [];
29
- this.lastOutputTime = Date.now();
30
- // 监听数据事件
31
- stream.on("data", (data) => {
32
- const text = data.toString();
33
- this.outputBuffer += text;
34
- this.lastOutputTime = Date.now();
35
- // 按行分割并存储
36
- const lines = this.outputBuffer.split("\n");
37
- // 最后一个可能是不完整的行,保留在 buffer
38
- this.outputBuffer = lines.pop() || "";
39
- this.outputLines.push(...lines);
40
- // 限制缓冲区大小
41
- if (this.outputLines.length > this.config.maxBufferLines) {
42
- this.outputLines = this.outputLines.slice(-this.config.maxBufferLines);
43
- }
44
- });
45
- // 监听关闭事件
46
- stream.on("close", () => {
47
- this.shell = null;
48
- });
49
- stream.on("error", (err) => {
50
- console.error("Shell error:", err.message);
51
- });
30
+ this.isLocal = false;
31
+ this.setupStream(stream);
52
32
  // 等待初始提示符
53
33
  setTimeout(() => {
54
34
  resolve();
@@ -56,12 +36,111 @@ export class ShellManager {
56
36
  });
57
37
  });
58
38
  }
39
+ /**
40
+ * 打开本地 Shell
41
+ */
42
+ async openLocal() {
43
+ return new Promise((resolve, reject) => {
44
+ try {
45
+ // 检测系统默认 shell
46
+ const shellPath = process.env.SHELL || (process.platform === "win32" ? "cmd.exe" : "/bin/sh");
47
+ const proc = spawn(shellPath, [], {
48
+ stdio: ["pipe", "pipe", "pipe"],
49
+ env: { ...process.env, TERM: "xterm-256color" },
50
+ });
51
+ this.localProcess = proc;
52
+ this.isLocal = true;
53
+ // 创建统一的 stream 接口
54
+ const dataListeners = [];
55
+ const closeListeners = [];
56
+ const errorListeners = [];
57
+ proc.stdout?.on("data", (data) => {
58
+ dataListeners.forEach((l) => l(data));
59
+ });
60
+ proc.stderr?.on("data", (data) => {
61
+ dataListeners.forEach((l) => l(data));
62
+ });
63
+ proc.on("close", () => {
64
+ closeListeners.forEach((l) => l());
65
+ });
66
+ proc.on("error", (err) => {
67
+ errorListeners.forEach((l) => l(err));
68
+ });
69
+ const stream = {
70
+ write: (data) => {
71
+ proc.stdin?.write(data);
72
+ },
73
+ end: () => {
74
+ proc.stdin?.end();
75
+ proc.kill();
76
+ },
77
+ on: ((event, listener) => {
78
+ if (event === "data") {
79
+ dataListeners.push(listener);
80
+ }
81
+ else if (event === "close") {
82
+ closeListeners.push(listener);
83
+ }
84
+ else if (event === "error") {
85
+ errorListeners.push(listener);
86
+ }
87
+ }),
88
+ };
89
+ this.shell = stream;
90
+ this.setupStream(stream);
91
+ // 等待初始化
92
+ setTimeout(() => {
93
+ resolve();
94
+ }, 300);
95
+ }
96
+ catch (error) {
97
+ reject(new Error(`无法打开本地 shell: ${error instanceof Error ? error.message : String(error)}`));
98
+ }
99
+ });
100
+ }
101
+ /**
102
+ * 设置 stream 的事件监听
103
+ */
104
+ setupStream(stream) {
105
+ this.outputBuffer = "";
106
+ this.outputLines = [];
107
+ this.lastOutputTime = Date.now();
108
+ // 监听数据事件
109
+ stream.on("data", (data) => {
110
+ const text = data.toString();
111
+ this.outputBuffer += text;
112
+ this.lastOutputTime = Date.now();
113
+ // 按行分割并存储
114
+ const lines = this.outputBuffer.split("\n");
115
+ // 最后一个可能是不完整的行,保留在 buffer
116
+ this.outputBuffer = lines.pop() || "";
117
+ this.outputLines.push(...lines);
118
+ // 限制缓冲区大小
119
+ if (this.outputLines.length > this.config.maxBufferLines) {
120
+ this.outputLines = this.outputLines.slice(-this.config.maxBufferLines);
121
+ }
122
+ });
123
+ // 监听关闭事件
124
+ stream.on("close", () => {
125
+ this.shell = null;
126
+ this.localProcess = null;
127
+ });
128
+ stream.on("error", (err) => {
129
+ console.error("Shell error:", err.message);
130
+ });
131
+ }
59
132
  /**
60
133
  * 检查 shell 是否已打开
61
134
  */
62
135
  isOpen() {
63
136
  return this.shell !== null;
64
137
  }
138
+ /**
139
+ * 是否是本地 shell
140
+ */
141
+ isLocalShell() {
142
+ return this.isLocal;
143
+ }
65
144
  /**
66
145
  * 发送命令,智能等待完成
67
146
  */
@@ -140,8 +219,13 @@ export class ShellManager {
140
219
  this.shell.end();
141
220
  this.shell = null;
142
221
  }
222
+ if (this.localProcess) {
223
+ this.localProcess.kill();
224
+ this.localProcess = null;
225
+ }
143
226
  this.outputLines = [];
144
227
  this.outputBuffer = "";
228
+ this.isLocal = false;
145
229
  }
146
230
  /**
147
231
  * 获取当前缓冲区行数
@@ -1,13 +1,27 @@
1
1
  import { ServerConfig, ConnectionStatus } from "./types.js";
2
2
  import { ShellManager } from "./shell-manager.js";
3
+ export declare const LOCAL_SERVER: ServerConfig;
3
4
  export declare class SSHManager {
4
5
  private client;
5
6
  private currentServer;
6
7
  private isConnected;
7
8
  private shellManager;
9
+ private isLocalConnection;
8
10
  constructor();
9
11
  private expandPath;
12
+ /**
13
+ * 检查是否是本地连接
14
+ */
15
+ static isLocalServer(config: ServerConfig): boolean;
10
16
  connect(config: ServerConfig): Promise<void>;
17
+ /**
18
+ * 本地连接
19
+ */
20
+ private connectLocal;
21
+ /**
22
+ * SSH 远程连接
23
+ */
24
+ private connectSSH;
11
25
  disconnect(): Promise<void>;
12
26
  private cleanup;
13
27
  /**
@@ -15,5 +29,6 @@ export declare class SSHManager {
15
29
  */
16
30
  private registerSensitiveInfo;
17
31
  getShellManager(): ShellManager;
32
+ isLocal(): boolean;
18
33
  getStatus(): ConnectionStatus;
19
34
  }
@@ -4,11 +4,19 @@ import { homedir } from "os";
4
4
  import { join } from "path";
5
5
  import { ShellManager } from "./shell-manager.js";
6
6
  import { getSanitizer } from "./sanitizer.js";
7
+ // 内置的本地服务器配置
8
+ export const LOCAL_SERVER = {
9
+ name: "local",
10
+ host: "localhost",
11
+ port: 0,
12
+ username: process.env.USER || process.env.USERNAME || "local",
13
+ };
7
14
  export class SSHManager {
8
15
  client = null;
9
16
  currentServer = null;
10
17
  isConnected = false;
11
18
  shellManager;
19
+ isLocalConnection = false;
12
20
  constructor() {
13
21
  this.shellManager = new ShellManager();
14
22
  }
@@ -18,14 +26,46 @@ export class SSHManager {
18
26
  }
19
27
  return path;
20
28
  }
29
+ /**
30
+ * 检查是否是本地连接
31
+ */
32
+ static isLocalServer(config) {
33
+ return config.name === "local" || config.host === "local";
34
+ }
21
35
  async connect(config) {
22
36
  if (this.isConnected) {
23
37
  await this.disconnect();
24
38
  }
39
+ // 检查是否是本地连接
40
+ if (SSHManager.isLocalServer(config)) {
41
+ return this.connectLocal();
42
+ }
43
+ return this.connectSSH(config);
44
+ }
45
+ /**
46
+ * 本地连接
47
+ */
48
+ async connectLocal() {
49
+ try {
50
+ await this.shellManager.openLocal();
51
+ this.isConnected = true;
52
+ this.isLocalConnection = true;
53
+ this.currentServer = LOCAL_SERVER;
54
+ }
55
+ catch (error) {
56
+ this.cleanup();
57
+ throw error;
58
+ }
59
+ }
60
+ /**
61
+ * SSH 远程连接
62
+ */
63
+ async connectSSH(config) {
25
64
  return new Promise((resolve, reject) => {
26
65
  this.client = new Client();
27
66
  this.client.on("ready", async () => {
28
67
  this.isConnected = true;
68
+ this.isLocalConnection = false;
29
69
  this.currentServer = config;
30
70
  // 注册敏感信息到过滤器
31
71
  this.registerSensitiveInfo(config);
@@ -77,13 +117,14 @@ export class SSHManager {
77
117
  async disconnect() {
78
118
  // 先关闭 shell
79
119
  this.shellManager.close();
80
- if (this.client && this.isConnected) {
120
+ if (this.client && this.isConnected && !this.isLocalConnection) {
81
121
  this.client.end();
82
122
  }
83
123
  this.cleanup();
84
124
  }
85
125
  cleanup() {
86
126
  this.isConnected = false;
127
+ this.isLocalConnection = false;
87
128
  this.currentServer = null;
88
129
  this.client = null;
89
130
  // 清除敏感信息
@@ -105,11 +146,14 @@ export class SSHManager {
105
146
  getShellManager() {
106
147
  return this.shellManager;
107
148
  }
149
+ isLocal() {
150
+ return this.isLocalConnection;
151
+ }
108
152
  getStatus() {
109
153
  return {
110
154
  connected: this.isConnected,
111
155
  serverName: this.currentServer?.name || null,
112
- host: this.currentServer?.host || null,
156
+ host: this.isLocalConnection ? "local" : (this.currentServer?.host || null),
113
157
  username: this.currentServer?.username || null,
114
158
  };
115
159
  }
package/dist/tools.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { LOCAL_SERVER } from "./ssh-manager.js";
2
3
  import { sanitize } from "./sanitizer.js";
3
4
  /**
4
5
  * 过滤 ShellResult 中的敏感信息
@@ -151,10 +152,19 @@ ssh({ signal: "SIGINT" }) # Ctrl+C 停止`,
151
152
  case "list": {
152
153
  const servers = configManager.listServers();
153
154
  const status = sshManager.getStatus();
154
- const list = servers.map((s) => ({
155
- name: s.name,
156
- connected: status.serverName === s.name,
157
- }));
155
+ // 添加内置的 local 服务器
156
+ const list = [
157
+ {
158
+ name: LOCAL_SERVER.name,
159
+ connected: status.serverName === LOCAL_SERVER.name,
160
+ type: "built-in",
161
+ },
162
+ ...servers.map((s) => ({
163
+ name: s.name,
164
+ connected: status.serverName === s.name,
165
+ type: "configured",
166
+ })),
167
+ ];
158
168
  return {
159
169
  content: [{ type: "text", text: JSON.stringify(list, null, 2) }],
160
170
  };
@@ -166,9 +176,19 @@ ssh({ signal: "SIGINT" }) # Ctrl+C 停止`,
166
176
  isError: true,
167
177
  };
168
178
  }
179
+ // 检查是否是本地连接
180
+ if (serverName === "local") {
181
+ await sshManager.connect(LOCAL_SERVER);
182
+ return {
183
+ content: [{
184
+ type: "text",
185
+ text: "成功连接到本地 Shell",
186
+ }],
187
+ };
188
+ }
169
189
  const serverConfig = configManager.getServer(serverName);
170
190
  if (!serverConfig) {
171
- const available = configManager.listServers().map((s) => s.name);
191
+ const available = ["local", ...configManager.listServers().map((s) => s.name)];
172
192
  return {
173
193
  content: [{
174
194
  type: "text",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-ssh-pty",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "MCP Server for SSH remote command execution with PTY shell support",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",