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.
- dcc_mcp_runtime/__init__.py +17 -0
- dcc_mcp_runtime/bootstrap.py +38 -0
- dcc_mcp_runtime/cli.py +25 -0
- dcc_mcp_runtime/handshake.py +55 -0
- dcc_mcp_runtime/lifecycle.py +51 -0
- dcc_mcp_runtime/manifest.py +111 -0
- dcc_mcp_runtime-0.2.0.dist-info/METADATA +52 -0
- dcc_mcp_runtime-0.2.0.dist-info/RECORD +11 -0
- dcc_mcp_runtime-0.2.0.dist-info/WHEEL +4 -0
- dcc_mcp_runtime-0.2.0.dist-info/entry_points.txt +2 -0
- dcc_mcp_runtime-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -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,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.
|