mcp-ssh-pty 1.0.0 → 1.1.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/dist/config.d.ts CHANGED
@@ -1,12 +1,24 @@
1
1
  import { ServersConfig, ServerConfig } from "./types.js";
2
+ export type ConfigScope = "local" | "global";
2
3
  export declare class ConfigManager {
3
4
  private config;
4
5
  private configPath;
5
- constructor(configPath?: string);
6
+ private scope;
7
+ constructor(configPath?: string, scope?: ConfigScope);
8
+ private getPathByScope;
9
+ static getLocalPath(): string;
10
+ static getGlobalPath(): string;
6
11
  private resolveConfigPath;
7
12
  private expandPath;
8
13
  load(): ServersConfig;
14
+ save(): void;
9
15
  getServer(name: string): ServerConfig | undefined;
10
16
  listServers(): ServerConfig[];
17
+ addServer(server: ServerConfig): void;
18
+ removeServer(name: string): boolean;
11
19
  getConfigPath(): string;
20
+ getScope(): ConfigScope;
21
+ configExists(): boolean;
22
+ static localConfigExists(): boolean;
23
+ static globalConfigExists(): boolean;
12
24
  }
package/dist/config.js CHANGED
@@ -1,24 +1,55 @@
1
- import { readFileSync, existsSync } from "fs";
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
2
2
  import { homedir } from "os";
