@pyrokine/mcp-ssh 1.0.0 → 1.1.2

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.
@@ -0,0 +1,216 @@
1
+ /**
2
+ * SSH Config Parser
3
+ *
4
+ * 解析 ~/.ssh/config 文件,提取 Host 配置
5
+ * 支持:
6
+ * - Host 多别名(Host a b c)
7
+ * - Host * 全局默认配置继承
8
+ * - ProxyJump 解析(支持 user@host:port 格式)
9
+ */
10
+ import * as fs from 'fs';
11
+ import * as os from 'os';
12
+ import * as path from 'path';
13
+ /**
14
+ * 解析 host:port 或 [ipv6]:port
15
+ * 返回 { host, port },host 不含方括号
16
+ */
17
+ function parseHostPort(s) {
18
+ // IPv6 方括号格式: [addr]:port 或 [addr]
19
+ if (s.startsWith('[')) {
20
+ const closeBracket = s.indexOf(']');
21
+ if (closeBracket !== -1) {
22
+ const host = s.slice(1, closeBracket); // 去掉方括号
23
+ const rest = s.slice(closeBracket + 1);
24
+ if (rest.startsWith(':')) {
25
+ const parsedPort = parseInt(rest.slice(1), 10);
26
+ if (!isNaN(parsedPort)) {
27
+ return { host, port: parsedPort };
28
+ }
29
+ }
30
+ return { host };
31
+ }
32
+ }
33
+ // 检测裸 IPv6(多个冒号但无方括号):安全失败,当作 host-only
34
+ const colonCount = (s.match(/:/g) || []).length;
35
+ if (colonCount >= 2) {
36
+ return { host: s };
37
+ }
38
+ // 普通格式: host:port 或 host
39
+ const colonIndex = s.lastIndexOf(':');
40
+ if (colonIndex !== -1) {
41
+ const host = s.slice(0, colonIndex);
42
+ const portStr = s.slice(colonIndex + 1);
43
+ const parsedPort = parseInt(portStr, 10);
44
+ if (!isNaN(parsedPort)) {
45
+ return { host, port: parsedPort };
46
+ }
47
+ }
48
+ return { host: s };
49
+ }
50
+ /**
51
+ * 解析 ProxyJump 字符串
52
+ * 支持格式:host, user@host, host:port, user@host:port, [ipv6]:port
53
+ * 注意:只解析第一跳,不支持逗号分隔的多跳链路
54
+ */
55
+ export function parseProxyJump(proxyJump) {
56
+ if (!proxyJump) {
57
+ return null;
58
+ }
59
+ // 取第一跳(如果有逗号分隔)
60
+ const firstJump = proxyJump.split(',')[0].trim();
61
+ if (!firstJump) {
62
+ return null;
63
+ }
64
+ let user;
65
+ // 解析 user@... 格式
66
+ const atIndex = firstJump.indexOf('@');
67
+ if (atIndex !== -1) {
68
+ user = firstJump.slice(0, atIndex);
69
+ const rest = firstJump.slice(atIndex + 1);
70
+ const { host, port } = parseHostPort(rest);
71
+ return { user, host, port };
72
+ }
73
+ const { host, port } = parseHostPort(firstJump);
74
+ return { user, host, port };
75
+ }
76
+ /**
77
+ * 展开 ~ 路径
78
+ */
79
+ function expandTilde(filePath) {
80
+ if (filePath.startsWith('~')) {
81
+ return path.join(os.homedir(), filePath.slice(1));
82
+ }
83
+ return filePath;
84
+ }
85
+ /**
86
+ * 剥离行尾注释
87
+ * "value # comment" -> "value"
88
+ */
89
+ function stripInlineComment(value) {
90
+ // 查找不在引号内的 #
91
+ let inQuote = false;
92
+ let quoteChar = '';
93
+ for (let i = 0; i < value.length; i++) {
94
+ const ch = value[i];
95
+ if (!inQuote && (ch === '"' || ch === '\'')) {
96
+ inQuote = true;
97
+ quoteChar = ch;
98
+ }
99
+ else if (inQuote && ch === quoteChar) {
100
+ inQuote = false;
101
+ }
102
+ else if (!inQuote && ch === '#') {
103
+ return value.slice(0, i).trim();
104
+ }
105
+ }
106
+ return value.trim();
107
+ }
108
+ /**
109
+ * 解析 SSH config 文件
110
+ * 支持 Host 多别名和 Host * 继承
111
+ * 跳过 Match 块(避免条件配置被误应用)
112
+ */
113
+ export function parseSSHConfig(configPath) {
114
+ const filePath = configPath || path.join(os.homedir(), '.ssh', 'config');
115
+ if (!fs.existsSync(filePath)) {
116
+ return [];
117
+ }
118
+ const content = fs.readFileSync(filePath, 'utf-8');
119
+ const lines = content.split('\n');
120
+ // 第一遍:收集所有配置块
121
+ const blocks = [];
122
+ let currentBlock = null;
123
+ let globalDefaults = {};
124
+ let inMatchBlock = false; // 跳过 Match 块
125
+ for (const line of lines) {
126
+ const trimmed = line.trim();
127
+ // 跳过空行和注释
128
+ if (!trimmed || trimmed.startsWith('#')) {
129
+ continue;
130
+ }
131
+ // 解析 key value(支持 = 和空格分隔)
132
+ const match = trimmed.match(/^(\S+)\s*[=\s]\s*(.+)$/);
133
+ if (!match) {
134
+ continue;
135
+ }
136
+ const [, key, rawValue] = match;
137
+ const keyLower = key.toLowerCase();
138
+ const value = stripInlineComment(rawValue);
139
+ if (keyLower === 'host') {
140
+ // Host 块开始,结束 Match 块
141
+ inMatchBlock = false;
142
+ // 保存上一个 block
143
+ if (currentBlock) {
144
+ blocks.push(currentBlock);
145
+ }
146
+ // Host 行可能有多个别名/模式,用空格分隔
147
+ const patterns = value.split(/\s+/).filter(p => p.length > 0);
148
+ currentBlock = { patterns, config: {} };
149
+ }
150
+ else if (keyLower === 'match') {
151
+ // Match 块开始,跳过直到下一个 Host
152
+ inMatchBlock = true;
153
+ // 保存当前 block(如果有)
154
+ if (currentBlock) {
155
+ blocks.push(currentBlock);
156
+ currentBlock = null;
157
+ }
158
+ }
159
+ else if (!inMatchBlock && currentBlock) {
160
+ // 解析配置项(不在 Match 块内)
161
+ switch (keyLower) {
162
+ case 'hostname':
163
+ currentBlock.config.hostName = value;
164
+ break;
165
+ case 'user':
166
+ currentBlock.config.user = value;
167
+ break;
168
+ case 'port':
169
+ currentBlock.config.port = parseInt(value, 10);
170
+ break;
171
+ case 'identityfile':
172
+ currentBlock.config.identityFile = expandTilde(value);
173
+ break;
174
+ case 'proxyjump':
175
+ currentBlock.config.proxyJump = value;
176
+ break;
177
+ }
178
+ }
179
+ }
180
+ // 保存最后一个 block
181
+ if (currentBlock && !inMatchBlock) {
182
+ blocks.push(currentBlock);
183
+ }
184
+ // 第二遍:提取 Host * 的全局默认配置
185
+ for (const block of blocks) {
186
+ if (block.patterns.length === 1 && block.patterns[0] === '*') {
187
+ globalDefaults = { ...block.config };
188
+ break;
189
+ }
190
+ }
191
+ // 第三遍:展开所有 Host,应用继承
192
+ const hosts = [];
193
+ for (const block of blocks) {
194
+ for (const pattern of block.patterns) {
195
+ // 跳过通配符模式(*, *.example.com 等)
196
+ if (pattern.includes('*') || pattern.includes('?')) {
197
+ continue;
198
+ }
199
+ // 合并配置:全局默认 + 当前块配置
200
+ const merged = {
201
+ host: pattern,
202
+ ...globalDefaults,
203
+ ...block.config,
204
+ };
205
+ hosts.push(merged);
206
+ }
207
+ }
208
+ return hosts;
209
+ }
210
+ /**
211
+ * 根据 Host 名称获取配置
212
+ */
213
+ export function getHostConfig(hostName, configPath) {
214
+ const hosts = parseSSHConfig(configPath);
215
+ return hosts.find(h => h.host === hostName) || null;
216
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyrokine/mcp-ssh",
3
- "version": "1.0.0",
3
+ "version": "1.1.2",
4
4
  "description": "A comprehensive SSH MCP Server for Claude Code",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -25,7 +25,7 @@
25
25
  "license": "MIT",
26
26
  "repository": {
27
27
  "type": "git",
28
- "url": "https://github.com/user/ssh-mcp-pro"
28
+ "url": "https://github.com/Pyrokine/claude-mcp-tools.git"
29
29
  },
30
30
  "dependencies": {
31
31
  "@modelcontextprotocol/sdk": "^1.25.3",