dcc-mcp-nuke 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.
@@ -0,0 +1,29 @@
1
+ name: CI
2
+ on:
3
+ push: {branches: [main, "release-please--branches--**"]}
4
+ pull_request: {branches: [main]}
5
+ concurrency: {group: "${{ github.workflow }}-${{ github.ref }}", cancel-in-progress: true}
6
+ permissions: {contents: read}
7
+ jobs:
8
+ test:
9
+ runs-on: ${{ matrix.os }}
10
+ strategy:
11
+ fail-fast: false
12
+ matrix: {os: [ubuntu-latest, windows-latest, macos-latest], python: ["3.9", "3.11", "3.12"], exclude: [{os: windows-latest, python: "3.9"}, {os: macos-latest, python: "3.9"}]}
13
+ steps:
14
+ - uses: actions/checkout@v6
15
+ - uses: actions/setup-python@v6
16
+ with: {python-version: "${{ matrix.python }}"}
17
+ - run: python -m pip install -e ".[dev]"
18
+ - run: python -m pytest
19
+ lint-build:
20
+ runs-on: ubuntu-latest
21
+ steps:
22
+ - uses: actions/checkout@v6
23
+ - uses: actions/setup-python@v6
24
+ with: {python-version: "3.11"}
25
+ - run: python -m pip install -e ".[dev]"
26
+ - run: ruff check src tests tools
27
+ - run: ruff format --check src tests tools
28
+ - run: python tools/lint_skills.py
29
+ - run: python -m build && python -m twine check dist/*
@@ -0,0 +1,25 @@
1
+ name: Release
2
+ on:
3
+ push: {branches: [main]}
4
+ permissions: {contents: write, pull-requests: write, id-token: write}
5
+ jobs:
6
+ release-please:
7
+ runs-on: ubuntu-latest
8
+ outputs: {release_created: "${{ steps.release.outputs.release_created }}", tag_name: "${{ steps.release.outputs.tag_name }}"}
9
+ steps:
10
+ - uses: googleapis/release-please-action@v5
11
+ id: release
12
+ with: {token: "${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}", config-file: release-please-config.json, manifest-file: .release-please-manifest.json}
13
+ publish:
14
+ needs: release-please
15
+ if: needs.release-please.outputs.release_created == 'true'
16
+ runs-on: ubuntu-latest
17
+ environment: {name: pypi, url: "https://pypi.org/p/dcc-mcp-nuke"}
18
+ permissions: {id-token: write}
19
+ steps:
20
+ - uses: actions/checkout@v6
21
+ with: {ref: "${{ needs.release-please.outputs.tag_name }}"}
22
+ - uses: actions/setup-python@v6
23
+ with: {python-version: "3.11"}
24
+ - run: python -m pip install build twine && python -m build && python -m twine check dist/*
25
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,6 @@
1
+ __pycache__/
2
+ .pytest_cache/
3
+ .ruff_cache/
4
+ build/
5
+ dist/
6
+ *.egg-info/
@@ -0,0 +1 @@
1
+ {".":"0.2.0"}
@@ -0,0 +1,21 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ ## [0.2.0](https://github.com/dcc-mcp/dcc-mcp-nuke/compare/v0.1.0...v0.2.0) (2026-07-13)
6
+
7
+
8
+ ### Features
9
+
10
+ * add Nuke MCP adapter ([1ff27dd](https://github.com/dcc-mcp/dcc-mcp-nuke/commit/1ff27dd43a6031e754de5a1de7b5372d126d2fe3))
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * mark package version for release please ([1e31082](https://github.com/dcc-mcp/dcc-mcp-nuke/commit/1e310822ddeef3ac7e25c0594bd096485277e242))
16
+
17
+ ## [0.1.0] - 2026-07-14
18
+
19
+ ### Added
20
+
21
+ - Initial Nuke MCP adapter with typed script tools.
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Long Hao
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
@@ -0,0 +1,43 @@
1
+ Metadata-Version: 2.4
2
+ Name: dcc-mcp-nuke
3
+ Version: 0.2.0
4
+ Summary: Nuke adapter for the DCC Model Context Protocol ecosystem
5
+ Project-URL: Homepage, https://github.com/dcc-mcp/dcc-mcp-nuke
6
+ Project-URL: Repository, https://github.com/dcc-mcp/dcc-mcp-nuke
7
+ Project-URL: Issues, https://github.com/dcc-mcp/dcc-mcp-nuke/issues
8
+ Author-email: Long Hao <hal.long@outlook.com>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Multimedia :: Graphics
19
+ Requires-Python: >=3.9
20
+ Requires-Dist: dcc-mcp-core<1.0.0,>=0.19.3
21
+ Provides-Extra: dev
22
+ Requires-Dist: build; extra == 'dev'
23
+ Requires-Dist: pytest>=7; extra == 'dev'
24
+ Requires-Dist: pyyaml>=6; extra == 'dev'
25
+ Requires-Dist: ruff>=0.8; extra == 'dev'
26
+ Requires-Dist: twine; extra == 'dev'
27
+ Description-Content-Type: text/markdown
28
+
29
+ # dcc-mcp-nuke
30
+
31
+ Nuke adapter for the DCC Model Context Protocol. It embeds a Streamable HTTP
32
+ MCP server in Nuke and uses Nuke's main-thread execution API for scene tools.
33
+
34
+ ```bash
35
+ python -m pip install dcc-mcp-nuke
36
+ ```
37
+
38
+ Add the installed package's `dcc_mcp_nuke/nuke_plugin` folder to `NUKE_PATH`.
39
+ Nuke loads its `init.py` and serves MCP at `http://127.0.0.1:8765/mcp`.
40
+
41
+ The bundled `nuke-script` skill inspects scripts and nodes and can explicitly
42
+ save the current script. Releases are published through `release.yaml` and the
43
+ GitHub `pypi` environment.
@@ -0,0 +1,15 @@
1
+ # dcc-mcp-nuke
2
+
3
+ Nuke adapter for the DCC Model Context Protocol. It embeds a Streamable HTTP
4
+ MCP server in Nuke and uses Nuke's main-thread execution API for scene tools.
5
+
6
+ ```bash
7
+ python -m pip install dcc-mcp-nuke
8
+ ```
9
+
10
+ Add the installed package's `dcc_mcp_nuke/nuke_plugin` folder to `NUKE_PATH`.
11
+ Nuke loads its `init.py` and serves MCP at `http://127.0.0.1:8765/mcp`.
12
+
13
+ The bundled `nuke-script` skill inspects scripts and nodes and can explicitly
14
+ save the current script. Releases are published through `release.yaml` and the
15
+ GitHub `pypi` environment.
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "dcc-mcp-nuke"
7
+ version = "0.2.0"
8
+ description = "Nuke adapter for the DCC Model Context Protocol ecosystem"
9
+ authors = [{ name = "Long Hao", email = "hal.long@outlook.com" }]
10
+ readme = "README.md"
11
+ license = { text = "MIT" }
12
+ requires-python = ">=3.9"
13
+ dependencies = ["dcc-mcp-core>=0.19.3,<1.0.0"]
14
+ classifiers = ["Development Status :: 3 - Alpha", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Topic :: Multimedia :: Graphics"]
15
+
16
+ [project.optional-dependencies]
17
+ dev = ["build", "pytest>=7", "ruff>=0.8", "twine", "pyyaml>=6"]
18
+
19
+ [project.urls]
20
+ Homepage = "https://github.com/dcc-mcp/dcc-mcp-nuke"
21
+ Repository = "https://github.com/dcc-mcp/dcc-mcp-nuke"
22
+ Issues = "https://github.com/dcc-mcp/dcc-mcp-nuke/issues"
23
+
24
+ [project.entry-points."dcc_mcp.adapters"]
25
+ nuke = "dcc_mcp_nuke:NukeMcpServer"
26
+
27
+ [tool.hatch.build.targets.wheel]
28
+ packages = ["src/dcc_mcp_nuke"]
29
+ artifacts = ["src/dcc_mcp_nuke/skills/**", "src/dcc_mcp_nuke/nuke_plugin/**"]
30
+
31
+ [tool.pytest.ini_options]
32
+ testpaths = ["tests"]
33
+ pythonpath = ["src"]
34
+
35
+ [tool.ruff]
36
+ line-length = 120
37
+ target-version = "py39"
38
+
39
+ [tool.ruff.lint]
40
+ select = ["E", "F", "I", "W", "B"]
41
+ ignore = ["E501"]
@@ -0,0 +1 @@
1
+ {"packages":{".":{"release-type":"python","package-name":"dcc-mcp-nuke","changelog-path":"CHANGELOG.md","include-component-in-tag":false,"extra-files":[{"type":"toml","path":"pyproject.toml","jsonpath":"$.project.version"},{"type":"generic","path":"src/dcc_mcp_nuke/__version__.py"}]}}}
@@ -0,0 +1,4 @@
1
+ from dcc_mcp_nuke.__version__ import __version__
2
+ from dcc_mcp_nuke.server import NukeMcpServer, start_server, stop_server
3
+
4
+ __all__ = ["__version__", "NukeMcpServer", "start_server", "stop_server"]
@@ -0,0 +1 @@
1
+ __version__ = "0.2.0" # x-release-please-version
@@ -0,0 +1,23 @@
1
+ """Nuke's native main-thread bridge."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import threading
6
+ from typing import Any, Callable
7
+
8
+
9
+ class NukeDispatcher:
10
+ """Route core skill calls into Nuke without managing a second queue."""
11
+
12
+ def dispatch_callable(self, func: Callable[..., Any], *args: Any, **kwargs: Any) -> Any:
13
+ kwargs.pop("context", None)
14
+ kwargs.pop("action_name", None)
15
+ kwargs.pop("skill_name", None)
16
+ kwargs.pop("execution", None)
17
+ kwargs.pop("timeout_hint_secs", None)
18
+ kwargs.pop("affinity", kwargs.pop("thread_affinity", None))
19
+ if threading.current_thread() is threading.main_thread():
20
+ return func(*args, **kwargs)
21
+ import nuke # Lazy import: requires a running Nuke process.
22
+
23
+ return nuke.executeInMainThreadWithResult(func, args, kwargs)
@@ -0,0 +1,5 @@
1
+ """Nuke startup entry point; loaded from NUKE_PATH."""
2
+
3
+ from dcc_mcp_nuke.plugin import initialize
4
+
5
+ initialize()
@@ -0,0 +1,9 @@
1
+ from dcc_mcp_nuke.server import start_server, stop_server
2
+
3
+
4
+ def initialize() -> None:
5
+ start_server()
6
+
7
+
8
+ def shutdown() -> None:
9
+ stop_server()
@@ -0,0 +1,52 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+ from dcc_mcp_core import DccServerOptions, HostExecutionBridge
8
+ from dcc_mcp_core.server_base import DccServerBase
9
+
10
+ from dcc_mcp_nuke.__version__ import __version__
11
+ from dcc_mcp_nuke.dispatcher import NukeDispatcher
12
+
13
+ DEFAULT_PORT = 8765
14
+ SERVER_NAME = "dcc-mcp-nuke"
15
+ _server: Optional["NukeMcpServer"] = None
16
+
17
+
18
+ class NukeMcpServer(DccServerBase):
19
+ def __init__(self, port: int = DEFAULT_PORT) -> None:
20
+ options = DccServerOptions.from_env(
21
+ "nuke",
22
+ Path(__file__).resolve().parent / "skills",
23
+ port=port,
24
+ server_name=SERVER_NAME,
25
+ server_version=__version__,
26
+ execution_bridge=HostExecutionBridge(dispatcher=NukeDispatcher()),
27
+ )
28
+ super().__init__(options=options)
29
+
30
+ def _version_string(self) -> str:
31
+ try:
32
+ import nuke
33
+
34
+ return str(nuke.env.get("NukeVersionString", "Nuke"))
35
+ except Exception:
36
+ return "Nuke"
37
+
38
+
39
+ def start_server(port: Optional[int] = None) -> NukeMcpServer:
40
+ global _server
41
+ if _server is None or not _server.is_running:
42
+ _server = NukeMcpServer(port or int(os.environ.get("DCC_MCP_NUKE_PORT", DEFAULT_PORT)))
43
+ _server.register_builtin_actions()
44
+ _server.start()
45
+ return _server
46
+
47
+
48
+ def stop_server() -> None:
49
+ global _server
50
+ if _server is not None:
51
+ _server.stop()
52
+ _server = None
@@ -0,0 +1,20 @@
1
+ ---
2
+ name: nuke-script
3
+ description: >-
4
+ Host skill - inspect and explicitly save the active Nuke script. Use when
5
+ checking root settings, selected nodes, or saving a known path. Not for raw Python execution.
6
+ license: MIT
7
+ compatibility: "Nuke Python API; dcc-mcp-core 0.19+"
8
+ allowed-tools: Python
9
+ metadata:
10
+ dcc-mcp:
11
+ dcc: nuke
12
+ version: "0.0.0"
13
+ layer: domain
14
+ stage: scene
15
+ search-hint: "nuke script root selected nodes inspect save nk"
16
+ tags: "nuke, compositing, nodes, script"
17
+ tools: tools.yaml
18
+ ---
19
+
20
+ # Nuke Script
@@ -0,0 +1,16 @@
1
+ from dcc_mcp_core.skill import skill_entry, skill_success
2
+
3
+
4
+ @skill_entry
5
+ def main(**_kwargs):
6
+ import nuke # Lazy import: requires Nuke.
7
+
8
+ root = nuke.root()
9
+ selected = nuke.selectedNodes()
10
+ return skill_success(
11
+ "Inspected Nuke script",
12
+ path=nuke.scriptName(),
13
+ first_frame=root["first_frame"].value(),
14
+ last_frame=root["last_frame"].value(),
15
+ selected_nodes=[node.name() for node in selected],
16
+ )
@@ -0,0 +1,9 @@
1
+ from dcc_mcp_core.skill import skill_entry, skill_success
2
+
3
+
4
+ @skill_entry
5
+ def main(**_kwargs):
6
+ import nuke # Lazy import: requires Nuke.
7
+
8
+ nodes = [{"name": node.name(), "class": node.Class()} for node in nuke.allNodes(recurseGroups=True)]
9
+ return skill_success("Listed Nuke nodes", node_count=len(nodes), nodes=nodes)
@@ -0,0 +1,15 @@
1
+ from pathlib import Path
2
+
3
+ from dcc_mcp_core.skill import skill_entry, skill_error, skill_success
4
+
5
+
6
+ @skill_entry
7
+ def main(path: str, **_kwargs):
8
+ import nuke # Lazy import: requires Nuke.
9
+
10
+ target = Path(path).expanduser().resolve()
11
+ if target.suffix.lower() != ".nk":
12
+ return skill_error("Nuke script path must end in .nk", str(target))
13
+ target.parent.mkdir(parents=True, exist_ok=True)
14
+ nuke.scriptSaveAs(str(target))
15
+ return skill_success("Saved Nuke script", path=str(target))
@@ -0,0 +1,34 @@
1
+ tools:
2
+ - name: inspect_script
3
+ description: Inspect the active Nuke script, frame range, and selected nodes without changing it.
4
+ input_schema: {type: object, properties: {}}
5
+ read_only: true
6
+ destructive: false
7
+ idempotent: true
8
+ execution: sync
9
+ affinity: main
10
+ timeout_hint_secs: 15
11
+ source_file: scripts/inspect_script.py
12
+ - name: list_nodes
13
+ description: List nodes in the current Nuke script without modifying it.
14
+ input_schema: {type: object, properties: {}}
15
+ read_only: true
16
+ destructive: false
17
+ idempotent: true
18
+ execution: sync
19
+ affinity: main
20
+ timeout_hint_secs: 30
21
+ source_file: scripts/list_nodes.py
22
+ - name: save_script
23
+ description: Save the current Nuke script to an explicit absolute .nk path.
24
+ input_schema:
25
+ type: object
26
+ required: [path]
27
+ properties: {path: {type: string, description: Absolute .nk output path.}}
28
+ read_only: false
29
+ destructive: true
30
+ idempotent: false
31
+ execution: sync
32
+ affinity: main
33
+ timeout_hint_secs: 60
34
+ source_file: scripts/save_script.py
@@ -0,0 +1,23 @@
1
+ import json
2
+ import re
3
+ from pathlib import Path
4
+
5
+ import dcc_mcp_nuke
6
+
7
+ ROOT = Path(__file__).resolve().parents[1]
8
+
9
+
10
+ def test_version_metadata_is_synchronized():
11
+ version = re.search(r'(?m)^version = "([^"]+)"$', ROOT.joinpath("pyproject.toml").read_text())
12
+ assert (
13
+ version
14
+ and version.group(1)
15
+ == dcc_mcp_nuke.__version__
16
+ == json.loads(ROOT.joinpath(".release-please-manifest.json").read_text())["."]
17
+ )
18
+
19
+
20
+ def test_bundled_contract_files_exist():
21
+ package = ROOT / "src" / "dcc_mcp_nuke"
22
+ assert package.joinpath("nuke_plugin", "init.py").exists()
23
+ assert package.joinpath("skills", "nuke-script", "tools.yaml").exists()
@@ -0,0 +1,8 @@
1
+ from pathlib import Path
2
+
3
+ from dcc_mcp_core import validate_skill
4
+
5
+ paths = [path for path in (Path(__file__).parents[1] / "src" / "dcc_mcp_nuke" / "skills").iterdir() if path.is_dir()]
6
+ reports = [validate_skill(str(path)) for path in paths]
7
+ assert all(report.is_clean for report in reports), [report.issues for report in reports]
8
+ print(f"validated {len(reports)} bundled skills")