@huzhihui_c/mcp-ssh 1.0.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/LICENSE +22 -0
- package/README.md +50 -0
- package/dist/config.d.ts +33 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +76 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/package-metadata.d.ts +2 -0
- package/dist/package-metadata.d.ts.map +1 -0
- package/dist/package-metadata.js +7 -0
- package/dist/package-metadata.js.map +1 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +117 -0
- package/dist/server.js.map +1 -0
- package/dist/ssh-client.d.ts +20 -0
- package/dist/ssh-client.d.ts.map +1 -0
- package/dist/ssh-client.js +139 -0
- package/dist/ssh-client.js.map +1 -0
- package/package.json +40 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MCP Forge contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# @huzhihui_c/mcp-ssh
|
|
2
|
+
|
|
3
|
+
一个通过 SSH 在远程主机上执行命令的 MCP Server。
|
|
4
|
+
|
|
5
|
+
## 启动
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm --filter @huzhihui_c/mcp-ssh build
|
|
9
|
+
pnpm --filter @huzhihui_c/mcp-ssh start
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
MCP 客户端通常直接启动 npm 包的 bin:
|
|
13
|
+
|
|
14
|
+
```json
|
|
15
|
+
{
|
|
16
|
+
"mcpServers": {
|
|
17
|
+
"ssh": {
|
|
18
|
+
"command": "npx",
|
|
19
|
+
"args": ["-y", "@huzhihui_c/mcp-ssh"],
|
|
20
|
+
"env": {
|
|
21
|
+
"MCP_SSH_HOST": "example.com",
|
|
22
|
+
"MCP_SSH_USERNAME": "ubuntu",
|
|
23
|
+
"MCP_SSH_PRIVATE_KEY_PATH": "C:/Users/you/.ssh/id_ed25519"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
也可以在每次调用 `ssh_exec` 时传入 `host`、`username`、`password` 或 `privateKey`。调用参数优先于环境变量。
|
|
31
|
+
|
|
32
|
+
支持的环境变量:
|
|
33
|
+
|
|
34
|
+
- `MCP_SSH_HOST`:默认主机。
|
|
35
|
+
- `MCP_SSH_PORT`:默认端口,默认为 `22`。
|
|
36
|
+
- `MCP_SSH_USERNAME`:默认用户名。
|
|
37
|
+
- `MCP_SSH_PASSWORD`:密码认证。
|
|
38
|
+
- `MCP_SSH_PRIVATE_KEY`:私钥内容。
|
|
39
|
+
- `MCP_SSH_PRIVATE_KEY_PATH`:私钥文件路径。
|
|
40
|
+
- `MCP_SSH_PASSPHRASE`:私钥口令。
|
|
41
|
+
- `MCP_SSH_HOST_FINGERPRINT`:必填,服务器的 OpenSSH SHA256 指纹,例如 `SHA256:abc...`。也可以在工具调用中传入 `hostFingerprint`。
|
|
42
|
+
- `MCP_SSH_AUTH_SOCK`:SSH Agent socket;未设置时会尝试使用 `SSH_AUTH_SOCK`。
|
|
43
|
+
|
|
44
|
+
## 工具
|
|
45
|
+
|
|
46
|
+
### `ssh_exec`
|
|
47
|
+
|
|
48
|
+
在远程主机上执行命令。必填参数是 `command` 和服务器 host fingerprint;连接参数可以由调用参数或环境变量提供。
|
|
49
|
+
|
|
50
|
+
返回标准输出、标准错误和退出码。默认最多返回 1 MiB 输出,可通过 `maxOutputBytes` 调整,上限为 10 MiB。
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface SshToolInput {
|
|
2
|
+
host?: string;
|
|
3
|
+
port?: number;
|
|
4
|
+
username?: string;
|
|
5
|
+
password?: string;
|
|
6
|
+
privateKey?: string;
|
|
7
|
+
passphrase?: string;
|
|
8
|
+
hostFingerprint?: string;
|
|
9
|
+
command?: string;
|
|
10
|
+
cwd?: string;
|
|
11
|
+
timeoutMs?: number;
|
|
12
|
+
maxOutputBytes?: number;
|
|
13
|
+
}
|
|
14
|
+
export interface SshConnectionOptions {
|
|
15
|
+
host: string;
|
|
16
|
+
port: number;
|
|
17
|
+
username: string;
|
|
18
|
+
password?: string;
|
|
19
|
+
privateKey?: string;
|
|
20
|
+
passphrase?: string;
|
|
21
|
+
hostFingerprint: string;
|
|
22
|
+
agent?: string;
|
|
23
|
+
readyTimeout: number;
|
|
24
|
+
}
|
|
25
|
+
export declare class SshConfigurationError extends Error {
|
|
26
|
+
constructor(message: string);
|
|
27
|
+
}
|
|
28
|
+
export declare function resolveConnectionOptions(input: SshToolInput): SshConnectionOptions;
|
|
29
|
+
export declare function resolveExecutionLimits(input: SshToolInput): {
|
|
30
|
+
timeoutMs: number;
|
|
31
|
+
maxOutputBytes: number;
|
|
32
|
+
};
|
|
33
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,qBAAa,qBAAsB,SAAQ,KAAK;gBAClC,OAAO,EAAE,MAAM;CAI5B;AA0BD,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,YAAY,GAAG,oBAAoB,CAoDlF;AAED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,YAAY,GAAG;IAC3D,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;CACxB,CAUA"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
export class SshConfigurationError extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = 'SshConfigurationError';
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
function optionalNonEmpty(value) {
|
|
9
|
+
return value && value.trim().length > 0 ? value : undefined;
|
|
10
|
+
}
|
|
11
|
+
function readPrivateKeyFromEnvironment() {
|
|
12
|
+
const inlineKey = optionalNonEmpty(process.env.MCP_SSH_PRIVATE_KEY);
|
|
13
|
+
if (inlineKey) {
|
|
14
|
+
return inlineKey;
|
|
15
|
+
}
|
|
16
|
+
const keyPath = optionalNonEmpty(process.env.MCP_SSH_PRIVATE_KEY_PATH);
|
|
17
|
+
if (!keyPath) {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
return readFileSync(keyPath, 'utf8');
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
throw new SshConfigurationError(`Unable to read the private key configured by MCP_SSH_PRIVATE_KEY_PATH: ${keyPath}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function resolveConnectionOptions(input) {
|
|
28
|
+
const host = optionalNonEmpty(input.host) ?? optionalNonEmpty(process.env.MCP_SSH_HOST);
|
|
29
|
+
const username = optionalNonEmpty(input.username) ?? optionalNonEmpty(process.env.MCP_SSH_USERNAME);
|
|
30
|
+
const port = input.port ?? Number.parseInt(process.env.MCP_SSH_PORT ?? '22', 10);
|
|
31
|
+
const timeoutMs = input.timeoutMs ?? Number.parseInt(process.env.MCP_SSH_TIMEOUT_MS ?? '30000', 10);
|
|
32
|
+
const password = optionalNonEmpty(input.password) ?? optionalNonEmpty(process.env.MCP_SSH_PASSWORD);
|
|
33
|
+
const privateKey = optionalNonEmpty(input.privateKey) ?? readPrivateKeyFromEnvironment();
|
|
34
|
+
const passphrase = optionalNonEmpty(input.passphrase) ?? optionalNonEmpty(process.env.MCP_SSH_PASSPHRASE);
|
|
35
|
+
const hostFingerprint = optionalNonEmpty(input.hostFingerprint) ??
|
|
36
|
+
optionalNonEmpty(process.env.MCP_SSH_HOST_FINGERPRINT);
|
|
37
|
+
const agent = optionalNonEmpty(process.env.MCP_SSH_AUTH_SOCK) ?? optionalNonEmpty(process.env.SSH_AUTH_SOCK);
|
|
38
|
+
if (!host) {
|
|
39
|
+
throw new SshConfigurationError('Missing SSH host. Pass host or set MCP_SSH_HOST.');
|
|
40
|
+
}
|
|
41
|
+
if (!username) {
|
|
42
|
+
throw new SshConfigurationError('Missing SSH username. Pass username or set MCP_SSH_USERNAME.');
|
|
43
|
+
}
|
|
44
|
+
if (!Number.isInteger(port) || port < 1 || port > 65_535) {
|
|
45
|
+
throw new SshConfigurationError('SSH port must be an integer between 1 and 65535.');
|
|
46
|
+
}
|
|
47
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs < 1_000 || timeoutMs > 120_000) {
|
|
48
|
+
throw new SshConfigurationError('timeoutMs must be an integer between 1000 and 120000.');
|
|
49
|
+
}
|
|
50
|
+
if (!hostFingerprint) {
|
|
51
|
+
throw new SshConfigurationError('Missing SSH host fingerprint. Pass hostFingerprint or set MCP_SSH_HOST_FINGERPRINT.');
|
|
52
|
+
}
|
|
53
|
+
if (!password && !privateKey && !agent) {
|
|
54
|
+
throw new SshConfigurationError('Missing SSH authentication. Provide password/privateKey or configure MCP_SSH_PRIVATE_KEY_PATH or SSH_AUTH_SOCK.');
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
host,
|
|
58
|
+
port,
|
|
59
|
+
username,
|
|
60
|
+
...(password ? { password } : {}),
|
|
61
|
+
...(privateKey ? { privateKey } : {}),
|
|
62
|
+
...(passphrase ? { passphrase } : {}),
|
|
63
|
+
hostFingerprint,
|
|
64
|
+
...(agent ? { agent } : {}),
|
|
65
|
+
readyTimeout: timeoutMs,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
export function resolveExecutionLimits(input) {
|
|
69
|
+
const timeoutMs = input.timeoutMs ?? Number.parseInt(process.env.MCP_SSH_TIMEOUT_MS ?? '30000', 10);
|
|
70
|
+
const maxOutputBytes = input.maxOutputBytes ?? 1_048_576;
|
|
71
|
+
if (!Number.isInteger(maxOutputBytes) || maxOutputBytes < 1 || maxOutputBytes > 10_485_760) {
|
|
72
|
+
throw new SshConfigurationError('maxOutputBytes must be an integer between 1 and 10485760.');
|
|
73
|
+
}
|
|
74
|
+
return { timeoutMs, maxOutputBytes };
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { createServer } from './server.js';
|
|
4
|
+
const server = createServer();
|
|
5
|
+
const transport = new StdioServerTransport();
|
|
6
|
+
await server.connect(transport);
|
|
7
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAEjF,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,MAAM,MAAM,GAAG,YAAY,EAAE,CAAC;AAC9B,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;AAE7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"package-metadata.d.ts","sourceRoot":"","sources":["../src/package-metadata.ts"],"names":[],"mappings":"AAcA,eAAO,MAAM,cAAc,QAA0B,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
const packageManifest = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
3
|
+
if (typeof packageManifest.version !== 'string' || packageManifest.version.length === 0) {
|
|
4
|
+
throw new Error('The package.json version is missing or invalid.');
|
|
5
|
+
}
|
|
6
|
+
export const packageVersion = packageManifest.version;
|
|
7
|
+
//# sourceMappingURL=package-metadata.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"package-metadata.js","sourceRoot":"","sources":["../src/package-metadata.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAMvC,MAAM,eAAe,GAAG,IAAI,CAAC,KAAK,CAChC,YAAY,CAAC,IAAI,GAAG,CAAC,iBAAiB,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAC/C,CAAC;AAErB,IAAI,OAAO,eAAe,CAAC,OAAO,KAAK,QAAQ,IAAI,eAAe,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;IACxF,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAC;AACrE,CAAC;AAED,MAAM,CAAC,MAAM,cAAc,GAAG,eAAe,CAAC,OAAO,CAAC"}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAwCpE,wBAAgB,YAAY,IAAI,SAAS,CAgGxC"}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { resolveConnectionOptions, resolveExecutionLimits } from './config.js';
|
|
4
|
+
import { packageVersion } from './package-metadata.js';
|
|
5
|
+
import { executeSshCommand, testSshConnection } from './ssh-client.js';
|
|
6
|
+
const sshConnectionSchema = {
|
|
7
|
+
host: z.string().min(1).optional().describe('SSH host; falls back to MCP_SSH_HOST.'),
|
|
8
|
+
port: z.number().int().min(1).max(65_535).optional().describe('SSH port; defaults to 22.'),
|
|
9
|
+
username: z.string().min(1).optional().describe('SSH username; falls back to MCP_SSH_USERNAME.'),
|
|
10
|
+
password: z.string().optional().describe('Password authentication.'),
|
|
11
|
+
privateKey: z.string().optional().describe('Private key contents for key authentication.'),
|
|
12
|
+
passphrase: z.string().optional().describe('Passphrase for the private key.'),
|
|
13
|
+
hostFingerprint: z
|
|
14
|
+
.string()
|
|
15
|
+
.min(1)
|
|
16
|
+
.optional()
|
|
17
|
+
.describe('Expected OpenSSH SHA256 host fingerprint; falls back to MCP_SSH_HOST_FINGERPRINT.'),
|
|
18
|
+
};
|
|
19
|
+
function errorMessage(error) {
|
|
20
|
+
return error instanceof Error ? error.message : 'Unknown SSH error.';
|
|
21
|
+
}
|
|
22
|
+
function outputText(result) {
|
|
23
|
+
const sections = [`Exit code: ${result.exitCode}`];
|
|
24
|
+
if (result.signal) {
|
|
25
|
+
sections.push(`Signal: ${result.signal}`);
|
|
26
|
+
}
|
|
27
|
+
sections.push(`STDOUT:\n${result.stdout || '(empty)'}`);
|
|
28
|
+
sections.push(`STDERR:\n${result.stderr || '(empty)'}`);
|
|
29
|
+
return sections.join('\n\n');
|
|
30
|
+
}
|
|
31
|
+
export function createServer() {
|
|
32
|
+
const server = new McpServer({
|
|
33
|
+
name: 'mcp-ssh',
|
|
34
|
+
version: packageVersion,
|
|
35
|
+
});
|
|
36
|
+
server.registerTool('ssh_exec', {
|
|
37
|
+
title: 'Execute an SSH command',
|
|
38
|
+
description: 'Connect to a remote host over SSH and execute one command. Connection credentials can be supplied per call or through MCP_SSH_* environment variables.',
|
|
39
|
+
inputSchema: {
|
|
40
|
+
...sshConnectionSchema,
|
|
41
|
+
command: z.string().min(1).describe('The shell command to execute remotely.'),
|
|
42
|
+
cwd: z.string().min(1).optional().describe('Remote working directory.'),
|
|
43
|
+
timeoutMs: z
|
|
44
|
+
.number()
|
|
45
|
+
.int()
|
|
46
|
+
.min(1_000)
|
|
47
|
+
.max(120_000)
|
|
48
|
+
.optional()
|
|
49
|
+
.describe('Connection and command timeout in milliseconds.'),
|
|
50
|
+
maxOutputBytes: z
|
|
51
|
+
.number()
|
|
52
|
+
.int()
|
|
53
|
+
.min(1)
|
|
54
|
+
.max(10_485_760)
|
|
55
|
+
.optional()
|
|
56
|
+
.describe('Maximum combined stdout and stderr size; defaults to 1 MiB.'),
|
|
57
|
+
},
|
|
58
|
+
}, async (input) => {
|
|
59
|
+
try {
|
|
60
|
+
const args = input;
|
|
61
|
+
const connection = resolveConnectionOptions(args);
|
|
62
|
+
const limits = resolveExecutionLimits(args);
|
|
63
|
+
const result = await executeSshCommand({
|
|
64
|
+
connection,
|
|
65
|
+
command: args.command,
|
|
66
|
+
...limits,
|
|
67
|
+
...(args.cwd ? { cwd: args.cwd } : {}),
|
|
68
|
+
});
|
|
69
|
+
return {
|
|
70
|
+
content: [{ type: 'text', text: outputText(result) }],
|
|
71
|
+
isError: result.exitCode !== 0,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
return {
|
|
76
|
+
content: [
|
|
77
|
+
{ type: 'text', text: `SSH execution failed: ${errorMessage(error)}` },
|
|
78
|
+
],
|
|
79
|
+
isError: true,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
server.registerTool('ssh_test_connection', {
|
|
84
|
+
title: 'Test an SSH connection',
|
|
85
|
+
description: 'Verify SSH credentials and connectivity without executing a remote command.',
|
|
86
|
+
inputSchema: {
|
|
87
|
+
...sshConnectionSchema,
|
|
88
|
+
timeoutMs: z
|
|
89
|
+
.number()
|
|
90
|
+
.int()
|
|
91
|
+
.min(1_000)
|
|
92
|
+
.max(120_000)
|
|
93
|
+
.optional()
|
|
94
|
+
.describe('Connection timeout in milliseconds.'),
|
|
95
|
+
},
|
|
96
|
+
}, async (input) => {
|
|
97
|
+
try {
|
|
98
|
+
const args = input;
|
|
99
|
+
const connection = resolveConnectionOptions(args);
|
|
100
|
+
const { timeoutMs } = resolveExecutionLimits(args);
|
|
101
|
+
await testSshConnection(connection, timeoutMs);
|
|
102
|
+
return {
|
|
103
|
+
content: [{ type: 'text', text: 'SSH connection succeeded.' }],
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
return {
|
|
108
|
+
content: [
|
|
109
|
+
{ type: 'text', text: `SSH connection failed: ${errorMessage(error)}` },
|
|
110
|
+
],
|
|
111
|
+
isError: true,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
return server;
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { SshConnectionOptions } from './config.js';
|
|
2
|
+
export interface ExecuteOptions {
|
|
3
|
+
connection: SshConnectionOptions;
|
|
4
|
+
command: string;
|
|
5
|
+
cwd?: string;
|
|
6
|
+
timeoutMs: number;
|
|
7
|
+
maxOutputBytes: number;
|
|
8
|
+
}
|
|
9
|
+
export interface ExecuteResult {
|
|
10
|
+
stdout: string;
|
|
11
|
+
stderr: string;
|
|
12
|
+
exitCode: number;
|
|
13
|
+
signal?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare class SshExecutionError extends Error {
|
|
16
|
+
constructor(message: string);
|
|
17
|
+
}
|
|
18
|
+
export declare function executeSshCommand(options: ExecuteOptions): Promise<ExecuteResult>;
|
|
19
|
+
export declare function testSshConnection(connection: SshConnectionOptions, timeoutMs: number): Promise<void>;
|
|
20
|
+
//# sourceMappingURL=ssh-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ssh-client.d.ts","sourceRoot":"","sources":["../src/ssh-client.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAExD,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,oBAAoB,CAAC;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,qBAAa,iBAAkB,SAAQ,KAAK;gBAC9B,OAAO,EAAE,MAAM;CAI5B;AAoHD,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC,CA4BvF;AAED,wBAAsB,iBAAiB,CACrC,UAAU,EAAE,oBAAoB,EAChC,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAoBf"}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { Client } from 'ssh2';
|
|
3
|
+
export class SshExecutionError extends Error {
|
|
4
|
+
constructor(message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = 'SshExecutionError';
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
function shellQuote(value) {
|
|
10
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
11
|
+
}
|
|
12
|
+
function commandWithWorkingDirectory(command, cwd) {
|
|
13
|
+
return cwd ? `cd ${shellQuote(cwd)} && ${command}` : command;
|
|
14
|
+
}
|
|
15
|
+
function fingerprintForHostKey(key) {
|
|
16
|
+
const digest = createHash('sha256').update(key).digest('base64').replace(/=+$/, '');
|
|
17
|
+
return `SHA256:${digest}`;
|
|
18
|
+
}
|
|
19
|
+
function normalizedFingerprint(value) {
|
|
20
|
+
const trimmed = value.trim();
|
|
21
|
+
return trimmed.startsWith('SHA256:') ? trimmed : `SHA256:${trimmed}`;
|
|
22
|
+
}
|
|
23
|
+
function connectConfig(connection, timeoutMs) {
|
|
24
|
+
const { hostFingerprint, ...sshConnection } = connection;
|
|
25
|
+
const expectedFingerprint = normalizedFingerprint(hostFingerprint);
|
|
26
|
+
return {
|
|
27
|
+
...sshConnection,
|
|
28
|
+
readyTimeout: timeoutMs,
|
|
29
|
+
hostVerifier: (key) => fingerprintForHostKey(key) === expectedFingerprint,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function appendChunk(chunks, chunk, state, maxOutputBytes) {
|
|
33
|
+
state.bytes += chunk.byteLength;
|
|
34
|
+
if (state.bytes > maxOutputBytes) {
|
|
35
|
+
throw new SshExecutionError(`Remote command output exceeded ${maxOutputBytes} bytes.`);
|
|
36
|
+
}
|
|
37
|
+
chunks.push(chunk);
|
|
38
|
+
}
|
|
39
|
+
function execOnClient(client, command, timeoutMs, maxOutputBytes) {
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
let channel;
|
|
42
|
+
let settled = false;
|
|
43
|
+
const stdout = [];
|
|
44
|
+
const stderr = [];
|
|
45
|
+
const outputState = { bytes: 0 };
|
|
46
|
+
const finish = (callback) => {
|
|
47
|
+
if (settled) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
settled = true;
|
|
51
|
+
clearTimeout(timer);
|
|
52
|
+
callback();
|
|
53
|
+
};
|
|
54
|
+
const timer = setTimeout(() => {
|
|
55
|
+
channel?.close();
|
|
56
|
+
finish(() => reject(new SshExecutionError(`Remote command timed out after ${timeoutMs} ms.`)));
|
|
57
|
+
}, timeoutMs);
|
|
58
|
+
client.exec(command, (error, nextChannel) => {
|
|
59
|
+
if (error) {
|
|
60
|
+
finish(() => reject(new SshExecutionError(`Unable to start remote command: ${error.message}`)));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
channel = nextChannel;
|
|
64
|
+
channel.on('data', (chunk) => {
|
|
65
|
+
try {
|
|
66
|
+
appendChunk(stdout, chunk, outputState, maxOutputBytes);
|
|
67
|
+
}
|
|
68
|
+
catch (chunkError) {
|
|
69
|
+
channel?.close();
|
|
70
|
+
finish(() => reject(chunkError));
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
channel.stderr.on('data', (chunk) => {
|
|
74
|
+
try {
|
|
75
|
+
appendChunk(stderr, chunk, outputState, maxOutputBytes);
|
|
76
|
+
}
|
|
77
|
+
catch (chunkError) {
|
|
78
|
+
channel?.close();
|
|
79
|
+
finish(() => reject(chunkError));
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
channel.once('error', (channelError) => {
|
|
83
|
+
finish(() => reject(new SshExecutionError(`SSH channel error: ${channelError.message}`)));
|
|
84
|
+
});
|
|
85
|
+
channel.once('close', (code, signal) => {
|
|
86
|
+
finish(() => resolve({
|
|
87
|
+
stdout: Buffer.concat(stdout).toString('utf8'),
|
|
88
|
+
stderr: Buffer.concat(stderr).toString('utf8'),
|
|
89
|
+
exitCode: code ?? (signal ? 1 : 0),
|
|
90
|
+
...(signal ? { signal } : {}),
|
|
91
|
+
}));
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
export async function executeSshCommand(options) {
|
|
97
|
+
const client = new Client();
|
|
98
|
+
try {
|
|
99
|
+
await new Promise((resolve, reject) => {
|
|
100
|
+
const onReady = () => {
|
|
101
|
+
client.removeListener('error', onError);
|
|
102
|
+
resolve();
|
|
103
|
+
};
|
|
104
|
+
const onError = (error) => {
|
|
105
|
+
client.removeListener('ready', onReady);
|
|
106
|
+
reject(new SshExecutionError(`SSH connection failed: ${error.message}`));
|
|
107
|
+
};
|
|
108
|
+
client.once('ready', onReady);
|
|
109
|
+
client.once('error', onError);
|
|
110
|
+
client.connect(connectConfig(options.connection, options.timeoutMs));
|
|
111
|
+
});
|
|
112
|
+
return await execOnClient(client, commandWithWorkingDirectory(options.command, options.cwd), options.timeoutMs, options.maxOutputBytes);
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
client.end();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
export async function testSshConnection(connection, timeoutMs) {
|
|
119
|
+
const client = new Client();
|
|
120
|
+
try {
|
|
121
|
+
await new Promise((resolve, reject) => {
|
|
122
|
+
const onReady = () => {
|
|
123
|
+
client.removeListener('error', onError);
|
|
124
|
+
resolve();
|
|
125
|
+
};
|
|
126
|
+
const onError = (error) => {
|
|
127
|
+
client.removeListener('ready', onReady);
|
|
128
|
+
reject(new SshExecutionError(`SSH connection failed: ${error.message}`));
|
|
129
|
+
};
|
|
130
|
+
client.once('ready', onReady);
|
|
131
|
+
client.once('error', onError);
|
|
132
|
+
client.connect(connectConfig(connection, timeoutMs));
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
client.end();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
//# sourceMappingURL=ssh-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ssh-client.js","sourceRoot":"","sources":["../src/ssh-client.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@huzhihui_c/mcp-ssh",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "An MCP server for executing commands over SSH.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"mcp-ssh": "dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc -p tsconfig.json",
|
|
18
|
+
"clean": "node -e \"import('node:fs').then(({rmSync}) => rmSync('dist', { recursive: true, force: true }))\"",
|
|
19
|
+
"dev": "tsx src/index.ts",
|
|
20
|
+
"prepublishOnly": "pnpm build",
|
|
21
|
+
"start": "node dist/index.js",
|
|
22
|
+
"test": "vitest run",
|
|
23
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=20"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
33
|
+
"ssh2": "^1.16.0",
|
|
34
|
+
"zod": "^3.25.76"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/ssh2": "^1.15.5",
|
|
38
|
+
"tsx": "^4.19.4"
|
|
39
|
+
}
|
|
40
|
+
}
|