Skip to main content
Glama
thekk1
by thekk1

ssh-mcp

一个 MCP 服务器,让 LLM 能够通过 SSH 运行 shell 命令,使用每个用户自己的个人 SSH 密钥进行身份验证,而不是使用单个共享服务账户。专为多用户聊天平台(例如 LibreChat)构建,在这些平台中服务器是共享的,但每个请求的 SSH 身份不应共享。

只有一个工具:ssh_exec(host, port, username, command)。没有主机允许列表,没有命令白名单——原因见下文"安全模型",以及这对任何部署此项目的人意味着什么。

为什么存在

在编写此项目之前,检查了几个现有的开源 SSH MCP 服务器(vignitin/multi-ssh-mcp、giuliolibrando/ssh-mcp-server、tufantunc/ssh-mcp)。它们都不支持按请求提供凭据:每个都将单个主机/用户/凭据固化到环境变量或启动时的配置文件中,这仅适用于单用户部署或共享服务账户。这些都不适合许多不同的人(每个人都有自己的 SSH 密钥)共享一个正在运行的 MCP 服务器的场景。

因此,这是一个针对 asyncssh 构建的小型、专用服务器,而不是现有工具的包装器——没有适合包装的东西。

Related MCP server: terminal-mcp-server

工作原理

MCP client --(streamable-http, /mcp, per-request headers)--> ssh-mcp
                                                                  |
                                                                  | asyncssh,
                                                                  | one connection
                                                                  | per tool call
                                                                  v
                                                            arbitrary target host

凭据作为按请求的 HTTP 头传输,而不是服务器配置:

  • x-ssh-private-key —— 用于身份验证的私钥,base64 编码(原始的多行 PEM 块无法作为 HTTP 头值使用)

  • x-ssh-key-passphrase —— 可选,如果该密钥受密码短语保护

两者在每次工具调用时重新读取、解码、直接交给 asyncssh,然后丢弃——不会写入磁盘,也不会跨请求缓存。由调用客户端负责为正确的用户附加正确的头;有关一种实现方式,请参阅下面的"与 LibreChat 一起使用"。

主机密钥使用真正的信任首次使用(TOFU),而不是"始终接受任何内容":首次连接到给定的 host:port 会将其密钥指纹固定到磁盘上的 JSON 文件(hostkeys.py);之后的每次连接必须完全匹配该固定值,否则会被拒绝并显示 host_key_mismatch。这无法阻止在首次接触主机时的中间人攻击,但它会将之后未经宣布的密钥更改——轮换或真正的攻击——变成响亮、明确的失败,而不是无声的漏洞。

MCP 连接本身没有 API 密钥或 bearer token 门禁。这是针对特定部署形态的刻意简化选择:一个只能从受信任的内部网络访问的服务器,客户端自己附加每用户 SSH 凭据(见下文),网络位置是实际的访问边界。如果你在不太受信任的地方暴露此服务,请在它前面加一个门禁——本项目不包含门禁。

安全模型

ssh_exec 不过滤允许哪些主机、命令或用户。调用者传递的任何主机/端口/用户名/命令都会被尝试,仅此而已。这是一个刻意的权衡,而不是疏忽:从 MCP 服务器内部按主机或命令过滤将是安全剧场,因为任何拥有有效密钥的调用者都可以直接通过 SSH 访问那里,而不需要这个工具。真正介于请求和真实 shell 之间的两件事是:

  1. 任何能够访问此服务器并设置凭据头的人——完全在此代码的控制之外。如果你在多租户客户端后面部署此服务,限制哪些用户能看到/使用此工具是那个客户端的工作(有关一种具体方法,请参阅"与 LibreChat 一起使用")。

  2. 与所使用的密钥关联的真实 Unix 权限。 ssh_exec 恰好以该密钥目标账户拥有的权限运行——不多不少。

如果这两者在给定部署中都没有被实际执行,这个工具就等同于在每个调用者的密钥能到达的每台主机上给他们一个裸终端。这就是预期的模型——SSH 自己的授权,而不是重新实现它——所以在部署之前请确保这确实是你想要的模型。

缺少主机/用户名:通过 MCP elicitation 询问,而不是由模型猜测

host 和 username 刻意不在工具的 required schema 字段中(command 保持必填——决定运行什么是模型的工作,而不是人的)。将主机/用户名设为必填会使符合规范的模型在缺少它们时拒绝调用工具,并自己即兴提出一个纯文本的后续问题——这正是此设计要避免的 UX。当任一缺失时,ssh_mcp/app.py 中的 elicit_missing_ssh_args() 通过 MCP elicitation(elicitation/create,表单模式)直接以一个组合表单询问人类——不是模型必须自己措辞的东西,也不是每个字段的单独往返。port 在同一表单中随行,通过 schema 的 default 预填其通常默认值(22),可编辑,但当它是唯一未设置的内容时本身不会成为中断的理由。

在不支持 elicitation 的客户端上,这会安全降级。 elicit_missing_ssh_args() 在发送请求之前检查客户端声明的能力(session.check_client_capability(...)),并捕获调用本身的任何失败;无论哪种方式,它都会回退到普通的 missing_host/missing_username 错误,模型仍然可以将其作为文本问题转达,而不是工具调用出错或挂起。Elicitation 支持因客户端而异——在撰写本文时,几个流行的 MCP 客户端(包括 LibreChat)尚未实现它,所以这今天主要是作为向前兼容的基础工作。在不支持时零成本,并在任何以后添加真实 elicitation 支持的客户端上自动激活,无需在此处进行任何更改。

