#!/usr/bin/env python3 """Deterministic GitHub MCP installer used by the MCPHub admin server. The installer deliberately does not invoke a shell. It accepts one GitHub repository, resolves the requested ref to an immutable commit, builds either a Python or Node runtime in versioned MCPHub appdata and returns a stdio server configuration. Publishing and MCPHub API updates remain the responsibility of the small admin MCP wrapper. """ from __future__ import annotations import hashlib import json import os import pathlib import re import shutil import subprocess import tempfile from dataclasses import dataclass from typing import Any from urllib.parse import urlparse NAME_RE = re.compile(r"^[a-z0-9][a-z0-9-]{0,62}$") REF_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._/+~-]{0,199}$") ENV_RE = re.compile(r"^[A-Z_][A-Z0-9_]{0,127}$") EXECUTABLE_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._+-]{0,127}$") MAX_ARGS = 20 MAX_REQUIRED_ENV = 40 COMMAND_TIMEOUT = 900 class GitInstallError(RuntimeError): """A bounded, user-facing installation error.""" @dataclass(frozen=True) class GitInstallSpec: name: str repository: str ref: str runtime: str entrypoint: str subdirectory: str = "." arguments: tuple[str, ...] = () required_env: tuple[str, ...] = () run_build: bool = True def _bounded(text: object, limit: int = 600) -> str: value = str(text).replace("\x00", "").strip() return value if len(value) <= limit else value[:limit] + "..." def _run(argv: list[str], cwd: pathlib.Path | None = None, timeout: int = COMMAND_TIMEOUT) -> str: try: result = subprocess.run( argv, cwd=cwd, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, timeout=timeout, check=False, env={**os.environ, "GIT_TERMINAL_PROMPT": "0"}, ) except (OSError, subprocess.TimeoutExpired) as exc: raise GitInstallError(f"Command failed to start or timed out: {_bounded(exc)}") from exc if result.returncode != 0: command = pathlib.Path(argv[0]).name raise GitInstallError(f"{command} exited with {result.returncode}: {_bounded(result.stdout)}") return result.stdout def validate_repository(value: str) -> str: value = value.strip().removesuffix("/") parsed = urlparse(value) if parsed.scheme != "https" or parsed.hostname not in {"github.com", "www.github.com"}: raise GitInstallError("Repository must be an HTTPS GitHub URL") if parsed.username or parsed.password or parsed.query or parsed.fragment: raise GitInstallError("Repository URL must not contain credentials, query parameters or fragments") parts = [item for item in parsed.path.removesuffix(".git").split("/") if item] if len(parts) != 2 or any(not re.fullmatch(r"[A-Za-z0-9._-]+", item) for item in parts): raise GitInstallError("Repository must have the form https://github.com/OWNER/REPOSITORY") return f"https://github.com/{parts[0]}/{parts[1]}.git" def validate_relative_path(value: str) -> str: value = value.strip() or "." path = pathlib.PurePosixPath(value) if path.is_absolute() or ".." in path.parts: raise GitInstallError("Subdirectory and entrypoint paths must stay inside the repository") return path.as_posix() def normalize_spec(spec: GitInstallSpec) -> GitInstallSpec: name = spec.name.strip().lower() if not NAME_RE.fullmatch(name): raise GitInstallError("Name must be lowercase letters, numbers or hyphens") repository = validate_repository(spec.repository) ref = spec.ref.strip() if not REF_RE.fullmatch(ref) or ref.startswith("-"): raise GitInstallError("Invalid Git ref") runtime = spec.runtime.strip().lower() if runtime not in {"python", "node"}: raise GitInstallError("Runtime must be python or node") subdirectory = validate_relative_path(spec.subdirectory) entrypoint = spec.entrypoint.strip() if runtime == "python": if not EXECUTABLE_RE.fullmatch(entrypoint): raise GitInstallError("Python entrypoint must be the installed console-script name") else: if entrypoint.startswith("bin:"): if not EXECUTABLE_RE.fullmatch(entrypoint[4:]): raise GitInstallError("Invalid Node package bin name") else: entrypoint = validate_relative_path(entrypoint) arguments = tuple(str(item) for item in spec.arguments) if len(arguments) > MAX_ARGS or any(len(item) > 300 or "\x00" in item for item in arguments): raise GitInstallError(f"At most {MAX_ARGS} bounded arguments are allowed") required_env = tuple(dict.fromkeys(str(item).strip() for item in spec.required_env)) if len(required_env) > MAX_REQUIRED_ENV or any(not ENV_RE.fullmatch(item) for item in required_env): raise GitInstallError("Required environment names must use uppercase ENV_STYLE names") return GitInstallSpec( name=name, repository=repository, ref=ref, runtime=runtime, entrypoint=entrypoint, subdirectory=subdirectory, arguments=arguments, required_env=required_env, run_build=bool(spec.run_build), ) def _atomic_json(path: pathlib.Path, value: dict[str, Any]) -> None: path.parent.mkdir(parents=True, exist_ok=True) fd, temporary = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent) try: with os.fdopen(fd, "w", encoding="utf-8") as handle: json.dump(value, handle, indent=2, ensure_ascii=False) handle.write("\n") os.chmod(temporary, 0o600) os.replace(temporary, path) finally: if os.path.exists(temporary): os.unlink(temporary) def env_key_state(path: pathlib.Path, required: tuple[str, ...]) -> tuple[bool, list[str]]: if not required: return True, [] present: set[str] = set() if path.is_file(): for raw in path.read_text(encoding="utf-8", errors="replace").splitlines(): line = raw.strip() if not line or line.startswith("#") or "=" not in line: continue key, value = line.split("=", 1) if value.strip().strip("\"'"): present.add(key.removeprefix("export ").strip()) missing = [item for item in required if item not in present] return not missing, missing def _clone(spec: GitInstallSpec, destination: pathlib.Path) -> str: if destination.exists(): shutil.rmtree(destination) destination.parent.mkdir(parents=True, exist_ok=True) _run(["git", "-c", "core.hooksPath=/dev/null", "init", str(destination)]) _run(["git", "-C", str(destination), "remote", "add", "origin", spec.repository]) _run([ "git", "-c", "core.hooksPath=/dev/null", "-C", str(destination), "fetch", "--depth", "1", "origin", spec.ref, ]) _run(["git", "-c", "core.hooksPath=/dev/null", "-C", str(destination), "checkout", "--detach", "FETCH_HEAD"]) commit = _run(["git", "-C", str(destination), "rev-parse", "HEAD"]).strip() if not re.fullmatch(r"[0-9a-f]{40}", commit): raise GitInstallError("Git returned an invalid commit id") return commit def _source_root(checkout: pathlib.Path, subdirectory: str) -> pathlib.Path: root = (checkout / subdirectory).resolve() if checkout.resolve() != root and checkout.resolve() not in root.parents: raise GitInstallError("Subdirectory escaped the checkout") if not root.is_dir(): raise GitInstallError(f"Repository subdirectory does not exist: {subdirectory}") return root def _python_release(source: pathlib.Path, release: pathlib.Path, entrypoint: str) -> list[str]: if not ((source / "pyproject.toml").is_file() or (source / "setup.py").is_file()): raise GitInstallError("Python project has neither pyproject.toml nor setup.py") release.mkdir(parents=True, exist_ok=False) venv = release / ".venv" try: _run(["uv", "venv", "--python", "python3", str(venv)]) _run(["uv", "pip", "install", "--python", str(venv / "bin/python"), str(source)]) executable = venv / "bin" / entrypoint if not executable.is_file(): raise GitInstallError(f"Installed Python console script does not exist: {entrypoint}") executable.chmod(executable.stat().st_mode | 0o100) return [str(executable)] except BaseException: shutil.rmtree(release, ignore_errors=True) raise def _node_release(source: pathlib.Path, release: pathlib.Path, entrypoint: str, run_build: bool) -> list[str]: package_file = source / "package.json" if not package_file.is_file(): raise GitInstallError("Node project has no package.json") try: package = json.loads(package_file.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError) as exc: raise GitInstallError("Node package.json is invalid") from exc app = release / "app" release.mkdir(parents=True, exist_ok=False) try: shutil.copytree(source, app, dirs_exist_ok=True, ignore=shutil.ignore_patterns(".git", "node_modules")) install = ["npm", "ci"] if (app / "package-lock.json").is_file() else ["npm", "install"] _run(install, cwd=app) scripts = package.get("scripts") if isinstance(package.get("scripts"), dict) else {} if run_build and "build" in scripts: _run(["npm", "run", "build"], cwd=app) if entrypoint.startswith("bin:"): bin_value = package.get("bin") bin_name = entrypoint[4:] if isinstance(bin_value, str): bin_path = bin_value elif isinstance(bin_value, dict) and isinstance(bin_value.get(bin_name), str): bin_path = bin_value[bin_name] else: raise GitInstallError(f"Node package does not declare bin: {bin_name}") executable = (app / bin_path).resolve() if app.resolve() not in executable.parents or not executable.is_file(): raise GitInstallError(f"Built Node package bin does not exist: {bin_name}") return ["node", str(executable)] executable = (app / entrypoint).resolve() if app.resolve() not in executable.parents or not executable.is_file(): raise GitInstallError(f"Node entrypoint does not exist: {entrypoint}") return ["node", str(executable)] except BaseException: shutil.rmtree(release, ignore_errors=True) raise def prepare_release(spec: GitInstallSpec, appdata: pathlib.Path, secrets: pathlib.Path) -> dict[str, Any]: spec = normalize_spec(spec) work = appdata / "work" / spec.name checkout = work / "source" commit = _clone(spec, checkout) release_id = commit[:12] extension = appdata / "extensions" / spec.name release = extension / "releases" / release_id source = _source_root(checkout, spec.subdirectory) if release.exists(): state_file = extension / "install-state.json" if not state_file.is_file(): raise GitInstallError("Existing release has no installation state; refusing to guess") state = json.loads(state_file.read_text(encoding="utf-8")) release_info = (state.get("releases") or {}).get(release_id) expected = { "commit": commit, "repository": spec.repository, "runtime": spec.runtime, "entrypoint": spec.entrypoint, "subdirectory": spec.subdirectory, "arguments": list(spec.arguments), "required_env": list(spec.required_env), "run_build": spec.run_build, } if not isinstance(release_info, dict) or any( release_info.get(key) != value for key, value in expected.items() ): raise GitInstallError("Existing release metadata does not match the resolved commit") command = list(release_info["command"]) elif spec.runtime == "python": command = _python_release(source, release, spec.entrypoint) command.extend(spec.arguments) else: command = _node_release(source, release, spec.entrypoint, spec.run_build) command.extend(spec.arguments) secret_file = secrets / f"{spec.name}.env" credentials_ready, missing = env_key_state(secret_file, spec.required_env) if spec.required_env: hub_command = "/usr/local/bin/run-with-env" hub_args = [f"/run/secrets/mcphub/{spec.name}.env", "--", *command] else: hub_command, *hub_args = command config = { "type": "stdio", "command": hub_command, "args": hub_args, "enabled": False, "owner": "admin", } state_file = extension / "install-state.json" previous_state: dict[str, Any] = {} if state_file.is_file(): try: previous_state = json.loads(state_file.read_text(encoding="utf-8")) except json.JSONDecodeError as exc: raise GitInstallError("Existing installation state is invalid") from exc old_current = previous_state.get("current_release") releases = previous_state.get("releases") if isinstance(previous_state.get("releases"), dict) else {} releases[release_id] = { "commit": commit, "repository": spec.repository, "ref": spec.ref, "runtime": spec.runtime, "entrypoint": spec.entrypoint, "subdirectory": spec.subdirectory, "arguments": list(spec.arguments), "required_env": list(spec.required_env), "run_build": spec.run_build, "command": command, "config": config, } state = { "version": 1, "current_release": release_id, "previous_release": old_current if old_current and old_current != release_id else previous_state.get("previous_release"), "releases": releases, } _atomic_json(state_file, state) return { "name": spec.name, "repository": spec.repository, "requested_ref": spec.ref, "commit": commit, "release": release_id, "previous_release": state.get("previous_release"), "runtime": spec.runtime, "config": config, "required_env": list(spec.required_env), "secret_file": f"{spec.name}.env" if spec.required_env else None, "credentials_ready": credentials_ready, "missing_env": missing, } def rollback_release(name: str, appdata: pathlib.Path) -> dict[str, Any]: name = name.strip().lower() if not NAME_RE.fullmatch(name): raise GitInstallError("Invalid server name") state_file = appdata / "extensions" / name / "install-state.json" if not state_file.is_file(): raise GitInstallError(f"No managed Git installation exists: {name}") try: state = json.loads(state_file.read_text(encoding="utf-8")) except json.JSONDecodeError as exc: raise GitInstallError("Installation state is invalid") from exc current = state.get("current_release") previous = state.get("previous_release") releases = state.get("releases") if isinstance(state.get("releases"), dict) else {} release = releases.get(previous) if not previous or not isinstance(release, dict): raise GitInstallError("No previous release is available") state["current_release"] = previous state["previous_release"] = current _atomic_json(state_file, state) return { "name": name, "release": previous, "previous_release": current, "commit": release.get("commit"), "config": release.get("config"), } def registry_entry(result: dict[str, Any], description: str, clients: list[str] | None = None) -> dict[str, Any]: name = str(result["name"]) config = json.loads(json.dumps(result["config"])) return { "id": f"{name}-local", "hermes_id": name, "name": name, "description": _bounded(description, 300), "url": f"http://192.168.1.2:8787/mcp/{name}", "clients": list(clients or []), "deployment": { "kind": "git", "repository": result["repository"], "ref": result["requested_ref"], "commit": result["commit"], "release": result["release"], "required_env": result["required_env"], "secret_file": result["secret_file"], }, "hub": config, } def update_registry(path: pathlib.Path, entry: dict[str, Any]) -> None: if path.is_file(): document = json.loads(path.read_text(encoding="utf-8")) else: document = {"version": 1, "servers": []} if document.get("version") != 1 or not isinstance(document.get("servers"), list): raise GitInstallError("Unsupported MCP registry schema") server_id = entry["id"] hermes_id = entry["hermes_id"] document["servers"] = [ item for item in document["servers"] if not isinstance(item, dict) or (item.get("id") != server_id and item.get("hermes_id") != hermes_id) ] document["servers"].append(entry) _atomic_json(path, document) def state_summary(name: str, appdata: pathlib.Path, secrets: pathlib.Path) -> dict[str, Any]: name = name.strip().lower() if not NAME_RE.fullmatch(name): raise GitInstallError("Invalid server name") state_file = appdata / "extensions" / name / "install-state.json" if not state_file.is_file(): return {"name": name, "managed": False} state = json.loads(state_file.read_text(encoding="utf-8")) current = state.get("current_release") release = (state.get("releases") or {}).get(current, {}) required = tuple(release.get("required_env") or ()) ready, missing = env_key_state(secrets / f"{name}.env", required) return { "name": name, "managed": True, "current_release": current, "previous_release": state.get("previous_release"), "commit": release.get("commit"), "repository": release.get("repository"), "runtime": release.get("runtime"), "credentials_ready": ready, "missing_env": missing, } def current_release(name: str, appdata: pathlib.Path, secrets: pathlib.Path) -> dict[str, Any]: """Return the active managed release, its disabled config and credential state.""" name = name.strip().lower() if not NAME_RE.fullmatch(name): raise GitInstallError("Invalid server name") state_file = appdata / "extensions" / name / "install-state.json" if not state_file.is_file(): raise GitInstallError(f"No managed Git installation exists: {name}") try: state = json.loads(state_file.read_text(encoding="utf-8")) except json.JSONDecodeError as exc: raise GitInstallError("Installation state is invalid") from exc release_id = state.get("current_release") release = (state.get("releases") or {}).get(release_id) if not isinstance(release, dict) or not isinstance(release.get("config"), dict): raise GitInstallError("Current managed release metadata is incomplete") required = tuple(release.get("required_env") or ()) ready, missing = env_key_state(secrets / f"{name}.env", required) config = json.loads(json.dumps(release["config"])) config["enabled"] = False return { "name": name, "repository": release.get("repository"), "requested_ref": release.get("ref"), "commit": release.get("commit"), "release": release_id, "runtime": release.get("runtime"), "config": config, "required_env": list(required), "secret_file": f"{name}.env" if required else None, "credentials_ready": ready, "missing_env": missing, }