dcc-mcp-runtime 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,17 @@
1
+ """Shared runtime contracts for external DCC-MCP adapters."""
2
+
3
+ from .handshake import CapabilityHandshake, HandshakeResult, negotiate
4
+ from .lifecycle import InstallPlan, InstallResult, plan_install
5
+ from .manifest import AdapterManifest, RuntimeManifest, load_manifest
6
+
7
+ __all__ = [
8
+ "AdapterManifest",
9
+ "CapabilityHandshake",
10
+ "HandshakeResult",
11
+ "InstallPlan",
12
+ "InstallResult",
13
+ "RuntimeManifest",
14
+ "load_manifest",
15
+ "negotiate",
16
+ "plan_install",
17
+ ]
@@ -0,0 +1,38 @@
1
+ """Compatibility bootstrap used by external adapter entry points.
2
+
3
+ It performs only manifest loading and negotiation. Process spawning, host UI
4
+ control and installer execution remain owned by each adapter/core.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ from pathlib import Path
11
+
12
+ from .handshake import HandshakeResult, negotiate
13
+ from .manifest import AdapterManifest, RuntimeManifest, load_manifest
14
+
15
+
16
+ def runtime_root() -> Path:
17
+ """Resolve the side-by-side runtime root without consulting the shell."""
18
+ configured = os.environ.get("DCC_MCP_RUNTIME_ROOT")
19
+ if configured:
20
+ return Path(configured).resolve()
21
+ # Installed package: <root>/lib/site-packages/dcc_mcp_runtime.
22
+ return Path(__file__).resolve().parents[3]
23
+
24
+
25
+ def negotiate_adapter(adapter_id: str) -> HandshakeResult:
26
+ root = runtime_root()
27
+ runtime = load_manifest(root / "runtime" / "manifest.json")
28
+ adapter = load_manifest(root / "runtime" / "manifests" / f"{adapter_id}.json")
29
+ if not isinstance(runtime, RuntimeManifest) or not isinstance(adapter, AdapterManifest):
30
+ raise ValueError("runtime or adapter manifest has an invalid shape")
31
+ return negotiate(runtime, adapter)
32
+
33
+
34
+ def require_adapter(adapter_id: str) -> HandshakeResult:
35
+ result = negotiate_adapter(adapter_id)
36
+ if not result.accepted:
37
+ raise RuntimeError(f"DCC_MCP_RUNTIME_HANDSHAKE_FAILED:{result.reason}")
38
+ return result
dcc_mcp_runtime/cli.py ADDED
@@ -0,0 +1,25 @@
1
+ """Small validation CLI used by installers and CI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+
8
+ from .manifest import load_manifest
9
+
10
+
11
+ def main() -> int:
12
+ parser = argparse.ArgumentParser(prog="dcc-mcp-runtime")
13
+ sub = parser.add_subparsers(dest="command", required=True)
14
+ validate = sub.add_parser("validate-manifest")
15
+ validate.add_argument("path")
16
+ args = parser.parse_args()
17
+ if args.command == "validate-manifest":
18
+ manifest = load_manifest(args.path)
19
+ print(json.dumps(manifest.to_dict(), ensure_ascii=False, indent=2))
20
+ return 0
21
+ return 2
22
+
23
+
24
+ if __name__ == "__main__": # pragma: no cover - exercised by the CLI smoke path
25
+ raise SystemExit(main())
@@ -0,0 +1,55 @@
1
+ """Runtime/adapter capability negotiation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from .manifest import AdapterManifest, RuntimeManifest
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class CapabilityHandshake:
12
+ runtime_id: str
13
+ runtime_version: str
14
+ python_abi: str
15
+ dcc_mcp_core: str
16
+ adapter_id: str
17
+ adapter_version: str
18
+ capabilities: tuple[str, ...]
19
+ capabilities_fingerprint: str
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class HandshakeResult:
24
+ accepted: bool
25
+ reason: str
26
+ handshake: CapabilityHandshake | None = None
27
+
28
+
29
+ def negotiate(runtime: RuntimeManifest, adapter: AdapterManifest) -> HandshakeResult:
30
+ if adapter.host_mode == "embedded":
31
+ return HandshakeResult(False, "embedded_adapter_requires_host_runtime")
32
+ if runtime.python_abi != adapter.python_abi:
33
+ return HandshakeResult(False, "python_abi_mismatch")
34
+ if runtime.dcc_mcp_core != adapter.dcc_mcp_core:
35
+ return HandshakeResult(False, "dcc_mcp_core_mismatch")
36
+ capabilities = tuple(sorted(set(adapter.capabilities)))
37
+ # The runtime does not sign/authorise capabilities; it carries a stable
38
+ # handshake value for the gateway to compare with its catalog.
39
+ import hashlib
40
+
41
+ fingerprint = hashlib.sha256("\n".join(capabilities).encode()).hexdigest()
42
+ return HandshakeResult(
43
+ True,
44
+ "accepted",
45
+ CapabilityHandshake(
46
+ runtime.runtime_id,
47
+ runtime.version,
48
+ runtime.python_abi,
49
+ runtime.dcc_mcp_core,
50
+ adapter.adapter_id,
51
+ adapter.version,
52
+ capabilities,
53
+ fingerprint,
54
+ ),
55
+ )
@@ -0,0 +1,51 @@
1
+ """Consent-gated, verifiable and rollback-safe install planning."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class InstallPlan:
10
+ operation_id: str
11
+ runtime_id: str
12
+ target_version: str
13
+ package_path: str
14
+ expected_sha256: str
15
+ previous_version: str | None
16
+ requires_confirmation: bool = True
17
+ rollback_version: str | None = None
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class InstallResult:
22
+ operation_id: str
23
+ status: str
24
+ active_version: str | None
25
+ reason: str
26
+
27
+
28
+ def plan_install(
29
+ *,
30
+ operation_id: str,
31
+ runtime_id: str,
32
+ target_version: str,
33
+ package_path: str,
34
+ expected_sha256: str,
35
+ previous_version: str | None = None,
36
+ ) -> InstallPlan:
37
+ """Return an exact plan; execution belongs to a host-owned allow-list."""
38
+ if not operation_id or not runtime_id or not target_version or not package_path:
39
+ raise ValueError("operation_id, runtime_id, target_version and package_path are required")
40
+ if len(expected_sha256) != 64:
41
+ raise ValueError("expected_sha256 must be a 64-character SHA-256 hex digest")
42
+ return InstallPlan(
43
+ operation_id,
44
+ runtime_id,
45
+ target_version,
46
+ package_path,
47
+ expected_sha256,
48
+ previous_version,
49
+ True,
50
+ previous_version,
51
+ )
@@ -0,0 +1,111 @@
1
+ """Strict, serialisable runtime and adapter manifests.
2
+
3
+ The manifest is deliberately data-only so it can be verified before importing
4
+ any adapter code. Hash/signature fields are opaque strings; signature checking
5
+ is delegated to the operator-owned trust service.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from dataclasses import dataclass, field
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ SCHEMA_VERSION = "1"
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class AdapterManifest:
20
+ adapter_id: str
21
+ version: str
22
+ dcc_mcp_core: str
23
+ python_abi: str
24
+ capabilities: tuple[str, ...]
25
+ wheel_sha256: str
26
+ signature: str | None = None
27
+ host_mode: str = "external"
28
+
29
+ def __post_init__(self) -> None:
30
+ if not self.adapter_id or not self.version or not self.python_abi:
31
+ raise ValueError("adapter_id, version and python_abi are required")
32
+ if self.host_mode not in {"external", "embedded"}:
33
+ raise ValueError("host_mode must be external or embedded")
34
+ if len(self.wheel_sha256) != 64:
35
+ raise ValueError("wheel_sha256 must be a 64-character SHA-256 hex digest")
36
+
37
+ @classmethod
38
+ def from_dict(cls, data: dict[str, Any]) -> AdapterManifest:
39
+ return cls(
40
+ adapter_id=str(data["adapter_id"]),
41
+ version=str(data["version"]),
42
+ dcc_mcp_core=str(data["dcc_mcp_core"]),
43
+ python_abi=str(data["python_abi"]),
44
+ capabilities=tuple(str(x) for x in data.get("capabilities", [])),
45
+ wheel_sha256=str(data["wheel_sha256"]),
46
+ signature=data.get("signature"),
47
+ host_mode=str(data.get("host_mode", "external")),
48
+ )
49
+
50
+ def to_dict(self) -> dict[str, Any]:
51
+ return {
52
+ "adapter_id": self.adapter_id,
53
+ "version": self.version,
54
+ "dcc_mcp_core": self.dcc_mcp_core,
55
+ "python_abi": self.python_abi,
56
+ "capabilities": list(self.capabilities),
57
+ "wheel_sha256": self.wheel_sha256,
58
+ "signature": self.signature,
59
+ "host_mode": self.host_mode,
60
+ }
61
+
62
+
63
+ @dataclass(frozen=True)
64
+ class RuntimeManifest:
65
+ runtime_id: str
66
+ version: str
67
+ python_version: str
68
+ python_abi: str
69
+ platform: str
70
+ dcc_mcp_core: str
71
+ adapters: tuple[AdapterManifest, ...] = field(default_factory=tuple)
72
+ runtime_sha256: str | None = None
73
+ signature: str | None = None
74
+ schema_version: str = SCHEMA_VERSION
75
+
76
+ def __post_init__(self) -> None:
77
+ if self.schema_version != SCHEMA_VERSION:
78
+ raise ValueError(f"unsupported manifest schema: {self.schema_version}")
79
+
80
+ def to_dict(self) -> dict[str, Any]:
81
+ return {
82
+ "schema_version": self.schema_version,
83
+ "runtime_id": self.runtime_id,
84
+ "version": self.version,
85
+ "python_version": self.python_version,
86
+ "python_abi": self.python_abi,
87
+ "platform": self.platform,
88
+ "dcc_mcp_core": self.dcc_mcp_core,
89
+ "runtime_sha256": self.runtime_sha256,
90
+ "signature": self.signature,
91
+ "adapters": [a.to_dict() for a in self.adapters],
92
+ }
93
+
94
+
95
+ def load_manifest(path: str | Path) -> RuntimeManifest | AdapterManifest:
96
+ """Load and validate either manifest shape without importing adapters."""
97
+ data = json.loads(Path(path).read_text(encoding="utf-8"))
98
+ if "runtime_id" in data:
99
+ return RuntimeManifest(
100
+ runtime_id=str(data["runtime_id"]),
101
+ version=str(data["version"]),
102
+ python_version=str(data["python_version"]),
103
+ python_abi=str(data["python_abi"]),
104
+ platform=str(data["platform"]),
105
+ dcc_mcp_core=str(data["dcc_mcp_core"]),
106
+ adapters=tuple(AdapterManifest.from_dict(x) for x in data.get("adapters", [])),
107
+ runtime_sha256=data.get("runtime_sha256"),
108
+ signature=data.get("signature"),
109
+ schema_version=str(data.get("schema_version", SCHEMA_VERSION)),
110
+ )
111
+ return AdapterManifest.from_dict(data)
@@ -0,0 +1,52 @@
1
+ Metadata-Version: 2.5
2
+ Name: dcc-mcp-runtime
3
+ Version: 0.2.0
4
+ Summary: Shared, signed runtime contract for external DCC-MCP adapters
5
+ Author-email: loonghao <hal.long@outlook.com>
6
+ License: MIT
7
+ License-File: LICENSE
8
+ Requires-Python: >=3.9
9
+ Provides-Extra: dev
10
+ Requires-Dist: build<2,>=1.2; extra == 'dev'
11
+ Requires-Dist: pytest<9,>=8; extra == 'dev'
12
+ Requires-Dist: ruff<1,>=0.8; extra == 'dev'
13
+ Description-Content-Type: text/markdown
14
+
15
+ # dcc-mcp-runtime
16
+
17
+ Shared Python runtime for **external** DCC-MCP adapters (Adobe, CapCut and OBS).
18
+ The runtime is a versioned, side-by-side install containing CPython, `dcc-mcp-core`
19
+ and adapter wheels under `lib/site-packages`. Development still uses `uv`/wheels;
20
+ PyOxidizer is the reproducible distribution path.
21
+
22
+ Embedded DCC Python (Maya, Blender, Houdini, 3ds Max, etc.) is intentionally not
23
+ injected into this runtime. Those adapters keep their host interpreter and only
24
+ consume the same protocol/manifest contract.
25
+
26
+ ## Quick start
27
+
28
+ ```powershell
29
+ uv sync --extra dev
30
+ uv run pytest
31
+ uv run ruff check .
32
+ python -m build
33
+ ```
34
+
35
+ `runtime/manifest.json` is the machine-readable contract. Validate an adapter
36
+ before loading it:
37
+
38
+ ```powershell
39
+ python -m dcc_mcp_runtime.cli validate-manifest runtime/manifests/capcut.json
40
+ ```
41
+
42
+ Installation and upgrade are represented by a typed plan. The runtime never
43
+ executes arbitrary shell/Python: an operator must approve an exact plan and a
44
+ host-owned installer performs the allow-listed operation. A failed health/hash
45
+ verification leaves the previous runtime active for rollback.
46
+
47
+ Releases use [release-please](https://github.com/googleapis/release-please):
48
+ Conventional Commits on `main` create a release PR; merging it creates the tag
49
+ and release, then the same workflow builds and uploads the wheel, sdist, and
50
+ runtime/adapter manifests to that exact release tag.
51
+
52
+ See [migration guides](docs/migration/) and [ADR 0001](docs/ADR-0001-shared-runtime.md).
@@ -0,0 +1,11 @@
1
+ dcc_mcp_runtime/__init__.py,sha256=8TL9PeGaEv4HKlPvJ4QO_7NabHO8rnweJnz_vwCTtsk,477
2
+ dcc_mcp_runtime/bootstrap.py,sha256=dZ8QBUtCxD14g3IwURAs6I_WyvhSM4MogSZu5JYBQn0,1418
3
+ dcc_mcp_runtime/cli.py,sha256=cHwybIJlQweOwrxL3tIWyudiyE0wuyfVKyfj1X5NH1U,723
4
+ dcc_mcp_runtime/handshake.py,sha256=zk0UbIcjcP9cJZYeKApSbegi83Z7LNt7V-7QzMF9PIw,1642
5
+ dcc_mcp_runtime/lifecycle.py,sha256=2ylj2RYkL-DbfWhDa9eaLk4zfLB0ucMKa1yQthoKjEw,1337
6
+ dcc_mcp_runtime/manifest.py,sha256=A3SfmD9zNUMW6Tazt-b5absNalxIejP0ayTJFMPJzXI,4037
7
+ dcc_mcp_runtime-0.2.0.dist-info/METADATA,sha256=l4UoKURlrYARmrNzvWHbuOzctZxTOhBovZRrSoCjxxQ,1924
8
+ dcc_mcp_runtime-0.2.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
9
+ dcc_mcp_runtime-0.2.0.dist-info/entry_points.txt,sha256=cw8ApKaEmDXDgRf5AgfZPeIWJIEW7bOTGDpdG5V5ac4,61
10
+ dcc_mcp_runtime-0.2.0.dist-info/licenses/LICENSE,sha256=k7pmNfphyrF9W_4TDwL3TTCuKDARdzrCHmhZT4yVMig,1065
11
+ dcc_mcp_runtime-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ dcc-mcp-runtime = dcc_mcp_runtime.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 loonghao
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.