# remote-connect-mcp **Repository Path**: 617726909/remote-connect-mcp ## Basic Information - **Project Name**: remote-connect-mcp - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-09-14 - **Last Updated**: 2026-09-30 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README [English](README.md) | 简体中文 # remote-connect-mcp 一个 MCP 服务器,为 AI 编码代理提供远程主机的 SSH 访问能力:登录、执行命令、 通过 SFTP 传输文件、维持长时间交互式会话——面向远程开发、联调、部署与测试 验证等工作流。 支持密码、私钥(含带口令保护的密钥)和 ssh-agent 认证。主机以命名 **profile** 的形式描述在 YAML 文件中(`~/.ssh/config.yaml`)。 ## 环境要求 - **运行**服务器需要 Node.js 20 或更高版本(MCP SDK 的下限) - **开发**需要 Node.js 22.12 或更高版本,测试运行器(Vitest 5)有此要求 - 目标主机上需要有 POSIX shell(命令类工具通过 `sh -c` 执行) ## 安装 ```bash git clone <本仓库> remote-connect-mcp cd remote-connect-mcp npm install ``` ## 配置 ```bash mkdir -p ~/.ssh && chmod 700 ~/.ssh cp config.example.yaml ~/.ssh/config.yaml chmod 600 ~/.ssh/config.yaml $EDITOR ~/.ssh/config.yaml ``` 从旧的 `~/.ssh-mcp` 布局升级?移动既有文件即可: ```bash mv ~/.ssh-mcp/config.yaml ~/.ssh/config.yaml mv ~/.ssh-mcp/known_hosts.json ~/.ssh/known_hosts.json ``` 一个最小 profile: ```yaml profiles: web-prod: host: web1.example.com username: deploy auth: type: privateKey privateKeyPath: ~/.ssh/id_ed25519 cwd: /srv/app sudoPassword: env:WEB_PROD_SUDO ``` 秘密值可写作 `env:NAME`(读取环境变量)或 `file:/path`(读取文件,去除一个 行尾换行)。裸字面量也可以,但启动时会产生警告;设置 `security.allowPlaintextSecrets: false` 可将其改为直接报错。 `security.localRoots` 限制上传的读取来源与下载的写入目标,默认为进程工作目录。 ## 环境变量 服务器从进程环境读取的每一个变量都有明确用途: | 变量 | 用途 | |---|---| | `SSH_MCP_CONFIG` | 配置**文件**路径。优先级最高。 | | `SSH_MCP_CONFIG_DIR` | 存放 `config.yaml` 的目录。默认 `~/.ssh`;设置 `SSH_MCP_CONFIG` 时被忽略。相对路径按服务器进程工作目录解析——建议使用 `~/` 前缀或绝对路径。 | | `SSH_AUTH_SOCK` | `auth.type: agent` 且未显式指定 `agent:` 路径的 profile 所用的 ssh-agent 套接字。 | | 用户自定义(经 `env:NAME`) | 秘密字段的取值来源——见下文*秘密间接引用*。 | | `SSH_MCP_TEST_SHELL` | 仅开发用:`bash` 不在 `PATH` 上时集成测试服务器所用的 shell。 | **配置文件定位优先级。** 服务器按以下顺序查找配置文件: 1. `SSH_MCP_CONFIG` —— 显式文件路径 2. `$SSH_MCP_CONFIG_DIR/config.yaml` 3. `~/.ssh/config.yaml` 默认位置没有文件不算错误:服务器以零 profile 启动,`ssh_connect` 仍接受临时 (ad-hoc)连接参数。但 `SSH_MCP_CONFIG` 指向的文件缺失**就是**错误——显式 请求的路径不存在是笔误,不是选择。 **秘密间接引用。** 携带秘密的字段(`auth.password`、`auth.passphrase`、 `auth.privateKey`、`sudoPassword`)接受四种写法: - `env:NAME` —— 环境变量 `NAME` 的值 - `file:/path` —— 文件内容,去除一个行尾换行(`~` 会展开) - `literal:VALUE` —— 显式字面量;值本身以 `env:` 开头时的转义手段 - 其他任何字符串 —— 字面量值,启动时警告;设置 `security.allowPlaintextSecrets: false` 后改为硬错误 `env:` 引用是懒解析的:认证秘密在 `ssh_connect` 执行时解析,sudo 密码在每次 `ssh_sudo_exec` 调用时解析。因此变量未设置只会让那一次调用失败,错误信息会 指明变量名——绝不会因你未连接的 profile 而阻断启动。变量必须存在于**服务器 进程**中:对 stdio MCP 宿主来说,就是*注册到宿主*一节展示的 `env` 块,或 导出了这些变量的父 shell。 **ssh-agent 回退。** `auth.type: agent` 依次尝试:profile 显式的 `agent:` 套接字路径、`$SSH_AUTH_SOCK`、Windows Pageant 命名管道(`pageant`)。 **主机密钥存储。** `defaults.hostKey.knownHostsPath` 默认为 `~/.ssh/known_hosts.json`——`.json` 后缀使其与同目录下 OpenSSH 自带的 `known_hosts` 文件互不干扰。 ## 注册到宿主 服务器通过 stdio 通信,由宿主以子进程方式启动。 **Claude Code** ```bash claude mcp add remote-connect -- npx tsx /absolute/path/to/remote-connect-mcp/src/index.ts ``` 或在 `.mcp.json` 中: ```json { "mcpServers": { "remote-connect": { "command": "npx", "args": ["tsx", "/absolute/path/to/remote-connect-mcp/src/index.ts"], "env": { "WEB_PROD_SUDO": "..." } } } } ``` **VS Code**(`.vscode/mcp.json`)与 **Cursor**(`~/.cursor/mcp.json`)结构 相同,VS Code 需额外加 `"type": "stdio"`: ```json { "servers": { "remote-connect": { "type": "stdio", "command": "npx", "args": ["tsx", "/absolute/path/to/remote-connect-mcp/src/index.ts"] } } } ``` ### 部署目录参数 上面的 `args` 硬编码了安装路径,程序一旦部署到别处,每台机器的 mcp.json 就会 产生差异。要让 `args` 固定下来,可以在 `env` 中声明一次安装目录——按惯例 命名为 `REMOTE_CONNECT_HOME`——并在 `args` 中引用它: ```json { "mcpServers": { "remote-connect": { "type": "stdio", "command": "npx", "args": ["tsx", "${REMOTE_CONNECT_HOME}/src/index.ts"], "env": { "REMOTE_CONNECT_HOME": "/opt/remote-connect-mcp", "SSH_MCP_CONFIG_DIR": "~/.ssh" } } } } ``` 此后换机器部署只需修改一个值:`REMOTE_CONNECT_HOME`。文件其余部分逐字节不变。 变量展开由 MCP **宿主**完成,支持程度不一: - **Claude Code** 会在 `.mcp.json` 的 `command`、`args`、`env` 字段中展开 `${VAR}`(及 `${VAR:-default}`)。 - 部分宿主使用其他语法(如 `${env:VAR}`),请查阅对应宿主的文档。 - 若宿主完全不支持变量展开,则退回在 `args` 中直接写安装绝对路径——所有宿主 都接受这种写法。 两点注意: - `REMOTE_CONNECT_HOME` 必须是**绝对**路径(或宿主支持的工作区变量)。相对 路径按宿主启动服务器的目录解析,而这一点没有契约保证。 - `SSH_MCP_CONFIG_DIR` 同理:`.mcp/ssh-config` 这类相对值按**服务器进程工作 目录**解析——通常是宿主启动它时所在的工作区,但并非契约行为。建议使用 `~/.ssh`、其他 `~/` 前缀路径或绝对路径。 需要更丰富的环境(密钥、agent 套接字、环境变量秘密)时,把父环境透传给宿主, 或从已导出这些变量的 shell 中启动宿主。 ## 工具 ### 连接 | 工具 | 用途 | |---|---| | `ssh_list_profiles` | 列出已配置的 profile(绝不返回秘密) | | `ssh_connect` | 按 profile 名建立连接,或使用临时连接参数 | | `ssh_list_connections` | 列出已打开的连接,含存活状态与最近使用时间 | | `ssh_close_connection` | 关闭连接及其上的所有会话 | 连接会一直保持,直到关闭或服务器退出,因此打开一个并复用——每条命令都重新 握手既慢又会使 sudo 的凭据缓存失效。 ### 命令 | 工具 | 用途 | |---|---| | `ssh_exec` | 执行单条命令;返回 stdout、stderr 与退出码 | | `ssh_sudo_exec` | 以 root 执行命令,密码经 stdin 喂入 | | `ssh_batch_exec` | 在多个 profile 上并发执行同一条命令 | **凡是非交互场景,`ssh_exec` 都是正确默认。** 它给出真实退出码,将 stdout 与 stderr 分离,且没有提示符噪音。支持 `cwd`、附加 `env`、超时与输出预算。 `ssh_sudo_exec` 把整条命令包进 sudo 之下的 `sh -c`,因此 `a && b` 的*两部分* 都会提权——裸写 `sudo a && b` 只会提升 `a`。密码写入 sudo 的 stdin,绝不会 出现在 `ps` 可见的命令行上。已有的 sudo 时间戳会被尽量复用(注意时间戳作用域 是登录会话,这正是复用同一条连接如此重要的原因)。 ### 文件 | 工具 | 用途 | |---|---| | `ssh_upload` / `ssh_download` | 传输单个文件,自动创建缺失的父目录 | | `ssh_transfer_dir` | 上行或下行传输整棵目录树 | | `ssh_ls` / `ssh_stat` | 查看远端路径 | | `ssh_mkdir` / `ssh_remove` / `ssh_rename` / `ssh_chmod` | 操作远端路径 | `ssh_mkdir` 默认递归。`ssh_remove` 对非空目录要求显式 `recursive: true`。 两侧的符号链接都绝不跟随。 ### 交互式会话 | 工具 | 用途 | |---|---| | `ssh_session_open` / `ssh_session_close` / `ssh_session_list` | 管理会话 | | `ssh_session_run` | 执行命令并等待其结束 | | `ssh_session_send` / `ssh_session_read` | 直接驱动程序 | 仅当状态必须跨调用存续——REPL、切换过的目录、导出的变量——或程序需要终端时 才使用会话。否则 `ssh_exec` 更简单也更可靠。 PTY shell 没有"命令已结束"信号,因此服务器在每次 `ssh_session_run` 前后用 哨兵 `printf` 调用包夹命令并检测标记。会话启动时设置 `stty -echo`,命令行 不会被回显。结束一次运行有三种情况:标记到达(`complete: true`)、输出安静 超过 `quietMs`(部分结果,快速返回)、或超时触发(`timedOut: true`)。出现 二级提示符说明命令把 shell 留在了未完结语句中——此时报告 `continuation: true` 并发送 Ctrl-C 以恢复提示符。 **对全屏程序,哨兵不是完成信号。** 一旦 `vim`、`top` 或嵌套 `ssh` 接管终端, 哨兵就成了程序输入,永远不会执行。请改用 `ssh_session_send` + `ssh_session_read` 长轮询驱动它们。 ## 安全模型 这是一个高特权工具:它持有 SSH 凭据并能执行任意命令。只把它连接到你信任的 MCP 客户端。 它做的事: - **主机密钥校验(TOFU)。** 首次连接主机时,其密钥指纹被记入 `~/.ssh/known_hosts.json`;之后不匹配即拒绝,并给出期望与实际两个指纹。 `hostKey.policy: strict` 会拒绝任何尚未记录的主机。校验器刻意采用同步 实现——异步版本会返回 Promise,而 ssh2 把它当真值处理,会静默放行所有主机。 - **秘密间接引用。** `env:` 与 `file:` 引用使凭据不落入配置文件。明文会被 警告或拒绝。 - **本地路径沙箱。** 上传来源与下载目标必须(穿过符号链接后)解析到 `security.localRoots` 之内。这阻止模型读取从未属于任务范围的文件。 - **不泄漏秘密。** `ssh_list_profiles` 只返回主机、端口、用户名与认证方式。 命令输出返回前会扫描 sudo 密码。 - **传输绝不跟随符号链接。** 上传侧阻止沙箱内的链接触及外部文件;下载侧阻止 经由恶意链接写入。可能逃出目标目录的远端条目名会被直接拒绝。 - **配置权限检查。** POSIX 上,组或其他用户可读的配置文件会产生警告。 - **干净退出。** 退出、SIGINT、SIGTERM 时关闭所有会话与连接。 它**不做**的事: - 远端路径没有沙箱。远端主机归你所有;你让它删什么它就删什么。 - 本地沙箱是先检查后使用。能在沙箱根内写入的本地攻击者,可在检查与写入之间 把组件换成符号链接。彻底关闭该窗口需要对每级路径组件使用 `O_NOFOLLOW`, 而 Node 没有可移植的暴露方式。 - 没有端口转发。若你需要:ssh2 完全用 JS 实现(Windows 与 Linux 行为一致), 唯一的真实约束是*服务器端*的 `AllowTcpForwarding` 设置——但绑定本地端口、 尤其是动态 SOCKS 代理,是显著的新暴露面,本服务器不会替你打开。 ## 值得了解的行为 - **stdout 是协议通道。** 所有诊断走 stderr。一个多余的 `console.log` 就会 损坏流,因此不要添加。 - **输出被截断,而非丢弃。** 大输出保留头部与尾部,中间是 `... [truncated N bytes of M total] ...` 标记,末尾处的失败依然可见。 截断信息也会出现在结构化结果中。 - **长操作报告进度。** 客户端提供进度令牌时,`ssh_batch_exec`、 `ssh_transfer_dir` 与传输类工具会发出 `notifications/progress`,避免客户端 的单请求超时先触发。 - **`ssh_session_read` 单次最多等待 25 秒**,稳妥落在客户端默认 60 秒请求 超时之内。 - **命令超时不杀连接。** 超时命令收到 SIGTERM,随后其通道被强制关闭;连接 仍可继续使用。 ## 开发 ```bash npm run typecheck # tsc --noEmit npm test # 单元 + 集成 npm run dev # 以 stdio 运行服务器 npm run inspector # 用 MCP Inspector 操作它 ``` 测试套件运行于进程内 `ssh2` 服务器之上:用真实 POSIX shell 应答 `exec` 与 `shell` 请求,外加本地文件系统支撑的假 `SFTPWrapper`。集成测试需要 `bash` 与 `ssh-keygen`(首次运行时生成密钥夹具);`bash` 不在 `PATH` 上时设置 `SSH_MCP_TEST_SHELL`。缺失时集成套件跳过,单元套件照常运行。 ## 许可证 MIT