dcc-mcp-runtime 0.2.0__tar.gz

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.
Files changed (33) hide show
  1. dcc_mcp_runtime-0.2.0/.github/workflows/ci.yml +41 -0
  2. dcc_mcp_runtime-0.2.0/.github/workflows/release.yml +61 -0
  3. dcc_mcp_runtime-0.2.0/.gitignore +7 -0
  4. dcc_mcp_runtime-0.2.0/.release-please-manifest.json +3 -0
  5. dcc_mcp_runtime-0.2.0/CHANGELOG.md +19 -0
  6. dcc_mcp_runtime-0.2.0/LICENSE +21 -0
  7. dcc_mcp_runtime-0.2.0/PKG-INFO +52 -0
  8. dcc_mcp_runtime-0.2.0/README.md +38 -0
  9. dcc_mcp_runtime-0.2.0/adapters/adobe_runtime_entry.py +18 -0
  10. dcc_mcp_runtime-0.2.0/adapters/aftereffects-runtime-handshake.patch +12 -0
  11. dcc_mcp_runtime-0.2.0/adapters/capcut-runtime-handshake.patch +12 -0
  12. dcc_mcp_runtime-0.2.0/adapters/capcut_runtime_entry.py +14 -0
  13. dcc_mcp_runtime-0.2.0/adapters/obs-runtime-handshake.patch +12 -0
  14. dcc_mcp_runtime-0.2.0/adapters/obs_runtime_entry.py +16 -0
  15. dcc_mcp_runtime-0.2.0/docs/ADR-0001-shared-runtime.md +24 -0
  16. dcc_mcp_runtime-0.2.0/docs/migration/adobe.md +18 -0
  17. dcc_mcp_runtime-0.2.0/docs/migration/capcut.md +11 -0
  18. dcc_mcp_runtime-0.2.0/docs/migration/obs.md +11 -0
  19. dcc_mcp_runtime-0.2.0/pyoxidizer.bzl +39 -0
  20. dcc_mcp_runtime-0.2.0/pyproject.toml +35 -0
  21. dcc_mcp_runtime-0.2.0/release-please-config.json +17 -0
  22. dcc_mcp_runtime-0.2.0/runtime/manifest.json +12 -0
  23. dcc_mcp_runtime-0.2.0/runtime/manifests/adobe.json +11 -0
  24. dcc_mcp_runtime-0.2.0/runtime/manifests/capcut.json +11 -0
  25. dcc_mcp_runtime-0.2.0/runtime/manifests/obs.json +11 -0
  26. dcc_mcp_runtime-0.2.0/src/dcc_mcp_runtime/__init__.py +17 -0
  27. dcc_mcp_runtime-0.2.0/src/dcc_mcp_runtime/bootstrap.py +38 -0
  28. dcc_mcp_runtime-0.2.0/src/dcc_mcp_runtime/cli.py +25 -0
  29. dcc_mcp_runtime-0.2.0/src/dcc_mcp_runtime/handshake.py +55 -0
  30. dcc_mcp_runtime-0.2.0/src/dcc_mcp_runtime/lifecycle.py +51 -0
  31. dcc_mcp_runtime-0.2.0/src/dcc_mcp_runtime/manifest.py +111 -0
  32. dcc_mcp_runtime-0.2.0/tests/test_bootstrap.py +14 -0
  33. dcc_mcp_runtime-0.2.0/tests/test_runtime.py +59 -0
