Adapter Install Lifecycle
Adapter installers often run inside the DCC process they are updating. On Windows, importing dcc_mcp_core._core loads _core.pyd; that native module stays locked until the process exits, so an uninstall or upgrade can fail while removing the adapter's bundled package tree.
Use dcc_mcp_core.deployment for installer and uninstaller code that must stay import-light. The module uses only the Python standard library and does not import _core.
Runtime Vocabulary
Use these terms consistently when documenting or implementing DCC startup:
| Term | Meaning | Process ownership |
|---|---|---|
| DCC startup hook | The adapter code that runs when Maya, Houdini, 3ds Max, or another host opens. It should only prepare environment data and launch the service path. | Runs inside the DCC process and must not block the UI/main thread. |
| Per-DCC service | One lightweight runtime registered for one concrete DCC instance. Python adapters may provide this through DccServerBase; plugin-startup adapters usually launch the sidecar service. | One row per DCC instance in the shared registry. |
| Sidecar | The Rust dcc-mcp-sidecar runtime launched through the stable dcc-mcp-server sidecar CLI. It bridges a real host RPC endpoint to MCP/REST, registers as a per-DCC service, and exits when the watched DCC process dies. | Separate child process owned by one DCC instance. |
| Gateway daemon | The machine-wide dcc-mcp-server gateway process that owns routing, dynamic capability search/describe/call, Gateway Admin, and the singleton gateway listener. | One process per workstation/registry/port. |
| Guardian | A lightweight loop kept by daemon-backed services. It probes gateway /health and re-runs the single-flight ensure path when the daemon disappears. | Not a separate process; it belongs to a live service/sidecar/backend. |
| Service heartbeat | The registry refresh that keeps a service row fresh while the owning DCC/service is alive. | Updates registration state only. It is not the gateway restart trigger. |
The ideal plugin startup flow is:
open DCC
-> startup hook launches the per-DCC service/sidecar without blocking
-> the service ensures the machine-wide gateway daemon exists
-> the service registers one instance row and keeps heartbeat fresh
-> the gateway daemon reads all rows and routes across all live instances
-> any surviving daemon-backed service guardian can re-ensure the gatewayIn other words, heartbeat keeps instance registration fresh; guardian recovery restarts the gateway daemon after /health disappears. Do not describe gateway recovery as "the next heartbeat restarts the gateway".
Capture Startup And Host Console Errors
Wrap the full adapter import/start block so failures that happen before the MCP server exists still reach the shared log directory:
from dcc_mcp_core import capture_bootstrap_errors
with capture_bootstrap_errors(
"3dsmax",
adapter_version="0.3.0",
min_core_version="0.19.0",
):
from dcc_mcp_3dsmax.max_bootstrap import main
main()The helper uses only the standard library, writes a bounded rotating dcc-mcp-<dcc>.<pid>.host-errors.log entry, and re-raises the exception so the DCC's native error dialog remains visible. The local file retains the traceback, loaded core path, Python executable, and version evidence needed to diagnose mixed installs. Gateway Admin exposes only the sanitized error summary, DCC/PID, phase, source, and versions; it does not return those local paths.
DccServerBase automatically captures logging.ERROR, sys.excepthook, threading.excepthook, and sys.unraisablehook while the server is running. It publishes the same error through the existing output://instance/<dcc-pid> and events://session/<dcc-pid> resources. A host-native script editor, console, or SDK callback still belongs to the adapter; forward it through server.report_host_error(message, source="3dsmax.listener"). Do not replace sys.stdout or sys.stderr process-wide.
Rez Deployment Layout
Resolve dcc_mcp_core, dcc_mcp_server, and the adapter package in the active Rez environment before launching the DCC. Rez publishes each resolved root as REZ_<PACKAGE>_ROOT; the bootstrap helper converts those roots into the PYTHONPATH and PATH entries needed by the sidecar or gateway:
from dcc_mcp_core.deployment import resolve_deployment_layout
layout = resolve_deployment_layout(adapter_package="dcc_mcp_maya")
python_paths = layout["environment"]["prepend"]["PYTHONPATH"]
path_entries = layout["environment"]["prepend"]["PATH"]The helper reads REZ_<PACKAGE>_ROOT variables such as REZ_DCC_MCP_CORE_ROOT, REZ_DCC_MCP_SERVER_ROOT, and REZ_DCC_MCP_MAYA_ROOT. Treat the resolved Rez environment as the deployment contract; do not embed workstation-specific package-cache paths in adapter startup scripts.
Import-Light Sidecar Launch
DCC plugins that run at application startup can build or launch the per-DCC sidecar without importing _core or blocking the host process:
from dcc_mcp_core.deployment import launch_sidecar
result = launch_sidecar(
dcc_type="maya",
host_rpc="commandport://127.0.0.1:6000",
display_name="Maya-Anim",
adapter_version="1.2.3",
)For in-process DCC plugins, omit watch_pid; the helper binds the sidecar to the current process id. Pass watch_pid only when an external supervisor is launching the sidecar on behalf of the DCC host. The module CLI commands sidecar-command and launch-sidecar still require explicit --watch-pid because their own process id is not the DCC host.
launch_sidecar() uses subprocess.Popen with stdin detached and captures stdout/stderr into log files under <registry_dir>/logs by default. The returned stdio object includes stdout_path and stderr_path; pass stdio_log_dir=... to choose a supervisor-owned directory, or capture_stdio=False / CLI --no-stdio-log only when another process manager already owns those streams. Adapter supervisors that must terminate the child later may pass return_process=True to include the subprocess.Popen handle in the returned dict; CLI/JSON callers must keep the default because that handle is not serializable. The returned server_binary object records the sidecar binary command, configuration source, resolved path when available, and bounded --version output or probe error so startup logs can show which dcc-mcp-server was used. The child runs dcc-mcp-server sidecar, registers a per-dcc-sidecar row in the shared FileRegistry, ensures the machine-wide gateway daemon and keeps a lightweight guardian unless no_ensure_gateway=True or legacy_gateway_election=True, and exits when the bound watch_pid dies. Use build_sidecar_command() instead when the adapter wants to hand the argv list to a studio process supervisor. Both helpers include readiness_selector, readiness_argv, and readiness_command so installers can run the matching import-light readiness check without re-deriving registry paths or host RPC filters. readiness_command uses DCC_MCP_PYTHON_EXECUTABLE when it is set; otherwise prefer readiness_argv if the DCC's sys.executable is a GUI host binary rather than a Python command-line executable.
Both helpers also return a dispatch_contract object describing the host RPC scheme, whether the URI shape is valid, whether it is test-only, and whether it can ever become dispatch-ready. Pass require_dispatch_capable=True (or CLI --require-dispatch-capable) when installer or startup code must fail fast before a malformed or non-callable sidecar is spawned. The default remains permissive so unsupported schemes can still register a diagnostic row for operators.
The Rust implementation of that child lives in the dcc-mcp-sidecar crate. Adapter launch helpers intentionally keep emitting the stable dcc-mcp-server sidecar command so existing installers and release assets do not need a new binary name.
Registration is not the same thing as dispatch readiness. The generic sidecar only becomes callable after its --host-rpc URI resolves to a supported HostRpcClient, that client connects to the DCC, and the sidecar publishes metadata.dispatch_status=ready plus a live metadata.mcp_url in the registry row. Startup failures keep the row visible for operators, mark metadata.dispatch_status=unavailable with failure_stage / failure_reason metadata, and may still publish metadata.mcp_url for structured diagnostics. Treat that URL as non-routable until dispatch_status=ready: its /v1/readyz returns dispatcher false, and tools/call returns the startup failure as a transport-error envelope. Gateway GET /v1/readyz mirrors this as per-instance dispatch plus dispatch-ready counters, so launchers can distinguish a listed DCC process from a callable sidecar dispatcher.
For supported real host RPC schemes such as commandport://, qtserver://, and ws://, the sidecar keeps retrying while the parent DCC process is alive. When the DCC-side bridge eventually accepts a connection, the same listener swaps in the connected dispatcher and the registry row is promoted to dispatch_status=ready. Adapter plugins must still expose a real host RPC bridge to their DCC dispatcher or skills; launch_sidecar() only launches and supervises the sidecar process. stub:// is reserved for tests and placeholder experiments: the sidecar keeps it dispatch_status=unavailable by default, even though the stub transport can "connect", so an adapter must never use it to claim startup readiness. For Maya commandport:// sidecars, a present dcc_mcp_maya package with a missing dcc_mcp_maya.sidecar._dispatcher returns a structured sidecar-dispatcher-unavailable backend envelope on the first call instead of a generic transport error, so installers can distinguish partial adapter installs from gateway routing failures.
from dcc_mcp_core.deployment import build_sidecar_command
from dcc_mcp_core.deployment import wait_for_sidecar_ready
contract = build_sidecar_command(
dcc_type="houdini",
host_rpc="qtserver://127.0.0.1:7001",
instance_id=current_instance_id,
registry_dir=r"C:\dcc-mcp\registry",
require_dispatch_capable=True,
)
command = contract["command"]
env_updates = contract["environment"]["set"]
selector = contract["readiness_selector"]
ready = wait_for_sidecar_ready(
dcc_type=selector["dcc_type"],
instance_id=selector["instance_id"],
host_rpc=selector["host_rpc"],
timeout_secs=5,
probe_tool="houdini_diagnostics__ping",
)When the startup hook is already running on a background thread or inside an installer/supervisor, launch_sidecar() can perform the same bounded check in one call:
result = launch_sidecar(
dcc_type="maya",
host_rpc="commandport://127.0.0.1:6000",
liveness_check_secs=1.0,
wait_ready_timeout_secs=5,
probe_tool="maya_diagnostics__ping",
)
ready = result.get("readiness", {})Leaving wait_ready_timeout_secs unset preserves the non-blocking startup contract. External supervisors that are not running inside the DCC process must pass watch_pid=current_dcc_pid; in-process hooks should keep the default. The sidecar records its own service PID/sentinel separately from this watched host PID. Registry reads require both lifetimes, so the row is evicted even if the sidecar briefly survives after Unreal, 3ds Max, Maya, or another host exits. During rolling upgrades, readers also recognise older per-dcc-sidecar rows whose watched DCC PID predates the explicit host_pid field. Out-of-process DccServerBase adapters that do not use the standard sidecar must pass dcc_pid=current_dcc_pid to DccServerOptions.from_env(...) for the same host-bound contract. Standalone/headless services must leave dcc_pid unset and pass instance_type="standalone"; their own sentinel, endpoint, and heartbeat are the complete lifetime. instance_type describes the process lifetime and is independent from standalone_main_thread, which only selects the execution/threading contract. It can also be supplied with DCC_MCP_<DCC>_INSTANCE_TYPE (or the process-wide DCC_MCP_INSTANCE_TYPE). The Python API keeps liveness_check_secs=0.0 by default for DCC UI startup hooks, but the module CLI defaults launch-sidecar to a 1-second process check because it is normally used from installers and supervisors. Pass --liveness-check-secs 0 only when another supervisor already performs that check. If the sidecar exits during the liveness window, the helper returns success=false, status="exited", reason="sidecar_exited_during_startup", the exit code, and the stdio log paths. This turns "gateway health was OK, but the sidecar died immediately after" into a first-class startup failure. Pass extra_args=[...] only for deliberate sidecar flags not yet modeled by the helper; for CLI values that start with --, use --extra-sidecar-arg=--flag-name.
Use sidecar_readiness_status() for a one-shot verdict (ready, missing, booting, unavailable, ambiguous, or dead) and wait_for_sidecar_ready() from an installer, supervisor, or background startup task when a short bounded poll is acceptable. Do not block a DCC UI or main thread waiting for readiness; launch the sidecar first and surface the verdict through logs or Gateway Admin.
When a startup hook wants to prove one newly-opened DCC instance is directly usable, pass the full instance_id and host_rpc from readiness_selector. If that selector still matches multiple live sidecars, the helpers return status="ambiguous" instead of selecting an arbitrary row. A dcc_type-only check remains useful for aggregate diagnostics such as "is any Maya sidecar ready?", but it is not an instance-level direct-use proof in a multi-DCC session.
When an adapter wants to claim "open DCC, directly usable", pass a cheap read-only diagnostic tool as probe_tool (or CLI --probe-tool). The helper will first require metadata.dispatch_status=ready, then POST one sidecar tools/call with optional probe_arguments / --probe-args-json before returning success. This distinguishes "the generic sidecar listener is alive" from "the adapter dispatcher and at least one skill path can execute". Probe failures are reported as status="probe_failed" and keep polling until the bounded timeout in wait_for_sidecar_ready().
Import-Light Preflight
from dcc_mcp_core.deployment import inspect_install_root
diagnostic = inspect_install_root(r"C:\Users\me\Documents\3dsMax\scripts\dcc_mcp_3dsmax")
if diagnostic["requires_restart"]:
schedule_deferred_cleanup(diagnostic)inspect_install_root() checks modules already loaded in the current process. If a native artifact under the install root is loaded, it returns:
{
"status": "requires_restart",
"requires_restart": true,
"locked_path": "C:\\...\\dcc_mcp_core\\_core.pyd",
"recommended_next_action": "Defer cleanup until the DCC host restarts, then remove or replace the install root."
}Registry Query And Sidecar Stop
Installers can inspect the shared FileRegistry without creating any Rust-backed objects:
from dcc_mcp_core.deployment import query_runtime_state
from dcc_mcp_core.deployment import sidecar_readiness_status
from dcc_mcp_core.deployment import stop_runtime_entries
state = query_runtime_state(dcc_type="3dsmax", role="per-dcc-sidecar")
ready = sidecar_readiness_status(dcc_type="3dsmax")
stop = stop_runtime_entries(dcc_type="3dsmax")For sidecars, each normalized entry exposes dispatch_status, dispatch_ready, host_rpc_uri, host_rpc_scheme, failure_stage, and failure_reason at the top level for compatibility. New installer and startup-hook code can read the same contract from the nested dispatch object (reported, status, ready, ready_at_unix, host_rpc_uri, host_rpc_scheme, failure_stage, and failure_reason). Startup hooks can poll dispatch.ready=True after launch_sidecar() without importing _core. Daemon-backed sidecars and Python DccServerBase adapters also publish gateway_runtime_mode and gateway_guardian_enabled, so operators can confirm whether the row is actually participating in standalone gateway self-recovery. Gateway Admin exposes the same sidecar readiness contract on GET /admin/api/workers as dispatch_status, dispatch_ready, host_rpc_uri, host_rpc_scheme, failure_stage, and failure_reason, so operators can distinguish registered-but-not-callable sidecars from routing failures. It also mirrors gateway_runtime_mode and gateway_guardian_enabled for guardian observability. Gateway instance surfaces (gateway://instances, GET /v1/instances, and /admin/api/instances) also expose a nested dispatch object with reported, status, ready, host-RPC metadata, and failure metadata for the same distinction.
By default, stop_runtime_entries() only targets rows that publish metadata.sidecar_pid. It does not terminate the parent DCC process unless include_host_processes=True is passed explicitly.
Mixed Runtime Version Plan
A gateway can see several DCC runtimes at once. For example, Maya may still be running an old sidecar while 3ds Max has already started a newer one. Treat each registered instance independently and plan restarts from registry metadata:
from dcc_mcp_core.deployment import plan_runtime_updates
from dcc_mcp_core.deployment import query_runtime_state
state = query_runtime_state()
plan = plan_runtime_updates(
state,
target_versions={
"core": "0.17.21",
"server": "0.17.21",
"adapter": "1.2.0",
},
)ServiceEntry.version is the DCC application's version, such as Maya 2026 or Photoshop 25.9; it is not the dcc-mcp-core package version. Runtime rows must publish package versions through metadata keys such as dcc_mcp_core_version, dcc_mcp_server_version, and adapter_version. When package metadata is missing, plan_runtime_updates() reports action=verify_runtime_metadata instead of treating the DCC app version as a package version.
Each plan row reports the component drift and a restart action:
{
"dcc_type": "maya",
"action": "restart_sidecar",
"restart_scope": "sidecar",
"stale_components": ["core", "server", "adapter"],
"recommended_next_action": "Stop the registered sidecar, restart it from the target deployment, then re-run MCP readiness."
}Admin surfaces should render action=restart_sidecar as a safe sidecar restart button when sidecar_pid is present. If a row reports manual_restart_required, the runtime is host-owned and the DCC process must be restarted before reset or MCP calls are expected to use the newer code. If a row reports verify_runtime_metadata, the registry row is missing enough package-version metadata to decide safely; verify or restart that runtime before assuming it is using the target deployment. After any stop or restart, verify readiness with the instance MCP endpoint and refresh gateway registry state before sending reset calls.
The gateway Admin JSON already exposes these operator hints on each instance:
{
"lifecycle": {
"role": "per-dcc-sidecar",
"owner": "release-smoke-test",
"session": "test",
"sidecar_pid": 31337,
"supports_safe_stop": true,
"safe_stop_url": "http://127.0.0.1:19000/safe-stop",
"safe_stop_method": "POST",
"restartable": true,
"restart_command": "rez-env dcc_mcp_maya -- maya-sidecar"
}
}Release smoke tests that launch their own DCC process should publish stable, public lifecycle metadata (owner, session) and, when supported, a safe_stop_url callback. The gateway and dcc-mcp-cli stop-instance only forward safe-stop requests to that explicit callback and never terminate a process directly.
Safe Remove Or Replace
from dcc_mcp_core.deployment import safe_remove_tree
from dcc_mcp_core.deployment import safe_replace_tree
removed = safe_remove_tree(install_root)
replaced = safe_replace_tree(staged_payload, install_root)Both helpers attempt immediate cleanup when preflight is clear. If Windows reports a native-file lock, the result is structured for a deferred startup hook:
{
"status": "requires_restart",
"requires_restart": true,
"locked_path": "C:\\...\\_core.pyd",
"reason": "windows_file_lock",
"deferred_operation": {
"operation": "remove_tree",
"path": "C:\\...\\dcc_mcp_3dsmax"
}
}Use the dedicated console entry point when a DCC-specific installer needs a JSON-only control path. dcc_mcp_core.install_lifecycle and python -m dcc_mcp_core.install_lifecycle remain compatibility aliases.
dcc-mcp-install-lifecycle inspect C:\path\to\adapter
dcc-mcp-install-lifecycle stop --dcc-type 3dsmax
dcc-mcp-install-lifecycle layout --adapter-package dcc_mcp_maya
dcc-mcp-install-lifecycle sidecar-command --dcc maya --host-rpc commandport://127.0.0.1:6000 --watch-pid 12345
dcc-mcp-install-lifecycle launch-sidecar --dcc maya --host-rpc commandport://127.0.0.1:6000 --watch-pid 12345
dcc-mcp-install-lifecycle sidecar-ready --dcc maya --timeout-secs 5
dcc-mcp-install-lifecycle plan-update --target-version core=0.17.21 --target-version server=0.17.21
dcc-mcp-install-lifecycle remove C:\path\to\adapter