#!/usr/bin/env bash set -euo pipefail # Fleet launches execute this source through an already-validated absolute bash # capability and pass all interpreter capabilities explicitly. Do not add PATH # lookup here: this helper is intentionally capability-minimal. MODE="apply" RUNTIME="all" STRICT_CHECK=0 CLAUDE_CONFIG_DIR="" PYTHON_BIN="" NODE_BIN="" NPX_BIN="" TIMEOUT_BIN="" PKG="@modelcontextprotocol/server-sequential-thinking" err() { echo "[mosaic-seq] ERROR: $*" >&2; } log() { echo "[mosaic-seq] $*"; } while [[ $# -gt 0 ]]; do case "$1" in --check) MODE="check"; shift ;; --runtime) RUNTIME="${2:?--runtime requires a value}"; shift 2 ;; --strict) STRICT_CHECK=1; shift ;; --claude-config-dir) CLAUDE_CONFIG_DIR="${2:?--claude-config-dir requires a value}"; shift 2 ;; --python-bin) PYTHON_BIN="${2:?--python-bin requires a value}"; shift 2 ;; --node-bin) NODE_BIN="${2:?--node-bin requires a value}"; shift 2 ;; --npx-bin) NPX_BIN="${2:?--npx-bin requires a value}"; shift 2 ;; --timeout-bin) TIMEOUT_BIN="${2:?--timeout-bin requires a value}"; shift 2 ;; *) err "Unknown argument: $1"; exit 2 ;; esac done case "$RUNTIME" in all|claude|codex|opencode) ;; *) err "Invalid runtime: $RUNTIME"; exit 2;; esac # Explicit fleet-seat operation is capability-minimal. Legacy operator repair # keeps its documented PATH-based compatibility contract. if [[ -n "$CLAUDE_CONFIG_DIR" || -n "$PYTHON_BIN$NODE_BIN$NPX_BIN$TIMEOUT_BIN" ]]; then [[ -n "$PYTHON_BIN" && -n "$NODE_BIN" && -n "$NPX_BIN" ]] || { err "Fleet capabilities are required"; exit 2; } else PYTHON_BIN=python3 NODE_BIN=node NPX_BIN=npx TIMEOUT_BIN=timeout fi warm_package() { local timeout_sec="${MOSAIC_SEQ_WARM_TIMEOUT_SEC:-15}" if [[ -n "$TIMEOUT_BIN" ]]; then "$TIMEOUT_BIN" "$timeout_sec" "$NPX_BIN" -y "$PKG" --help >/dev/null 2>&1 else "$NPX_BIN" -y "$PKG" --help >/dev/null 2>&1; fi } claude_config_python='import json, os, stat, tempfile from pathlib import Path def die(): raise SystemExit(1) def secure_dir(p): p=Path(p) if not p.is_absolute(): die() # Every parent may be sticky /tmp, but none may be a symlink. The fleet # config root itself must be private and owned by the invoking principal. for q in [p, *p.parents]: try: s=os.lstat(q) except OSError: die() if stat.S_ISLNK(s.st_mode) or not stat.S_ISDIR(s.st_mode): die() if q != p and s.st_mode & 0o022 and not (s.st_mode & stat.S_ISVTX): die() s=os.lstat(p) if s.st_uid not in (os.geteuid(), 0) or s.st_mode & 0o022: die() return p def read_private(p): try: fd=os.open(p, os.O_RDONLY|os.O_NOFOLLOW|os.O_NONBLOCK) except OSError: die() try: s=os.fstat(fd) if not stat.S_ISREG(s.st_mode) or s.st_uid not in (os.geteuid(),0) or s.st_mode & 0o077 or s.st_size>1048576: die() data=b"" while len(data)<=1048576: c=os.read(fd,65536) if not c: break data+=c if len(data)>1048576: die() return data, (s.st_dev,s.st_ino) finally: os.close(fd) def entry_ok(data): try: d=json.loads(data.decode()); e=d.get("mcpServers",{}).get("sequential-thinking",{}) except Exception: return False return e.get("command")=="npx" and e.get("args")==["-y","@modelcontextprotocol/server-sequential-thinking"] def explicit_check_or_apply(apply): root=secure_dir(os.environ["CLAUDE_CONFIG_DIR"]); p=root/".claude.json" if not apply: return 0 if entry_ok(read_private(str(p))[0]) else 1 old={}; identity=None if os.path.lexists(p): raw,identity=read_private(str(p)) try: old=json.loads(raw.decode()) except Exception: old={} mcp=old.get("mcpServers") if isinstance(old.get("mcpServers"),dict) else {} mcp["sequential-thinking"]={"command":"npx","args":["-y","@modelcontextprotocol/server-sequential-thinking"]}; old["mcpServers"]=mcp fd,tmp=tempfile.mkstemp(prefix=".claude.json.",dir=root) try: os.fchmod(fd,0o600); os.write(fd,(json.dumps(old,indent=2)+"\n").encode()); os.fsync(fd); os.close(fd) try: now=os.lstat(p); current=(now.st_dev,now.st_ino) except FileNotFoundError: current=None if current!=identity: die() os.replace(tmp,p) finally: try: os.close(fd) except OSError: pass try: os.unlink(tmp) except FileNotFoundError: pass return 0 if os.environ.get("CLAUDE_CONFIG_DIR"): raise SystemExit(explicit_check_or_apply(os.environ.get("SEQ_APPLY")=="1")) # Compatibility path is intentionally not fleet-authoritative. p=Path.home()/".claude.json" if not p.exists() and not os.environ.get("SEQ_APPLY")=="1": p=Path.home()/".claude"/"settings.json" if os.environ.get("SEQ_APPLY")=="1": try: d=json.loads(p.read_text()) if p.exists() else {} except Exception: d={} m=d.get("mcpServers") if isinstance(d.get("mcpServers"),dict) else {} m["sequential-thinking"]={"command":"npx","args":["-y","@modelcontextprotocol/server-sequential-thinking"]}; d["mcpServers"]=m p.parent.mkdir(parents=True,exist_ok=True); p.write_text(json.dumps(d,indent=2)+"\n") raise SystemExit(0) try: raise SystemExit(0 if entry_ok(p.read_bytes()) else 1) except Exception: raise SystemExit(1)' check_claude_config() { CLAUDE_CONFIG_DIR="$CLAUDE_CONFIG_DIR" SEQ_APPLY=0 "$PYTHON_BIN" -c "$claude_config_python"; } apply_claude_config() { CLAUDE_CONFIG_DIR="$CLAUDE_CONFIG_DIR" SEQ_APPLY=1 "$PYTHON_BIN" -c "$claude_config_python"; } check_codex_config() { CODEX_CFG="${CODEX_HOME:-$HOME/.codex}/config.toml" "$PYTHON_BIN" -c 'import os,re; from pathlib import Path; s=Path(os.environ["CODEX_CFG"]).read_text(); ok=bool(re.search(r"^\[mcp_servers\.(sequential-thinking|sequential_thinking)\]",s,re.M) and "command = \"npx\"" in s and "@modelcontextprotocol/server-sequential-thinking" in s); raise SystemExit(0 if ok else 1)'; } apply_codex_config() { CODEX_CFG="${CODEX_HOME:-$HOME/.codex}/config.toml" "$PYTHON_BIN" -c 'import os,re; from pathlib import Path; p=Path(os.environ["CODEX_CFG"]); p.parent.mkdir(parents=True,exist_ok=True); out=[]; skip=False for line in (p.read_text().splitlines() if p.exists() else []): if re.match(r"^\[mcp_servers\.(sequential-thinking|sequential_thinking)\]$",line): skip=True; continue if skip and line.startswith("["): skip=False if not skip: out.append(line) p.write_text("\n".join(out).rstrip()+"\n\n[mcp_servers.sequential-thinking]\ncommand = \"npx\"\nargs = [\"-y\", \"@modelcontextprotocol/server-sequential-thinking\"]\n")'; } check_opencode_config() { XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-}" "$PYTHON_BIN" -c 'import json,os; from pathlib import Path; p=Path(os.environ["XDG_CONFIG_HOME"])/"opencode/config.json" if os.environ.get("XDG_CONFIG_HOME") else Path.home()/".config/opencode/config.json"; d=json.loads(p.read_text()); e=d.get("mcp",{}).get("sequential-thinking"); expected={"type":"local","command":["npx","-y","@modelcontextprotocol/server-sequential-thinking"],"enabled":True}; raise SystemExit(0 if e==expected else 1)' ; } apply_opencode_config() { XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-}" "$PYTHON_BIN" -c 'import json,os; from pathlib import Path; p=Path(os.environ["XDG_CONFIG_HOME"])/"opencode/config.json" if os.environ.get("XDG_CONFIG_HOME") else Path.home()/".config/opencode/config.json"; p.parent.mkdir(parents=True,exist_ok=True); d=json.loads(p.read_text()) if p.exists() else {}; m=d.get("mcp") if isinstance(d.get("mcp"),dict) else {}; m["sequential-thinking"]={"type":"local","command":["npx","-y","@modelcontextprotocol/server-sequential-thinking"],"enabled":True}; d["mcp"]=m; p.write_text(json.dumps(d,indent=2)+"\n")'; } check_runtime_config() { case "$RUNTIME" in all) check_claude_config && check_codex_config && check_opencode_config;; claude) check_claude_config;; codex) check_codex_config;; opencode) check_opencode_config;; esac; } apply_runtime_config() { case "$RUNTIME" in claude) apply_claude_config;; codex) apply_codex_config;; opencode) apply_opencode_config;; all) apply_claude_config && apply_codex_config && apply_opencode_config;; esac; } if [[ "$MODE" == check ]]; then check_runtime_config if [[ "$STRICT_CHECK" == 1 || "${MOSAIC_SEQ_CHECK_WARM:-0}" == 1 ]]; then warm_package || { err "sequential-thinking package warm-up failed in strict mode"; exit 1; }; fi log "sequential-thinking MCP is configured and available (${RUNTIME})"; exit 0 fi warm_package || { err "sequential-thinking package warm-up failed"; exit 1; } apply_runtime_config log "sequential-thinking MCP configured (${RUNTIME})"