已通过 mcp.shared.memory 的内存传输使用真实的 ClientSession 验证:在注册和不注册 elicitation_callback 的情况下,接受/拒绝/取消分支,以及完整的组合表单(主机 + 用户名缺失,端口的默认值被覆盖)——确认所有三个值确实按 elicitation 的结果到达 SSH 调用,而不仅仅是根据阅读规范来断言。

与 LibreChat 一起使用

LibreChat 可以通过 customUserVars 将每用户的值附加到 MCP 请求头——每个用户在设置中输入一次自己的密钥,LibreChat 在每次该用户的请求中将其注入到配置的头中。librechat.yaml:

mcpServers:
  ssh:
    type: streamable-http
    url: http://ssh-mcp:8080/mcp
    serverInstructions: true
    headers:
      X-SSH-Private-Key: '{{SSH_PRIVATE_KEY}}'
      X-SSH-Key-Passphrase: '{{SSH_KEY_PASSPHRASE}}'
    customUserVars:
      SSH_PRIVATE_KEY:
        title: "SSH Private Key (Base64)"
        description: "Your personal SSH private key, base64-encoded: `base64 -w0 ~/.ssh/id_ed25519`"
      SSH_KEY_PASSPHRASE:
        title: "SSH Key Passphrase (optional)"
        description: "Only fill in if your private key is passphrase-protected"

两个 customUserVars 条目都需要同时有 title 和 description——仅 title 的条目会在启动时使 LibreChat 的配置验证失败,并抛出 ZodError,令人困惑的是,该错误会针对看起来不相关的字段报告(LibreChat 将整个 mcpServers 块作为一个传输类型联合体验证,因此一个缺失字段会同时表现为几个看似无关的错误)。librechat.yaml 仅在容器启动时读取——编辑后重启 LibreChat。

限制哪些用户可以看到此服务器

本项目中没有限制谁可以使用它——任何能设置 SSH_PRIVATE_KEY 头的用户都可以调用 ssh_exec。如果你需要将其限制为用户的子集,那必须在 LibreChat(或你使用的任何客户端)中实现,而不是在这里。截至 LibreChat 0.8.5+,其管理面板有一个配置覆盖系统(Configuration Management),可以将额外的 mcpServers 条目限定到特定角色或组——该组之外的用户在其解析后的配置中完全没有 ssh 条目,而不仅仅是一个隐藏的条目。在依赖此功能之前,有两件事值得针对你自己的 LibreChat 版本检查,而不是假设:

  • 截至撰写本文时,它被记录为**"预览版"**,而非 GA。

  • 有一个已知的历史问题:组范围的覆盖静默不生效,而角色范围的覆盖生效(danny-avila/LibreChat#13172)。通过直接测试确认修复在你的运行版本中——将用户放入/移出组,检查服务器是否真的为他们(不)出现。

运行

docker build -t ssh-mcp .
docker run --rm -p 8080:8080 -v ssh-mcp-hostkeys:/data ssh-mcp

/data 卷是让 TOFU 主机密钥固定值在容器重建后存活的关键——没有它,每次重新部署都会忘记所有以前见过的主机密钥,并在下次接触时重新固定(不是安全漏洞,只是暂时失去"检测后续更改"的属性,直到每台主机被重新接触一次)。

示例 docker-compose.yml 服务,从本地克隆构建:

services:
  ssh-mcp:
    build: .
    container_name: ssh-mcp
    volumes:
      - ssh-mcp-hostkeys:/data
    restart: always

volumes:
  ssh-mcp-hostkeys:

验证

curl -s http://127.0.0.1:8080/readyz   # "ok" once the session manager is up

已手动端到端验证(不仅仅是单元测试):构建镜像,运行它,通过 streamable-http 连接真实的 MCP 客户端并携带凭据头,tools/list 显示 ssh_exec,tools/call 针对一个正在运行的、基于 asyncssh 的临时 SSH 服务器执行了真实命令,经过真实的 SSH 握手并返回其实际 stdout。还直接(绕过 HTTP 层)针对同一个临时服务器进行了测试:首次接触时 TOFU 固定,匹配的第二次接触时接受,更改/不匹配的主机密钥时硬拒绝,垃圾私钥被拒绝为 invalid_key,未授权密钥被拒绝为 connection_failed,非零远程退出码作为 ok: true 传递并带有该退出码(不视为工具失败)。

测试

pip install -e '.[dev]'
pytest

单元测试(凭据头解析、TOFU 固定/接受/拒绝逻辑、工具 schema、elicit_missing_ssh_args 针对假会话的能力检查/字段选择/接受/拒绝/取消/失败分支)——没有真实网络、子进程或 MCP 传输。真实握手场景和真实 ClientSession elicitation 往返(见上文)是手动运行的,不属于自动化套件的一部分。

Related MCP Connectors

Related MCP Servers

  • A
    license
    B
    quality
    D
    maintenance
    Enables secure SSH connections to remote servers for executing shell commands and managing active sessions. It supports authentication via passwords or private keys and provides optional host-based access control.
    4
    106 npm
    MIT
  • A
    license
    C
    quality
    F
    maintenance
    Enables SSH remote command execution on remote machines with persistent connections, supporting automatic key discovery and connection pooling.
    2
    14 npm
    MIT
  • F
    license
    B
    quality
    D
    maintenance
    Enables SSH-based deployment operations such as git pull, command execution, script upload/run, and SSH config management.
    4
    -