@@ -0,0 +1,41 @@
1
+ name: CI
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+ branches: [main]
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ test:
13
+ name: ${{ matrix.os }} / Python ${{ matrix.python-version }}
14
+ runs-on: ${{ matrix.os }}
15
+ strategy:
16
+ fail-fast: false
17
+ matrix:
18
+ os: [ubuntu-latest, windows-latest, macos-latest]
19
+ python-version: ["3.9", "3.11", "3.12"]
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+ - uses: actions/setup-python@v5
23
+ with:
24
+ python-version: ${{ matrix.python-version }}
25
+ - name: Install package and test dependencies
26
+ run: python -m pip install -e ".[dev]"
27
+ - name: Test
28
+ run: python -m pytest -q
29
+ - name: Lint
30
+ run: python -m ruff check src tests
31
+ - name: Format check
32
+ run: python -m ruff format --check src tests
33
+ - name: Validate manifests
34
+ shell: bash
35
+ run: |
36
+ python -m dcc_mcp_runtime.cli validate-manifest runtime/manifest.json
37
+ for f in runtime/manifests/*.json; do
38
+ python -m dcc_mcp_runtime.cli validate-manifest "$f" >/dev/null
39
+ done
40
+ - name: Build artifacts
41
+ run: python -m build
@@ -0,0 +1,61 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ workflow_dispatch:
7
+
8
+ permissions:
9
+ contents: write
10
+ pull-requests: write
11
+
12
+ jobs:
13
+ release-please:
14
+ runs-on: ubuntu-latest
15
+ outputs:
16
+ release_created: ${{ steps.release.outputs.release_created }}
17
+ tag_name: ${{ steps.release.outputs.tag_name }}
18
+ version: ${{ steps.release.outputs.version }}
19
+ steps:
20
+ - id: release
21
+ uses: googleapis/release-please-action@v4
22
+ with:
23
+ config-file: release-please-config.json
24
+ manifest-file: .release-please-manifest.json
25
+
26
+ build-release:
27
+ needs: release-please
28
+ if: ${{ needs.release-please.outputs.release_created == 'true' }}
29
+ runs-on: ubuntu-latest
30
+ permissions:
31
+ contents: write
32
+ steps:
33
+ - uses: actions/checkout@v4
34
+ with:
35
+ ref: ${{ needs.release-please.outputs.tag_name }}
36
+ - uses: actions/setup-python@v5
37
+ with:
38
+ python-version: "3.12"
39
+ - name: Install package and release tooling
40
+ run: python -m pip install -e ".[dev]"
41
+ - name: Test release commit
42
+ run: python -m pytest -q
43
+ - name: Lint release commit
44
+ run: python -m ruff check src tests
45
+ - name: Check formatting
46
+ run: python -m ruff format --check src tests
47
+ - name: Validate manifests
48
+ run: |
49
+ python -m dcc_mcp_runtime.cli validate-manifest runtime/manifest.json
50
+ for f in runtime/manifests/*.json; do python -m dcc_mcp_runtime.cli validate-manifest "$f" >/dev/null; done
51
+ - name: Build release artifacts
52
+ run: python -m build
53
+ - name: Upload release artifacts
54
+ uses: softprops/action-gh-release@v2
55
+ with:
56
+ tag_name: ${{ needs.release-please.outputs.tag_name }}
57
+ files: |
58
+ dist/*
59
+ runtime/manifest.json
60
+ runtime/manifests/*.json
61
+ docs/migration/*.md
@@ -0,0 +1,7 @@
1
+ .venv/
2
+ __pycache__/
3
+ .pytest_cache/
4
+ .ruff_cache/
5
+ dist/
6
+ build/
7
+ *.egg-info/
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.2.0"
3
+ }
@@ -0,0 +1,19 @@
1
+ # Changelog
2
+
3
+ ## [0.2.0](https://github.com/dcc-mcp/dcc-mcp-runtime/compare/v0.1.0...v0.2.0) (2026-09-12)
4
+
5
+
6
+ ### Features
7
+
8
+ * route OBS through shared runtime ([42dc3e7](https://github.com/dcc-mcp/dcc-mcp-runtime/commit/42dc3e7950309280edc215662d5793601f68de87))
9
+
10
+ ## 0.1.0
11
+
12
+ - Initial shared external-adapter runtime contract.
13
+ - Added manifest validation, capability handshake, install-plan lifecycle, and
14
+ PyOxidizer packaging scaffold.
15
+ - Added migration entrypoints for Adobe, CapCut, and OBS.
16
+
17
+ This first release is a prerelease scaffold. Runtime and adapter signatures,
18
+ wheel hashes, and final PyOxidizer binaries must be populated before a stable
19
+ production release.
@@ -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.
@@ -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,38 @@
1
+ # dcc-mcp-runtime
2
+
3
+ Shared Python runtime for **external** DCC-MCP adapters (Adobe, CapCut and OBS).
4
+ The runtime is a versioned, side-by-side install containing CPython, `dcc-mcp-core`
5
+ and adapter wheels under `lib/site-packages`. Development still uses `uv`/wheels;
6
+ PyOxidizer is the reproducible distribution path.
7
+
8
+ Embedded DCC Python (Maya, Blender, Houdini, 3ds Max, etc.) is intentionally not
9
+ injected into this runtime. Those adapters keep their host interpreter and only
10
+ consume the same protocol/manifest contract.
11
+
12
+ ## Quick start
13
+
14
+ ```powershell
15
+ uv sync --extra dev
16
+ uv run pytest
17
+ uv run ruff check .
18
+ python -m build
19
+ ```
20
+
21
+ `runtime/manifest.json` is the machine-readable contract. Validate an adapter
22
+ before loading it:
23
+
24
+ ```powershell
25
+ python -m dcc_mcp_runtime.cli validate-manifest runtime/manifests/capcut.json
26
+ ```
27
+
28
+ Installation and upgrade are represented by a typed plan. The runtime never
29
+ executes arbitrary shell/Python: an operator must approve an exact plan and a
30
+ host-owned installer performs the allow-listed operation. A failed health/hash
31
+ verification leaves the previous runtime active for rollback.
32
+
33
+ Releases use [release-please](https://github.com/googleapis/release-please):
34
+ Conventional Commits on `main` create a release PR; merging it creates the tag
35
+ and release, then the same workflow builds and uploads the wheel, sdist, and
36
+ runtime/adapter manifests to that exact release tag.
37
+
38
+ See [migration guides](docs/migration/) and [ADR 0001](docs/ADR-0001-shared-runtime.md).
@@ -0,0 +1,18 @@
1
+ """Standalone Adobe/After Effects bridge entry point.
2
+
3
+ The selected standalone DCC-MCP adapter is ``dcc-mcp-aftereffects``. The
4
+ ``adobepy`` repository remains an SDK/broker and is not launched here.
5
+ """
6
+
7
+ from dcc_mcp_runtime.bootstrap import require_adapter
8
+
9
+
10
+ def main() -> None:
11
+ require_adapter("adobe")
12
+ from dcc_mcp_aftereffects.server import start_server
13
+
14
+ start_server()
15
+
16
+
17
+ if __name__ == "__main__":
18
+ main()
@@ -0,0 +1,12 @@
1
+ diff --git a/src/dcc_mcp_aftereffects/server.py b/src/dcc_mcp_aftereffects/server.py
2
+ --- a/src/dcc_mcp_aftereffects/server.py
3
+ +++ b/src/dcc_mcp_aftereffects/server.py
4
+ @@
5
+ from dcc_mcp_core.server_base import DccServerBase
6
+ +from dcc_mcp_runtime.bootstrap import require_adapter
7
+ @@
8
+ def start_server(port: Optional[int] = None) -> AfterEffectsMcpServer:
9
+ global _server
10
+ if _server is None or not _server.is_running:
11
+ + require_adapter("adobe")
12
+ _server = AfterEffectsMcpServer(
@@ -0,0 +1,12 @@
1
+ diff --git a/src/dcc_mcp_capcut/server.py b/src/dcc_mcp_capcut/server.py
2
+ --- a/src/dcc_mcp_capcut/server.py
3
+ +++ b/src/dcc_mcp_capcut/server.py
4
+ @@
5
+ from dcc_mcp_core.server_base import DccServerBase
6
+ +from dcc_mcp_runtime.bootstrap import require_adapter
7
+ @@
8
+ def start_server(port: Optional[int] = None) -> CapCutMcpServer:
9
+ global _server
10
+ if _server is None or not _server.is_running:
11
+ + require_adapter("capcut")
12
+ _server = CapCutMcpServer(port if port is not None else DEFAULT_PORT)
@@ -0,0 +1,14 @@
1
+ """Drop-in entry point for dcc-mcp-capcut in the shared runtime."""
2
+
3
+ from dcc_mcp_runtime.bootstrap import require_adapter
4
+
5
+
6
+ def main() -> None:
7
+ require_adapter("capcut")
8
+ from dcc_mcp_capcut.server import start_server
9
+
10
+ start_server()
11
+
12
+
13
+ if __name__ == "__main__":
14
+ main()
@@ -0,0 +1,12 @@
1
+ diff --git a/src/dcc_mcp_obs/server.py b/src/dcc_mcp_obs/server.py
2
+ --- a/src/dcc_mcp_obs/server.py
3
+ +++ b/src/dcc_mcp_obs/server.py
4
+ @@
5
+ from dcc_mcp_core.server_base import DccServerBase
6
+ +from dcc_mcp_runtime.bootstrap import require_adapter
7
+ @@
8
+ def start_server(*, port: int | None = None, host_pid: int | None = None) -> ObsMcpServer:
9
+ global _server
10
+ if _server is None or not _server.is_running:
11
+ + require_adapter("obs")
12
+ if _server is not None:
@@ -0,0 +1,16 @@
1
+ """Drop-in entry point for dcc-mcp-obs in the shared runtime."""
2
+
3
+ from dcc_mcp_runtime.bootstrap import require_adapter
4
+
5
+
6
+ def main() -> None:
7
+ require_adapter("obs")
8
+ # The shared runtime owns Python and transport; OBS owns its native plugin
9
+ # and WebSocket bridge. Avoid the legacy self-contained wrapper.
10
+ from dcc_mcp_obs.server import main as obs_main
11
+
12
+ obs_main()
13
+
14
+
15
+ if __name__ == "__main__":
16
+ main()
@@ -0,0 +1,24 @@
1
+ # ADR 0001: Shared runtime for external DCC adapters
2
+
3
+ ## Decision
4
+
5
+ Adobe, CapCut and OBS use a side-by-side PyOxidizer runtime containing a
6
+ supported CPython build, `dcc-mcp-core`, and independently versioned adapter
7
+ wheels in `lib/site-packages`. Adapter startup performs a manifest/ABI/
8
+ capability handshake before registering with the gateway.
9
+
10
+ Maya, Blender, Houdini, 3ds Max and other embedded hosts retain their native
11
+ Python. They may consume the manifest protocol, but are never forced to load a
12
+ foreign interpreter or ABI.
13
+
14
+ ## Integrity and lifecycle
15
+
16
+ Every runtime and wheel records SHA-256 and an operator-issued signature. The
17
+ installer receives an exact typed plan, requires explicit confirmation, verifies
18
+ the downloaded artifact before activation, and keeps the previous side-by-side
19
+ version until health and capability checks pass. Failed activation restores the
20
+ previous version. The runtime has no generic shell, registry, or raw Python
21
+ execution API.
22
+
23
+ PyOxidizer is a publishing mechanism; `uv sync`, wheel builds and normal Python
24
+ tests remain the development path.
@@ -0,0 +1,18 @@
1
+ # Adobe migration
2
+
3
+ ## Selected standalone adapter
4
+
5
+ The current standalone DCC-MCP bridge is `F:\github\dcc-mcp-aftereffects`: it
6
+ contains `DccServerBase`, a CEP bridge broker and MCP server lifecycle. The
7
+ `F:\github\dcc-mcp-adobepy` repository is an Adobe SDK/broker and remains a
8
+ library/transport dependency; it is not treated as the standalone adapter.
9
+
10
+ Keep the existing Photoshop/After Effects bridge and host plug-in in Adobe's
11
+ native process. Replace only the standalone service launch with the shared
12
+ runtime executable and pass the adapter wheel through the operator-owned
13
+ `adapter_wheels` allow-list. Do not import the runtime into CEP/UXP or inject
14
+ CPython into an Adobe process.
15
+
16
+ At startup load `runtime/manifests/adobe.json`, call `negotiate`, then register
17
+ the resulting `capabilities_fingerprint`. A failed ABI/core/signature check is
18
+ reported as not-ready and leaves the previous runtime active.
@@ -0,0 +1,11 @@
1
+ # CapCut migration
2
+
3
+ CapCut is an external bridge: package `dcc-mcp-capcut` as a wheel and install it
4
+ under `lib/site-packages` of the shared runtime. Keep CapCut Desktop UI control,
5
+ token bridge and installation lifecycle in the adapter. The runtime only owns
6
+ process startup, MCP/gateway registration and typed handshake.
7
+
8
+ Use the exact install plan from `dcc_mcp_runtime.lifecycle.plan_install`; route
9
+ execution through `ui_control__system_operation` with an operator grant. Never
10
+ add a PowerShell or arbitrary Python fallback. Preserve side-by-side rollback
11
+ when CapCut or the runtime fails readiness.
@@ -0,0 +1,11 @@
1
+ # OBS migration
2
+
3
+ OBS is an external native/WebSocket bridge. Build `dcc-mcp-obs` as a wheel and
4
+ place it in the runtime's `lib/site-packages`; start `adapters/obs_runtime_entry.py`
5
+ from the shared runtime. Keep OBS process discovery, WebSocket authentication
6
+ and scene operations in that adapter. Do not load the shared runtime into OBS's
7
+ own plug-in Python.
8
+
9
+ Use the existing OBS acceptance and readiness checks after handshake. Upgrade
10
+ only after wheel hash/signature verification and a successful loopback health
11
+ probe; retain the previous runtime for rollback.
@@ -0,0 +1,39 @@
1
+ """PyOxidizer distribution for the shared external-adapter runtime.
2
+
3
+ Build with `pyoxidizer build --var core_version=0.20.14`. Adapter wheels are
4
+ provided through the `DCC_MCP_RUNTIME_ADAPTER_WHEELS` build variable (a
5
+ semicolon-separated, operator-supplied allow-list). No network or arbitrary
6
+ commands are executed by this file.
7
+ """
8
+
9
+ def make_exe():
10
+ dist = default_python_distribution()
11
+ policy = dist.make_python_packaging_policy()
12
+ policy.resources_location = "filesystem-relative:lib"
13
+ python_config = dist.make_python_interpreter_config()
14
+ python_config.oxidized_importer = False
15
+ python_config.filesystem_importer = True
16
+ python_config.module_search_paths = ["$ORIGIN/lib/site-packages"]
17
+ python_config.run_module = "dcc_mcp_runtime.cli"
18
+ python_config.parse_argv = True
19
+ exe = dist.to_python_executable(
20
+ name="dcc-mcp-runtime",
21
+ packaging_policy=policy,
22
+ config=python_config,
23
+ )
24
+ wheels = VARS.get("adapter_wheels", "").split(";")
25
+ wheels = [wheel for wheel in wheels if wheel]
26
+ resources = exe.pip_install([".", "dcc-mcp-core=={}".format(VARS["core_version"])])
27
+ if wheels:
28
+ resources += exe.pip_install(wheels)
29
+ exe.add_python_resources(resources)
30
+ return exe
31
+
32
+ def make_install(exe):
33
+ files = FileManifest()
34
+ files.add_python_resource(".", exe)
35
+ return files
36
+
37
+ register_target("exe", make_exe)
38
+ register_target("install", make_install, depends=["exe"], default=True)
39
+ resolve_targets()
@@ -0,0 +1,35 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.26,<2"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "dcc-mcp-runtime"
7
+ version = "0.2.0"
8
+ description = "Shared, signed runtime contract for external DCC-MCP adapters"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "loonghao", email = "hal.long@outlook.com" }]
13
+ dependencies = []
14
+
15
+ [project.optional-dependencies]
16
+ dev = ["build>=1.2,<2", "pytest>=8,<9", "ruff>=0.8,<1"]
17
+
18
+ [project.scripts]
19
+ dcc-mcp-runtime = "dcc_mcp_runtime.cli:main"
20
+
21
+ [tool.hatch.build.targets.wheel]
22
+ packages = ["src/dcc_mcp_runtime"]
23
+
24
+ [tool.pytest.ini_options]
25
+ testpaths = ["tests"]
26
+ pythonpath = ["src"]
27
+
28
+ [tool.ruff]
29
+ target-version = "py39"
30
+ line-length = 100
31
+ src = ["src", "tests"]
32
+ exclude = ["build", "dist", ".venv", ".pytest_cache", ".ruff_cache"]
33
+
34
+ [tool.ruff.lint]
35
+ select = ["E", "F", "I", "B", "UP"]
@@ -0,0 +1,17 @@
1
+ {
2
+ "packages": {
3
+ ".": {
4
+ "release-type": "python",
5
+ "package-name": "dcc-mcp-runtime",
6
+ "changelog-path": "CHANGELOG.md",
7
+ "include-component-in-tag": false,
8
+ "extra-files": [
9
+ {
10
+ "type": "toml",
11
+ "path": "pyproject.toml",
12
+ "jsonpath": "$.project.version"
13
+ }
14
+ ]
15
+ }
16
+ }
17
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "schema_version": "1",
3
+ "runtime_id": "dcc-mcp-external",
4
+ "version": "0.1.0",
5
+ "python_version": "3.11",
6
+ "python_abi": "cp311-win_amd64",
7
+ "platform": "windows-x86_64",
8
+ "dcc_mcp_core": ">=0.19.13,<1.0.0",
9
+ "runtime_sha256": null,
10
+ "signature": null,
11
+ "adapters": []
12
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "schema_version": "1",
3
+ "adapter_id": "adobe",
4
+ "version": "0.1.0",
5
+ "dcc_mcp_core": ">=0.19.13,<1.0.0",
6
+ "python_abi": "cp311-win_amd64",
7
+ "capabilities": ["documents", "layers", "timeline", "media", "export"],
8
+ "wheel_sha256": "0000000000000000000000000000000000000000000000000000000000000000",
9
+ "signature": null,
10
+ "host_mode": "external"
11
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "schema_version": "1",
3
+ "adapter_id": "capcut",
4
+ "version": "0.1.0",
5
+ "dcc_mcp_core": ">=0.19.13,<1.0.0",
6
+ "python_abi": "cp311-win_amd64",
7
+ "capabilities": ["project", "media", "timeline", "captions", "audio", "effects", "export"],
8
+ "wheel_sha256": "0000000000000000000000000000000000000000000000000000000000000000",
9
+ "signature": null,
10
+ "host_mode": "external"
11
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "schema_version": "1",
3
+ "adapter_id": "obs",
4
+ "version": "1.4.0",
5
+ "dcc_mcp_core": ">=0.19.13,<1.0.0",
6
+ "python_abi": "cp311-win_amd64",
7
+ "capabilities": ["scenes", "sources", "streaming", "recording", "replay-buffer"],
8
+ "wheel_sha256": "0000000000000000000000000000000000000000000000000000000000000000",
9
+ "signature": null,
10
+ "host_mode": "external"
11
+ }
@@ -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
@@ -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,14 @@
1
+ from pathlib import Path
2
+
3
+ from dcc_mcp_runtime.bootstrap import negotiate_adapter, runtime_root
4
+
5
+
6
+ def test_runtime_root_can_be_explicit(monkeypatch):
7
+ monkeypatch.setenv("DCC_MCP_RUNTIME_ROOT", str(Path.cwd()))
8
+ assert runtime_root() == Path.cwd().resolve()
9
+
10
+
11
+ def test_capcut_and_obs_compatibility_entries_handshake(monkeypatch):
12
+ monkeypatch.setenv("DCC_MCP_RUNTIME_ROOT", str(Path.cwd()))
13
+ assert negotiate_adapter("capcut").accepted
14
+ assert negotiate_adapter("obs").accepted
@@ -0,0 +1,59 @@
1
+ from dcc_mcp_runtime.handshake import negotiate
2
+ from dcc_mcp_runtime.lifecycle import plan_install
3
+ from dcc_mcp_runtime.manifest import AdapterManifest, RuntimeManifest, load_manifest
4
+
5
+ HASH = "a" * 64
6
+
7
+
8
+ def runtime() -> RuntimeManifest:
9
+ return RuntimeManifest(
10
+ runtime_id="test-runtime",
11
+ version="1.0.0",
12
+ python_version="3.11",
13
+ python_abi="cp311-win_amd64",
14
+ platform="windows-x86_64",
15
+ dcc_mcp_core=">=0.19.13,<1.0.0",
16
+ )
17
+
18
+
19
+ def adapter(**overrides) -> AdapterManifest:
20
+ values = dict(
21
+ adapter_id="capcut",
22
+ version="0.1.0",
23
+ dcc_mcp_core=">=0.19.13,<1.0.0",
24
+ python_abi="cp311-win_amd64",
25
+ capabilities=("timeline", "media"),
26
+ wheel_sha256=HASH,
27
+ )
28
+ values.update(overrides)
29
+ return AdapterManifest(**values)
30
+
31
+
32
+ def test_external_handshake_includes_stable_fingerprint():
33
+ result = negotiate(runtime(), adapter())
34
+ assert result.accepted is True
35
+ assert result.handshake is not None
36
+ assert len(result.handshake.capabilities_fingerprint) == 64
37
+
38
+
39
+ def test_embedded_adapter_is_not_injected():
40
+ result = negotiate(runtime(), adapter(host_mode="embedded"))
41
+ assert result == result.__class__(False, "embedded_adapter_requires_host_runtime")
42
+
43
+
44
+ def test_install_plan_requires_confirmation_and_rollback():
45
+ plan = plan_install(
46
+ operation_id="op-1",
47
+ runtime_id="dcc-mcp-external",
48
+ target_version="0.2.0",
49
+ package_path="runtime-0.2.0.zip",
50
+ expected_sha256=HASH,
51
+ previous_version="0.1.0",
52
+ )
53
+ assert plan.requires_confirmation
54
+ assert plan.rollback_version == "0.1.0"
55
+
56
+
57
+ def test_runtime_manifest_is_loadable():
58
+ manifest = load_manifest("runtime/manifest.json")
59
+ assert manifest.runtime_id == "dcc-mcp-external"