from __future__ import annotations from .auth import connect_with_personal_key, normalize_api_base_url from .common import ( DEFAULT_ENV_NAME, ENV_NAME_PATTERN, SERVER_NAME, Any, Callable, InstallerError, Path, Runner, Which, _project_root, shutil, subprocess, sys, ) from .diagnostics import doctor_project from .project import inspect_project, prepare_project, render_client_config def install_project( project_root: str | Path | None, api_base_url: str, *, env_name: str = DEFAULT_ENV_NAME, enable_writes: bool = False, runner: Runner = subprocess.run, which: Which = shutil.which, python_version: tuple[int, int, int] | None = None, stdin_isatty: Callable[[], bool] = sys.stdin.isatty, ) -> dict[str, Any]: """Prepare, authenticate, diagnose, and describe one local installation. The returned client configuration is intentionally not written here: the installation Agent must merge it into its own client-specific format after showing the destination and creating a recoverable backup. """ root = _project_root(project_root) normalized_url = normalize_api_base_url(api_base_url) if not ENV_NAME_PATTERN.fullmatch(env_name): raise InstallerError( "Environment name must start with a letter or digit and contain only letters, digits, dot, underscore, or dash" ) if not stdin_isatty(): raise InstallerError( "Secure API Key entry requires a user-controlled local terminal. " "Do not paste the Key into Agent chat or pass it with --access-token." ) inspection = inspect_project(root, which=which, python_version=python_version) failed_prerequisites = [check["detail"] for check in inspection["checks"] if check["required"] and not check["ok"]] if failed_prerequisites: raise InstallerError("Installation prerequisites are not ready: " + "; ".join(failed_prerequisites)) prepared = prepare_project( root, runner=runner, which=which, python_version=python_version, ) authentication = connect_with_personal_key( root, normalized_url, env_name=env_name, runner=runner, which=which, stdin_isatty=stdin_isatty, ) diagnosis = doctor_project( root, runner=runner, which=which, python_version=python_version, ) client_config = render_client_config(root, enable_writes=enable_writes, which=which) local_ready = bool(diagnosis["ok"]) return { "ok": local_ready, "status": "ready-for-client-registration" if local_ready else "local-checks-failed", "projectRoot": str(root), "mode": "project-writes" if enable_writes else "read-only", "prepared": { "environmentPath": prepared["environmentPath"], "dependencyProfile": prepared["dependencies"], "configCreated": prepared["configCreated"], }, "authentication": authentication, "checks": diagnosis["checks"], "clientConfig": client_config, "clientRegistration": { "required": True, "serverName": SERVER_NAME, "backupRequired": True, "action": "merge the generated server entry, reload the client, then call crm_health", }, "remainingActions": [ "Merge clientConfig into the current MCP client without replacing other servers.", "Reload or restart the MCP client.", "Call crm_health and require ok=true before reporting installation complete.", ], } __all__ = ["install_project"]