From 9d700a412d1f564c69c2cf695113b6b26d2567d9 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Wed, 28 Jan 2026 11:03:05 +0800 Subject: [PATCH] add mcp-ssh-manager config --- .mcp.json | 11 ++ docs/mcp/install-launchd.sh | 222 ++++++++++++++++++++++++++++++ docs/mcp/mcp-ssh-manager-macos.md | 204 +++++++++++++++++++++++++++ 3 files changed, 437 insertions(+) create mode 100644 .mcp.json create mode 100644 docs/mcp/install-launchd.sh create mode 100644 docs/mcp/mcp-ssh-manager-macos.md diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 000000000..fbbe3e742 --- /dev/null +++ b/.mcp.json @@ -0,0 +1,11 @@ +{ + "mcpServers": { + "ssh-manager": { + "command": "node", + "args": ["/tmp/mcp-ssh-manager/node_modules/mcp-ssh-manager/src/index.js"], + "env": { + "DOTENV_CONFIG_PATH": ".env" + } + } + } +} diff --git a/docs/mcp/install-launchd.sh b/docs/mcp/install-launchd.sh new file mode 100644 index 000000000..701ce2d2a --- /dev/null +++ b/docs/mcp/install-launchd.sh @@ -0,0 +1,222 @@ +#!/usr/bin/env bash +set -euo pipefail + +LABEL="com.bvisible.mcp-ssh-manager" +PLIST_PATH="$HOME/Library/LaunchAgents/${LABEL}.plist" +LOG_DIR="$HOME/Library/Logs/mcp-ssh-manager" +OUT_LOG="$LOG_DIR/out.log" +ERR_LOG="$LOG_DIR/err.log" + +fail_cleanup() { + if launchctl print "gui/$(id -u)/${LABEL}" >/dev/null 2>&1; then + launchctl bootout "gui/$(id -u)" "$PLIST_PATH" >/dev/null 2>&1 || true + fi + if [ -f "$PLIST_PATH" ]; then + rm -f "$PLIST_PATH" || true + fi +} + +usage() { + echo "usage: $0 {install|uninstall|status|logs}" +} + +pick_node() { + if [ -x /opt/homebrew/bin/node ]; then + echo /opt/homebrew/bin/node + elif [ -x /usr/local/bin/node ]; then + echo /usr/local/bin/node + elif command -v node >/dev/null 2>&1; then + command -v node + else + echo "" + fi +} + +find_repo_up() { + local dir="$PWD" + for _ in 0 1 2 3 4 5; do + local pj="$dir/package.json" + if [ -f "$pj" ] && /usr/bin/grep -E '"name"[[:space:]]*:[[:space:]]*"[^\"]*mcp-ssh-manager' "$pj" >/dev/null 2>&1; then + echo "$dir" + return 0 + fi + dir="$(dirname "$dir")" + done + return 1 +} + +find_repo_search() { + local base + for base in "$HOME/code" "$HOME/projects" "$HOME/src"; do + if [ -d "$base" ]; then + local hit + hit="$(find "$base" -maxdepth 3 -type d -name '*mcp-ssh-manager*' 2>/dev/null | head -n 1)" + if [ -n "$hit" ]; then + echo "$hit" + return 0 + fi + fi + done + return 1 +} + +find_binary_under() { + local base + for base in /usr/local /opt/homebrew; do + if [ -d "$base" ]; then + local hit + hit="$(find "$base" -maxdepth 4 -type f -name 'mcp-ssh-manager' 2>/dev/null | head -n 1)" + if [ -n "$hit" ]; then + echo "$hit" + return 0 + fi + fi + done + return 1 +} + +write_plist() { + local workdir="$1" + shift + local -a args=("$@") + + mkdir -p "$(dirname "$PLIST_PATH")" "$LOG_DIR" + + { + echo '' + echo '' + echo '' + echo '' + echo ' Label' + echo " ${LABEL}" + echo '' + echo ' ProgramArguments' + echo ' ' + for arg in "${args[@]}"; do + echo " ${arg}" + done + echo ' ' + if [ -n "$workdir" ]; then + echo '' + echo ' WorkingDirectory' + echo " ${workdir}" + fi + echo '' + echo ' EnvironmentVariables' + echo ' ' + echo ' PATH' + echo ' /opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin' + echo ' ' + echo '' + echo ' RunAtLoad' + echo ' ' + echo '' + echo ' KeepAlive' + echo ' ' + echo '' + echo ' StandardOutPath' + echo " ${OUT_LOG}" + echo '' + echo ' StandardErrorPath' + echo " ${ERR_LOG}" + echo '' + echo '' + } > "$PLIST_PATH" +} + +install() { + trap 'fail_cleanup' ERR + + local binary_path="" + if command -v mcp-ssh-manager >/dev/null 2>&1; then + binary_path="$(command -v mcp-ssh-manager)" + else + binary_path="$(find_binary_under || true)" + fi + + if [ -n "$binary_path" ]; then + echo "using binary: $binary_path" + write_plist "" "$binary_path" + else + local repo="" + repo="$(find_repo_up || true)" + if [ -z "$repo" ]; then + repo="$(find_repo_search || true)" + fi + + if [ -z "$repo" ]; then + echo "could not find mcp-ssh-manager binary or repo" >&2 + exit 1 + fi + + local pj="$repo/package.json" + local has_start="" + if [ -f "$pj" ] && /usr/bin/grep -E '"start"[[:space:]]*:' "$pj" >/dev/null 2>&1; then + has_start=1 + fi + + if [ -n "$has_start" ]; then + local pm="" + if [ -f "$repo/pnpm-lock.yaml" ] && command -v pnpm >/dev/null 2>&1; then + pm="$(command -v pnpm)" + echo "using pnpm start in repo: $repo" + write_plist "$repo" "$pm" "start" + elif command -v npm >/dev/null 2>&1; then + pm="$(command -v npm)" + echo "using npm run start in repo: $repo" + write_plist "$repo" "$pm" "run" "start" + else + echo "start script exists but npm or pnpm not found" >&2 + exit 1 + fi + else + local node + node="$(pick_node)" + if [ -z "$node" ]; then + echo "node not found" >&2 + exit 1 + fi + local entry="" + for p in "dist/index.js" "build/index.js" "index.js" "src/index.ts"; do + if [ -f "$repo/$p" ]; then + entry="$repo/$p" + break + fi + done + if [ -z "$entry" ]; then + echo "no entry file found in repo" >&2 + exit 1 + fi + echo "using node entry: $entry" + write_plist "$repo" "$node" "$entry" + fi + fi + + launchctl bootstrap "gui/$(id -u)" "$PLIST_PATH" + launchctl kickstart -k "gui/$(id -u)/${LABEL}" + + echo "installed: $PLIST_PATH" + echo "logs: $LOG_DIR" +} + +uninstall() { + launchctl bootout "gui/$(id -u)" "$PLIST_PATH" >/dev/null 2>&1 || true + rm -f "$PLIST_PATH" + echo "removed: $PLIST_PATH" +} + +status() { + launchctl print "gui/$(id -u)/${LABEL}" +} + +logs() { + tail -f "$OUT_LOG" "$ERR_LOG" +} + +case "${1:-}" in + install) install ;; + uninstall) uninstall ;; + status) status ;; + logs) logs ;; + *) usage; exit 1 ;; +esac diff --git a/docs/mcp/mcp-ssh-manager-macos.md b/docs/mcp/mcp-ssh-manager-macos.md new file mode 100644 index 000000000..c0b5e6337 --- /dev/null +++ b/docs/mcp/mcp-ssh-manager-macos.md @@ -0,0 +1,204 @@ +# mcp-ssh-manager macOS 自启 launchd + +本指南适用于 **macOS 13+(含 macOS 26)**,使用 **launchd** 设置登录后自启。方案可回滚、可调试、可日志追踪,不修改业务代码,也不使用第三方守护工具。 + +## 1) 自适应探测规则 + +脚本会按顺序自动探测启动方式,成功即用: + +1) 若 `command -v mcp-ssh-manager` 存在,使用其绝对路径作为 `ProgramArguments[0]`。 +2) 否则按以下范围搜索仓库或安装路径: + - 当前工作目录及其父级(最多向上 5 层),查找 `package.json` 且 `name` 含 `mcp-ssh-manager`。 + - `~/code`、`~/projects`、`~/src` 下搜索目录名包含 `mcp-ssh-manager`,深度 `<=3`。 + - `/usr/local`、`/opt/homebrew` 下搜索 `mcp-ssh-manager` 相关文件。 +3) 若找到 Node 项目: + - 优先 `pnpm start` 或 `npm run start`(`scripts.start` 存在时)。 + - 否则使用 `node `,入口依次为:`dist/index.js` -> `build/index.js` -> `index.js` -> `src/index.ts`。 +4) Node 解释器绝对路径优先级:`/opt/homebrew/bin/node` -> `/usr/local/bin/node` -> `which node`。 +5) `ProgramArguments` 使用绝对路径,禁止依赖 shell PATH 或 `~/.zshrc`。 + +## 2) 安装脚本 + +同目录脚本:`docs/mcp/install-launchd.sh`。用法: + +``` +./install-launchd.sh install +``` + +脚本会生成并安装 plist: + +``` +~/Library/LaunchAgents/com.bvisible.mcp-ssh-manager.plist +``` + +日志目录: + +``` +~/Library/Logs/mcp-ssh-manager/ +``` + +## 3) 生成的 plist 完整内容 + +脚本会根据探测结果生成 plist。以下为两种可能的完整内容示例。 + +**示例 A:直接使用二进制** + +``` + + + + + Label + com.bvisible.mcp-ssh-manager + + ProgramArguments + + /usr/local/bin/mcp-ssh-manager + + + EnvironmentVariables + + PATH + /opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin + + + RunAtLoad + + + KeepAlive + + + StandardOutPath + /Users/USER/Library/Logs/mcp-ssh-manager/out.log + + StandardErrorPath + /Users/USER/Library/Logs/mcp-ssh-manager/err.log + + +``` + +**示例 B:Node 项目入口** + +``` + + + + + Label + com.bvisible.mcp-ssh-manager + + ProgramArguments + + /opt/homebrew/bin/node + /Users/USER/path/to/mcp-ssh-manager/dist/index.js + + + WorkingDirectory + /Users/USER/path/to/mcp-ssh-manager + + EnvironmentVariables + + PATH + /opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin + + + RunAtLoad + + + KeepAlive + + + StandardOutPath + /Users/USER/Library/Logs/mcp-ssh-manager/out.log + + StandardErrorPath + /Users/USER/Library/Logs/mcp-ssh-manager/err.log + + +``` + +## 4) plist 关键字段说明 + +- `Label`:launchd 唯一标识,用于 `launchctl` 管理。 +- `ProgramArguments`:启动命令与参数,必须为绝对路径,禁止使用 shell 包裹。 +- `WorkingDirectory`:Node 项目使用的工作目录,必须为绝对路径。 +- `EnvironmentVariables`:明确指定 PATH,避免 launchd 的默认 PATH 过短。 +- `RunAtLoad`:plist 加载时立即启动。 +- `KeepAlive`:进程退出时自动拉起。 +- `StandardOutPath`/`StandardErrorPath`:stdout 与 stderr 日志路径。 + +## 5) 一键命令 + +以下命令均可直接复制执行: + +``` +./install-launchd.sh install +./install-launchd.sh uninstall +./install-launchd.sh status +./install-launchd.sh logs +``` + +## 6) 手动命令 + +**安装与启动:** + +``` +launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.bvisible.mcp-ssh-manager.plist +launchctl kickstart -k gui/$(id -u)/com.bvisible.mcp-ssh-manager +``` + +**卸载与停止:** + +``` +launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.bvisible.mcp-ssh-manager.plist +rm -f ~/Library/LaunchAgents/com.bvisible.mcp-ssh-manager.plist +``` + +**状态与调试:** + +``` +launchctl print gui/$(id -u)/com.bvisible.mcp-ssh-manager +``` + +**日志追踪:** + +``` +tail -f ~/Library/Logs/mcp-ssh-manager/out.log ~/Library/Logs/mcp-ssh-manager/err.log +``` + +## 7) 验证是否自启成功 + +1) 登录后检查 launchd 状态: + +``` +launchctl print gui/$(id -u)/com.bvisible.mcp-ssh-manager +``` + +2) 查看日志是否有持续输出: + +``` +tail -f ~/Library/Logs/mcp-ssh-manager/out.log +``` + +## 8) 常见问题与排查 + +- **权限问题**:LaunchAgents 以当前用户运行,避免写入 root 目录。 +- **WorkingDirectory 不存在**:确保仓库路径存在且为绝对路径。 +- **Node 路径错误**:确认 `/opt/homebrew/bin/node` 或 `/usr/local/bin/node` 可执行。 +- **端口占用**:检查已有进程是否占用目标端口,必要时先停止旧进程。 +- **PATH 不一致**:保证 `EnvironmentVariables.PATH` 包含常用路径。 + +**临时前台运行对照验证:** + +- 若使用二进制: + +``` +/usr/local/bin/mcp-ssh-manager +``` + +- 若使用 Node 入口: + +``` +/opt/homebrew/bin/node /Users/USER/path/to/mcp-ssh-manager/dist/index.js +``` +