3
- import { join } from "path";
3
+ import { join, dirname } from "path";
4
4
  export class ConfigManager {
5
5
  config = null;
6
6
  configPath;
7
- constructor(configPath) {
8
- this.configPath = configPath || this.resolveConfigPath();
7
+ scope;
8
+ constructor(configPath, scope) {
9
+ if (configPath) {
10
+ this.configPath = configPath;
11
+ this.scope = "global";
12
+ }
13
+ else if (scope) {
14
+ this.configPath = this.getPathByScope(scope);
15
+ this.scope = scope;
16
+ }
17
+ else {
18
+ const resolved = this.resolveConfigPath();
19
+ this.configPath = resolved.path;
20
+ this.scope = resolved.scope;
21
+ }
22
+ }
23
+ getPathByScope(scope) {
24
+ if (scope === "local") {
25
+ return join(process.cwd(), ".claude", "ssh-servers.json");
26
+ }
27
+ return join(homedir(), ".claude", "ssh-servers.json");
28
+ }
29
+ static getLocalPath() {
30
+ return join(process.cwd(), ".claude", "ssh-servers.json");
31
+ }
32
+ static getGlobalPath() {
33
+ return join(homedir(), ".claude", "ssh-servers.json");
9
34
  }
10
35
  resolveConfigPath() {
11
36
  // 1. 优先使用环境变量
12
37
  if (process.env.SSH_MCP_CONFIG_PATH) {
13
- return this.expandPath(process.env.SSH_MCP_CONFIG_PATH);
38
+ return {
39
+ path: this.expandPath(process.env.SSH_MCP_CONFIG_PATH),
40
+ scope: "global",
41
+ };
14
42
  }
15
43
  // 2. 查找项目目录下的 .claude/ssh-servers.json
16
44
  const projectPath = join(process.cwd(), ".claude", "ssh-servers.json");
17
45
  if (existsSync(projectPath)) {
18
- return projectPath;
46
+ return { path: projectPath, scope: "local" };
19
47
  }
20
48
  // 3. 查找用户目录下的 ~/.claude/ssh-servers.json
21
- return join(homedir(), ".claude", "ssh-servers.json");
49
+ return {
50
+ path: join(homedir(), ".claude", "ssh-servers.json"),
51
+ scope: "global",
52
+ };
22
53
  }
23
54
  expandPath(path) {
24
55
  if (path.startsWith("~")) {
@@ -28,7 +59,9 @@ export class ConfigManager {
28
59
  }
29
60
  load() {
30
61
  if (!existsSync(this.configPath)) {
31
- throw new Error(`配置文件不存在: ${this.configPath}\n请创建配置文件或设置 SSH_MCP_CONFIG_PATH 环境变量`);
62
+ // 如果配置文件不存在,返回空配置
63
+ this.config = { servers: [] };
64
+ return this.config;
32
65
  }
33
66
  try {
34
67
  const content = readFileSync(this.configPath, "utf-8");
@@ -42,6 +75,14 @@ export class ConfigManager {
42
75
  throw error;
43
76
  }
44
77
  }
78
+ save() {
79
+ // 确保目录存在
80
+ const dir = dirname(this.configPath);
81
+ if (!existsSync(dir)) {
82
+ mkdirSync(dir, { recursive: true });
83
+ }
84
+ writeFileSync(this.configPath, JSON.stringify(this.config, null, 2), "utf-8");
85
+ }
45
86
  getServer(name) {
46
87
  if (!this.config)
47
88
  this.load();
@@ -52,7 +93,44 @@ export class ConfigManager {
52
93
  this.load();
53
94
  return this.config.servers;
54
95
  }
96
+ addServer(server) {
97
+ if (!this.config)
98
+ this.load();
99
+ // 检查是否已存在
100
+ const existing = this.config.servers.findIndex((s) => s.name === server.name);
101
+ if (existing >= 0) {
102
+ // 更新已有配置
103
+ this.config.servers[existing] = server;
104
+ }
105
+ else {
106
+ this.config.servers.push(server);
107
+ }
108
+ this.save();
109
+ }
110
+ removeServer(name) {
111
+ if (!this.config)
112
+ this.load();
113
+ const index = this.config.servers.findIndex((s) => s.name === name);
114
+ if (index < 0) {
115
+ return false;
116
+ }
117
+ this.config.servers.splice(index, 1);
118
+ this.save();
119
+ return true;
120
+ }
55
121
  getConfigPath() {
56
122
  return this.configPath;
57
123
  }
124
+ getScope() {
125
+ return this.scope;
126
+ }
127
+ configExists() {
128
+ return existsSync(this.configPath);
129
+ }
130
+ static localConfigExists() {
131
+ return existsSync(ConfigManager.getLocalPath());
132
+ }
133
+ static globalConfigExists() {
134
+ return existsSync(ConfigManager.getGlobalPath());
135
+ }
58
136
  }
package/dist/index.js CHANGED
@@ -4,7 +4,25 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
4
4
  import { SSHManager } from "./ssh-manager.js";
5
5
  import { ConfigManager } from "./config.js";
6
6
  import { registerTools } from "./tools.js";
7
- async function main() {
7
+ import { runCLI } from "./cli.js";
8
+ // CLI 命令列表
9
+ const CLI_COMMANDS = ["list", "add", "remove", "rm", "test", "config", "help", "--help", "-h", "--version", "-V"];
10
+ /**
11
+ * 检查是否是 CLI 模式
12
+ */
13
+ function isCLIMode() {
14
+ const args = process.argv.slice(2);
15
+ // 没有参数时启动 MCP 服务器
16
+ if (args.length === 0) {
17
+ return false;
18
+ }
19
+ // 如果第一个参数是 CLI 命令,则进入 CLI 模式
20
+ return CLI_COMMANDS.includes(args[0]);
21
+ }
22
+ /**
23
+ * 启动 MCP 服务器
24
+ */
25
+ async function startServer() {
8
26
  const server = new McpServer({
9
27
  name: "ssh-mcp-server",
10
28
  version: "1.0.0",
@@ -27,6 +45,17 @@ async function main() {
27
45
  process.exit(0);
28
46
  });
29
47
  }
48
+ /**
49
+ * 主函数
50
+ */
51
+ async function main() {
52
+ if (isCLIMode()) {
53
+ await runCLI();
54
+ }
55
+ else {
56
+ await startServer();
57
+ }
58
+ }
30
59
  main().catch((error) => {
31
60
  console.error("启动失败:", error);
32
61
  process.exit(1);
@@ -0,0 +1,48 @@
1
+ /**
2
+ * 敏感信息过滤器
3
+ * 用于在 MCP 返回中隐藏 IP、密码等敏感信息
4
+ */
5
+ export interface SensitivePattern {
6
+ pattern: RegExp;
7
+ replacement: string;
8
+ description: string;
9
+ }
10
+ export declare class Sanitizer {
11
+ private sensitiveValues;
12
+ private patterns;
13
+ constructor();
14
+ /**
15
+ * 添加自定义正则模式
16
+ */
17
+ addPattern(pattern: SensitivePattern): void;
18
+ /**
19
+ * 注册敏感值(如密码、私钥路径等)
20
+ * 这些值会被精确匹配并替换
21
+ */
22
+ addSensitiveValue(value: string, minLength?: number): void;
23
+ /**
24
+ * 批量注册敏感值
25
+ */
26
+ addSensitiveValues(values: (string | undefined)[]): void;
27
+ /**
28
+ * 清除所有注册的敏感值(保留模式)
29
+ */
30
+ clearSensitiveValues(): void;
31
+ /**
32
+ * 过滤文本中的敏感信息
33
+ */
34
+ sanitize(text: string): string;
35
+ /**
36
+ * 过滤对象中的敏感信息(递归处理)
37
+ */
38
+ sanitizeObject<T>(obj: T): T;
39
+ }
40
+ export declare function getSanitizer(): Sanitizer;
41
+ /**
42
+ * 便捷函数:过滤文本
43
+ */
44
+ export declare function sanitize(text: string): string;
45
+ /**
46
+ * 便捷函数:过滤对象
47
+ */
48
+ export declare function sanitizeObject<T>(obj: T): T;
@@ -0,0 +1,123 @@
1
+ /**
2
+ * 敏感信息过滤器
3
+ * 用于在 MCP 返回中隐藏 IP、密码等敏感信息
4
+ */
5
+ export class Sanitizer {
6
+ sensitiveValues = new Set();
7
+ patterns = [];
8
+ constructor() {
9
+ // 内置通用模式
10
+ this.addPattern({
11
+ pattern: /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g,
12
+ replacement: "[IP]",
13
+ description: "IPv4 地址",
14
+ });
15
+ this.addPattern({
16
+ pattern: /\b([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\b/g,
17
+ replacement: "[IPv6]",
18
+ description: "IPv6 地址",
19
+ });
20
+ this.addPattern({
21
+ pattern: /\b([0-9a-fA-F]{1,4}:){1,7}:\b/g,
22
+ replacement: "[IPv6]",
23
+ description: "IPv6 简写",
24
+ });
25
+ }
26
+ /**
27
+ * 添加自定义正则模式
28
+ */
29
+ addPattern(pattern) {
30
+ this.patterns.push(pattern);
31
+ }
32
+ /**
33
+ * 注册敏感值(如密码、私钥路径等)
34
+ * 这些值会被精确匹配并替换
35
+ */
36
+ addSensitiveValue(value, minLength = 3) {
37
+ if (value && value.length >= minLength) {
38
+ this.sensitiveValues.add(value);
39
+ }
40
+ }
41
+ /**
42
+ * 批量注册敏感值
43
+ */
44
+ addSensitiveValues(values) {
45
+ values.forEach((v) => {
46
+ if (v)
47
+ this.addSensitiveValue(v);
48
+ });
49
+ }
50
+ /**
51
+ * 清除所有注册的敏感值(保留模式)
52
+ */
53
+ clearSensitiveValues() {
54
+ this.sensitiveValues.clear();
55
+ }
56
+ /**
57
+ * 过滤文本中的敏感信息
58
+ */
59
+ sanitize(text) {
60
+ if (!text)
61
+ return text;
62
+ let result = text;
63
+ // 1. 先替换精确匹配的敏感值(按长度降序,避免短串误匹配)
64
+ const sortedValues = Array.from(this.sensitiveValues)
65
+ .filter((v) => v.length >= 3)
66
+ .sort((a, b) => b.length - a.length);
67
+ for (const value of sortedValues) {
68
+ // 转义正则特殊字符
69
+ const escaped = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
70
+ const regex = new RegExp(escaped, "g");
71
+ result = result.replace(regex, "[REDACTED]");
72
+ }
73
+ // 2. 应用正则模式
74
+ for (const { pattern, replacement } of this.patterns) {
75
+ // 重置正则状态(因为使用了 /g 标志)
76
+ pattern.lastIndex = 0;
77
+ result = result.replace(pattern, replacement);
78
+ }
79
+ return result;
80
+ }
81
+ /**
82
+ * 过滤对象中的敏感信息(递归处理)
83
+ */
84
+ sanitizeObject(obj) {
85
+ if (obj === null || obj === undefined) {
86
+ return obj;
87
+ }
88
+ if (typeof obj === "string") {
89
+ return this.sanitize(obj);
90
+ }
91
+ if (Array.isArray(obj)) {
92
+ return obj.map((item) => this.sanitizeObject(item));
93
+ }
94
+ if (typeof obj === "object") {
95
+ const result = {};
96
+ for (const [key, value] of Object.entries(obj)) {
97
+ result[key] = this.sanitizeObject(value);
98
+ }
99
+ return result;
100
+ }
101
+ return obj;
102
+ }
103
+ }
104
+ // 全局单例
105
+ let globalSanitizer = null;
106
+ export function getSanitizer() {
107
+ if (!globalSanitizer) {
108
+ globalSanitizer = new Sanitizer();
109
+ }
110
+ return globalSanitizer;
111
+ }
112
+ /**
113
+ * 便捷函数:过滤文本
114
+ */
115
+ export function sanitize(text) {
116
+ return getSanitizer().sanitize(text);
117
+ }
118
+ /**
119
+ * 便捷函数:过滤对象
120
+ */
121
+ export function sanitizeObject(obj) {
122
+ return getSanitizer().sanitizeObject(obj);
123
+ }
@@ -21,6 +21,9 @@ export declare class ShellManager {
21
21
  send(input: string, config?: Partial<ShellConfig>): Promise<ShellResult>;
22
22
  /**
23
23
  * 读取缓冲区内容
24
+ * @param lines 返回行数:不传默认 20 行,-1 返回全部,正整数返回对应行数
25
+ * @param offset 起始偏移,默认 0
26
+ * @param clear 读取后是否清空缓冲区
24
27
  */
25
28
  read(lines?: number, offset?: number, clear?: boolean): ShellResult;
26
29
  /**
@@ -85,14 +85,19 @@ export class ShellManager {
85
85
  }
86
86
  /**
87
87
  * 读取缓冲区内容
88
+ * @param lines 返回行数:不传默认 20 行,-1 返回全部,正整数返回对应行数
89
+ * @param offset 起始偏移,默认 0
90
+ * @param clear 读取后是否清空缓冲区
88
91
  */
89
92
  read(lines, offset, clear) {
90
93
  const startIdx = offset || 0;
91
- const endIdx = lines ? startIdx + lines : this.outputLines.length;
94
+ // 处理 lines 参数:undefined 默认 20,-1 返回全部,其他返回指定行数
95
+ const effectiveLines = lines === undefined ? 20 : (lines === -1 ? undefined : lines);
96
+ const endIdx = effectiveLines ? startIdx + effectiveLines : this.outputLines.length;
92
97
  const selectedLines = this.outputLines.slice(startIdx, endIdx);
93
98
  // 包含不完整的最后一行
94
99
  let output = selectedLines.join("\n");
95
- if (this.outputBuffer && (!lines || endIdx >= this.outputLines.length)) {
100
+ if (this.outputBuffer && (!effectiveLines || endIdx >= this.outputLines.length)) {
96
101
  output += (output ? "\n" : "") + this.outputBuffer;
97
102
  }
98
103
  const result = {
@@ -10,6 +10,10 @@ export declare class SSHManager {
10
10
  connect(config: ServerConfig): Promise<void>;
11
11
  disconnect(): Promise<void>;
12
12
  private cleanup;
13
+ /**
14
+ * 注册服务器配置中的敏感信息
15
+ */
16
+ private registerSensitiveInfo;
13
17
  getShellManager(): ShellManager;
14
18
  getStatus(): ConnectionStatus;
15
19
  }
@@ -3,6 +3,7 @@ import { readFileSync } from "fs";
3
3
  import { homedir } from "os";
4
4
  import { join } from "path";
5
5
  import { ShellManager } from "./shell-manager.js";
6
+ import { getSanitizer } from "./sanitizer.js";
6
7
  export class SSHManager {
7
8
  client = null;
8
9
  currentServer = null;
@@ -26,6 +27,8 @@ export class SSHManager {
26
27
  this.client.on("ready", async () => {
27
28
  this.isConnected = true;
28
29
  this.currentServer = config;
30
+ // 注册敏感信息到过滤器
31
+ this.registerSensitiveInfo(config);
29
32
  // 连接成功后自动打开 shell
30
33
  try {
31
34
  await this.shellManager.open(this.client);
@@ -83,6 +86,21 @@ export class SSHManager {
83
86
  this.isConnected = false;
84
87
  this.currentServer = null;
85
88
  this.client = null;
89
+ // 清除敏感信息
90
+ getSanitizer().clearSensitiveValues();
91
+ }
92
+ /**
93
+ * 注册服务器配置中的敏感信息
94
+ */
95
+ registerSensitiveInfo(config) {
96
+ const sanitizer = getSanitizer();
97
+ sanitizer.addSensitiveValues([
98
+ config.host,
99
+ config.password,
100
+ config.passphrase,
101
+ config.username,
102
+ config.privateKeyPath,
103
+ ]);
86
104
  }
87
105
  getShellManager() {
88
106
  return this.shellManager;