dcc-mcp-renderdoc 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 (31) hide show
  1. dcc_mcp_renderdoc-0.2.0/.gitignore +12 -0
  2. dcc_mcp_renderdoc-0.2.0/.release-please-manifest.json +2 -0
  3. dcc_mcp_renderdoc-0.2.0/CHANGELOG.md +21 -0
  4. dcc_mcp_renderdoc-0.2.0/LICENSE +22 -0
  5. dcc_mcp_renderdoc-0.2.0/PKG-INFO +79 -0
  6. dcc_mcp_renderdoc-0.2.0/README.md +51 -0
  7. dcc_mcp_renderdoc-0.2.0/pyproject.toml +68 -0
  8. dcc_mcp_renderdoc-0.2.0/release-please-config.json +2 -0
  9. dcc_mcp_renderdoc-0.2.0/src/dcc_mcp_renderdoc/__init__.py +6 -0
  10. dcc_mcp_renderdoc-0.2.0/src/dcc_mcp_renderdoc/__version__.py +3 -0
  11. dcc_mcp_renderdoc-0.2.0/src/dcc_mcp_renderdoc/runtime.py +255 -0
  12. dcc_mcp_renderdoc-0.2.0/src/dcc_mcp_renderdoc/server.py +78 -0
  13. dcc_mcp_renderdoc-0.2.0/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/SKILL.md +25 -0
  14. dcc_mcp_renderdoc-0.2.0/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/metadata/depends.md +2 -0
  15. dcc_mcp_renderdoc-0.2.0/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/scripts/export_thumbnail.py +15 -0
  16. dcc_mcp_renderdoc-0.2.0/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/scripts/export_timeline.py +15 -0
  17. dcc_mcp_renderdoc-0.2.0/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/scripts/inspect_capture.py +15 -0
  18. dcc_mcp_renderdoc-0.2.0/src/dcc_mcp_renderdoc/skills/renderdoc-analysis/tools.yaml +63 -0
  19. dcc_mcp_renderdoc-0.2.0/src/dcc_mcp_renderdoc/skills/renderdoc-capture/SKILL.md +25 -0
  20. dcc_mcp_renderdoc-0.2.0/src/dcc_mcp_renderdoc/skills/renderdoc-capture/metadata/depends.md +2 -0
  21. dcc_mcp_renderdoc-0.2.0/src/dcc_mcp_renderdoc/skills/renderdoc-capture/scripts/capture_program.py +34 -0
  22. dcc_mcp_renderdoc-0.2.0/src/dcc_mcp_renderdoc/skills/renderdoc-capture/scripts/get_version.py +14 -0
  23. dcc_mcp_renderdoc-0.2.0/src/dcc_mcp_renderdoc/skills/renderdoc-capture/tools.yaml +45 -0
  24. dcc_mcp_renderdoc-0.2.0/tests/live_renderdoc_smoke.py +142 -0
  25. dcc_mcp_renderdoc-0.2.0/tests/renderdoc_smoke.c +71 -0
  26. dcc_mcp_renderdoc-0.2.0/tests/test_download.py +15 -0
  27. dcc_mcp_renderdoc-0.2.0/tests/test_package.py +22 -0
  28. dcc_mcp_renderdoc-0.2.0/tests/test_runtime.py +57 -0
  29. dcc_mcp_renderdoc-0.2.0/tools/__init__.py +1 -0
  30. dcc_mcp_renderdoc-0.2.0/tools/download_latest_renderdoc.py +89 -0
  31. dcc_mcp_renderdoc-0.2.0/tools/lint_skills.py +8 -0
@@ -0,0 +1,12 @@
1
+ .venv/
2
+ .renderdoc-bin/
3
+ artifacts/
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ __pycache__/
8
+ .pytest_cache/
9
+ .ruff_cache/
10
+ tests/renderdoc_smoke
11
+ tests/renderdoc_smoke.exe
12
+
@@ -0,0 +1,2 @@
1
+ {".":"0.2.0"}
2
+
@@ -0,0 +1,21 @@
1
+ # Changelog
2
+
3
+ ## [0.2.0](https://github.com/dcc-mcp/dcc-mcp-renderdoc/compare/v0.1.0...v0.2.0) (2026-07-15)
4
+
5
+
6
+ ### Features
7
+
8
+ * add RenderDoc MCP adapter ([a324344](https://github.com/dcc-mcp/dcc-mcp-renderdoc/commit/a324344d5dcab758282c77598f4591076526f44e))
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * capture a supported OpenGL core context ([7817a6f](https://github.com/dcc-mcp/dcc-mcp-renderdoc/commit/7817a6fd3a7e683a37348ed4a0582bfe456024b6))
14
+ * include C boolean definitions in smoke target ([a9471ed](https://github.com/dcc-mcp/dcc-mcp-renderdoc/commit/a9471ed7fef6f663a74ed9c9207ae8231bc5dafc))
15
+ * initialize OpenGL before frame capture ([af3c93c](https://github.com/dcc-mcp/dcc-mcp-renderdoc/commit/af3c93c2b7fc794769187d7e237596d5c37de97b))
16
+ * target explicit RenderDoc frame capture ([99f1568](https://github.com/dcc-mcp/dcc-mcp-renderdoc/commit/99f1568e0367c156c6c01106109ea4333e0788f5))
17
+ * trigger a presented frame in RenderDoc smoke ([2d939cb](https://github.com/dcc-mcp/dcc-mcp-renderdoc/commit/2d939cb3432ff6280ac44e56b6f785d1ff98d2ee))
18
+
19
+ ## Changelog
20
+
21
+ All notable changes to this project will be documented in this file.
@@ -0,0 +1,22 @@
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.
22
+
@@ -0,0 +1,79 @@
1
+ Metadata-Version: 2.4
2
+ Name: dcc-mcp-renderdoc
3
+ Version: 0.2.0
4
+ Summary: RenderDoc capture and replay automation skills for DCC-MCP
5
+ Project-URL: Homepage, https://github.com/dcc-mcp/dcc-mcp-renderdoc
6
+ Project-URL: Repository, https://github.com/dcc-mcp/dcc-mcp-renderdoc
7
+ Project-URL: Issues, https://github.com/dcc-mcp/dcc-mcp-renderdoc/issues
8
+ Author-email: loonghao <hal.long@outlook.com>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: automation,game-development,graphics-debugging,mcp,renderdoc
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Games/Entertainment
20
+ Requires-Python: >=3.9
21
+ Requires-Dist: dcc-mcp-core<1.0.0,>=0.19.38
22
+ Provides-Extra: dev
23
+ Requires-Dist: build>=1.2; extra == 'dev'
24
+ Requires-Dist: pytest>=8; extra == 'dev'
25
+ Requires-Dist: ruff>=0.8; extra == 'dev'
26
+ Requires-Dist: twine>=6; extra == 'dev'
27
+ Description-Content-Type: text/markdown
28
+
29
+ # dcc-mcp-renderdoc
30
+
31
+ RenderDoc capture and replay automation for the DCC Model Context Protocol ecosystem.
32
+
33
+ The adapter is headless-first: it reuses the official `renderdoccmd` executable for capture and
34
+ conversion, so agents can automate graphics regression triage without keeping the RenderDoc GUI
35
+ open or installing a second bridge.
36
+
37
+ ## Install
38
+
39
+ ```bash
40
+ pip install dcc-mcp-renderdoc
41
+ ```
42
+
43
+ Install RenderDoc separately, then expose its command line tool with either `PATH` or:
44
+
45
+ ```bash
46
+ export DCC_MCP_RENDERDOC_CMD=/opt/renderdoc/bin/renderdoccmd
47
+ dcc-mcp-renderdoc
48
+ ```
49
+
50
+ On Windows, set the variable to `renderdoccmd.exe`.
51
+
52
+ ## Agent workflows
53
+
54
+ - Launch a game or test executable under RenderDoc and wait for a typed `.rdc` capture.
55
+ - Inspect capture driver, machine identity, chunk version, API-call counts, and representative calls.
56
+ - Export a capture thumbnail for visual review.
57
+ - Export Chrome trace JSON for timeline tooling.
58
+
59
+ The capture tool launches only the explicit executable and arguments supplied by the caller. It
60
+ never invokes a shell. Analysis tools are read-only with respect to the `.rdc` input.
61
+
62
+ ## Real CI
63
+
64
+ CI discovers the current stable RenderDoc build from the official downloads page. It compiles a
65
+ small OpenGL program, captures a real frame under Xvfb, calls the MCP analysis tool against the
66
+ resulting `.rdc`, and verifies thumbnail and timeline exports.
67
+
68
+ ## Development
69
+
70
+ ```bash
71
+ uv sync --extra dev
72
+ uv run python -m pytest
73
+ uv run ruff check src tests tools
74
+ uv run python tools/lint_skills.py
75
+ ```
76
+
77
+ RenderDoc is an MIT-licensed graphics debugger maintained independently at
78
+ [renderdoc.org](https://renderdoc.org/). This adapter is not affiliated with the RenderDoc project.
79
+
@@ -0,0 +1,51 @@
1
+ # dcc-mcp-renderdoc
2
+
3
+ RenderDoc capture and replay automation for the DCC Model Context Protocol ecosystem.
4
+
5
+ The adapter is headless-first: it reuses the official `renderdoccmd` executable for capture and
6
+ conversion, so agents can automate graphics regression triage without keeping the RenderDoc GUI
7
+ open or installing a second bridge.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pip install dcc-mcp-renderdoc
13
+ ```
14
+
15
+ Install RenderDoc separately, then expose its command line tool with either `PATH` or:
16
+
17
+ ```bash
18
+ export DCC_MCP_RENDERDOC_CMD=/opt/renderdoc/bin/renderdoccmd
19
+ dcc-mcp-renderdoc
20
+ ```
21
+
22
+ On Windows, set the variable to `renderdoccmd.exe`.
23
+
24
+ ## Agent workflows
25
+
26
+ - Launch a game or test executable under RenderDoc and wait for a typed `.rdc` capture.
27
+ - Inspect capture driver, machine identity, chunk version, API-call counts, and representative calls.
28
+ - Export a capture thumbnail for visual review.
29
+ - Export Chrome trace JSON for timeline tooling.
30
+
31
+ The capture tool launches only the explicit executable and arguments supplied by the caller. It
32
+ never invokes a shell. Analysis tools are read-only with respect to the `.rdc` input.
33
+
34
+ ## Real CI
35
+
36
+ CI discovers the current stable RenderDoc build from the official downloads page. It compiles a
37
+ small OpenGL program, captures a real frame under Xvfb, calls the MCP analysis tool against the
38
+ resulting `.rdc`, and verifies thumbnail and timeline exports.
39
+
40
+ ## Development
41
+
42
+ ```bash
43
+ uv sync --extra dev
44
+ uv run python -m pytest
45
+ uv run ruff check src tests tools
46
+ uv run python tools/lint_skills.py
47
+ ```
48
+
49
+ RenderDoc is an MIT-licensed graphics debugger maintained independently at
50
+ [renderdoc.org](https://renderdoc.org/). This adapter is not affiliated with the RenderDoc project.
51
+
@@ -0,0 +1,68 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.26"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "dcc-mcp-renderdoc"
7
+ version = "0.2.0"
8
+ description = "RenderDoc capture and replay automation skills for DCC-MCP"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "loonghao", email = "hal.long@outlook.com" }]
13
+ keywords = ["renderdoc", "mcp", "graphics-debugging", "game-development", "automation"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.9",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Topic :: Games/Entertainment",
23
+ ]
24
+ dependencies = ["dcc-mcp-core>=0.19.38,<1.0.0"]
25
+
26
+ [project.urls]
27
+ Homepage = "https://github.com/dcc-mcp/dcc-mcp-renderdoc"
28
+ Repository = "https://github.com/dcc-mcp/dcc-mcp-renderdoc"
29
+ Issues = "https://github.com/dcc-mcp/dcc-mcp-renderdoc/issues"
30
+
31
+ [project.optional-dependencies]
32
+ dev = ["build>=1.2", "pytest>=8", "ruff>=0.8", "twine>=6"]
33
+
34
+ [project.scripts]
35
+ dcc-mcp-renderdoc = "dcc_mcp_renderdoc.server:main"
36
+
37
+ [project.entry-points."dcc_mcp.adapters"]
38
+ renderdoc = "dcc_mcp_renderdoc:RenderDocMcpServer"
39
+
40
+ [tool.hatch.build.targets.wheel]
41
+ packages = ["src/dcc_mcp_renderdoc"]
42
+ artifacts = ["src/dcc_mcp_renderdoc/skills/**"]
43
+
44
+ [tool.hatch.build.targets.sdist]
45
+ include = [
46
+ "src/dcc_mcp_renderdoc",
47
+ "tests",
48
+ "tools",
49
+ "README.md",
50
+ "CHANGELOG.md",
51
+ "LICENSE",
52
+ "release-please-config.json",
53
+ ".release-please-manifest.json",
54
+ ]
55
+
56
+ [tool.pytest.ini_options]
57
+ testpaths = ["tests"]
58
+ pythonpath = ["src", "."]
59
+
60
+ [tool.ruff]
61
+ target-version = "py39"
62
+ line-length = 100
63
+
64
+ [tool.ruff.lint]
65
+ select = ["E", "F", "I", "B"]
66
+
67
+ [tool.ruff.lint.per-file-ignores]
68
+ "src/dcc_mcp_renderdoc/skills/**/scripts/*.py" = ["E501"]
@@ -0,0 +1,2 @@
1
+ {"packages":{".":{"release-type":"python","package-name":"dcc-mcp-renderdoc","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_renderdoc/__version__.py"}]}}}
2
+
@@ -0,0 +1,6 @@
1
+ """RenderDoc adapter for DCC-MCP."""
2
+
3
+ from .__version__ import __version__
4
+ from .server import RenderDocMcpServer
5
+
6
+ __all__ = ["RenderDocMcpServer", "__version__"]
@@ -0,0 +1,3 @@
1
+ """Package version."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,255 @@
1
+ """Small, typed wrapper around RenderDoc's official command-line client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import shutil
7
+ import subprocess
8
+ import tempfile
9
+ import xml.etree.ElementTree as ET
10
+ from collections import Counter
11
+ from pathlib import Path
12
+ from typing import Any, Iterable, Optional, Sequence
13
+
14
+
15
+ class RenderDocError(RuntimeError):
16
+ """Raised when a RenderDoc operation cannot satisfy its contract."""
17
+
18
+
19
+ def resolve_renderdoccmd(explicit: Optional[str] = None) -> Path:
20
+ """Resolve an executable RenderDoc CLI without invoking a shell."""
21
+ candidate = explicit or os.environ.get("DCC_MCP_RENDERDOC_CMD")
22
+ if candidate:
23
+ path = Path(candidate).expanduser().resolve()
24
+ if path.is_file():
25
+ return path
26
+ raise RenderDocError(f"RenderDoc command does not exist: {path}")
27
+
28
+ for name in ("renderdoccmd.exe", "renderdoccmd"):
29
+ found = shutil.which(name)
30
+ if found:
31
+ return Path(found).resolve()
32
+ raise RenderDocError(
33
+ "renderdoccmd was not found; install RenderDoc or set DCC_MCP_RENDERDOC_CMD"
34
+ )
35
+
36
+
37
+ def _run(
38
+ arguments: Sequence[str],
39
+ *,
40
+ timeout_secs: int,
41
+ command: Optional[str] = None,
42
+ ) -> subprocess.CompletedProcess[str]:
43
+ executable = resolve_renderdoccmd(command)
44
+ try:
45
+ result = subprocess.run(
46
+ [str(executable), *arguments],
47
+ check=False,
48
+ capture_output=True,
49
+ text=True,
50
+ timeout=timeout_secs,
51
+ shell=False,
52
+ )
53
+ except subprocess.TimeoutExpired as exc:
54
+ raise RenderDocError(f"RenderDoc command timed out after {timeout_secs}s") from exc
55
+ except OSError as exc:
56
+ raise RenderDocError(f"Could not start RenderDoc: {exc}") from exc
57
+ if result.returncode != 0:
58
+ detail = (result.stderr or result.stdout or "unknown error").strip()[-2000:]
59
+ raise RenderDocError(f"RenderDoc exited with code {result.returncode}: {detail}")
60
+ return result
61
+
62
+
63
+ def get_version(*, command: Optional[str] = None) -> dict[str, Any]:
64
+ result = _run(["version"], timeout_secs=30, command=command)
65
+ output = "\n".join(part.strip() for part in (result.stdout, result.stderr) if part.strip())
66
+ return {"command": str(resolve_renderdoccmd(command)), "version_output": output}
67
+
68
+
69
+ def capture_program(
70
+ executable: str,
71
+ output_template: str,
72
+ *,
73
+ arguments: Optional[Sequence[str]] = None,
74
+ working_directory: Optional[str] = None,
75
+ wait_for_exit: bool = True,
76
+ api_validation: bool = False,
77
+ hook_children: bool = False,
78
+ timeout_secs: int = 300,
79
+ command: Optional[str] = None,
80
+ ) -> dict[str, Any]:
81
+ """Launch one explicit executable under RenderDoc and return new captures."""
82
+ target = Path(executable).expanduser().resolve()
83
+ if not target.is_file():
84
+ raise RenderDocError(f"Target executable does not exist: {target}")
85
+ output = Path(output_template).expanduser().resolve()
86
+ output.parent.mkdir(parents=True, exist_ok=True)
87
+ cwd = Path(working_directory).expanduser().resolve() if working_directory else target.parent
88
+ if not cwd.is_dir():
89
+ raise RenderDocError(f"Working directory does not exist: {cwd}")
90
+
91
+ before = {path.resolve() for path in output.parent.glob("*.rdc")}
92
+ cli_args = ["capture", "--working-dir", str(cwd), "--capture-file", str(output)]
93
+ if wait_for_exit:
94
+ cli_args.append("--wait-for-exit")
95
+ if api_validation:
96
+ cli_args.append("--opt-api-validation")
97
+ if hook_children:
98
+ cli_args.append("--opt-hook-children")
99
+ cli_args.extend([str(target), *(str(value) for value in arguments or [])])
100
+ result = _run(cli_args, timeout_secs=timeout_secs, command=command)
101
+
102
+ captures = sorted(
103
+ (path.resolve() for path in output.parent.glob("*.rdc") if path.resolve() not in before),
104
+ key=lambda path: path.stat().st_mtime_ns,
105
+ )
106
+ if not captures:
107
+ output_detail = "\n".join(
108
+ part.strip() for part in (result.stdout, result.stderr) if part.strip()
109
+ )[-2000:]
110
+ raise RenderDocError(
111
+ "The target exited without creating a new .rdc capture; ensure it presents a frame "
112
+ f"or uses RenderDoc's in-application capture API. RenderDoc output: {output_detail}"
113
+ )
114
+ return {
115
+ "target": str(target),
116
+ "captures": [str(path) for path in captures],
117
+ "stdout": result.stdout.strip(),
118
+ }
119
+
120
+
121
+ def _require_capture(capture_file: str) -> Path:
122
+ path = Path(capture_file).expanduser().resolve()
123
+ if not path.is_file() or path.suffix.lower() != ".rdc":
124
+ raise RenderDocError(f"RenderDoc capture does not exist or is not .rdc: {path}")
125
+ return path
126
+
127
+
128
+ def _child_text(parent: ET.Element, name: str) -> Optional[str]:
129
+ child = parent.find(name)
130
+ return child.text.strip() if child is not None and child.text else None
131
+
132
+
133
+ def parse_capture_xml(xml_file: str, *, representative_limit: int = 20) -> dict[str, Any]:
134
+ """Parse the stable high-level fields emitted by RenderDoc's XML converter."""
135
+ path = Path(xml_file)
136
+ try:
137
+ root = ET.parse(path).getroot()
138
+ except (ET.ParseError, OSError) as exc:
139
+ raise RenderDocError(f"Could not parse RenderDoc XML: {exc}") from exc
140
+ if root.tag != "rdc":
141
+ raise RenderDocError(f"Unexpected RenderDoc XML root: {root.tag}")
142
+ header = root.find("header")
143
+ chunks = root.find("chunks")
144
+ if header is None or chunks is None:
145
+ raise RenderDocError("RenderDoc XML is missing header or chunks")
146
+
147
+ driver = header.find("driver")
148
+ thumbnail = header.find("thumbnail")
149
+ names = [chunk.get("name") or "unnamed" for chunk in chunks.findall("chunk")]
150
+ counts = Counter(names)
151
+ return {
152
+ "driver": {
153
+ "id": driver.get("id") if driver is not None else None,
154
+ "name": driver.text.strip() if driver is not None and driver.text else None,
155
+ },
156
+ "machine_ident": _child_text(header, "machineIdent"),
157
+ "thumbnail": {
158
+ "width": int(thumbnail.get("width", "0")) if thumbnail is not None else 0,
159
+ "height": int(thumbnail.get("height", "0")) if thumbnail is not None else 0,
160
+ },
161
+ "chunk_version": chunks.get("version"),
162
+ "chunk_count": len(names),
163
+ "chunk_frequencies": [
164
+ {"name": name, "count": count} for name, count in counts.most_common(20)
165
+ ],
166
+ "representative_chunks": names[:representative_limit],
167
+ }
168
+
169
+
170
+ def inspect_capture(
171
+ capture_file: str,
172
+ *,
173
+ representative_limit: int = 20,
174
+ command: Optional[str] = None,
175
+ ) -> dict[str, Any]:
176
+ capture = _require_capture(capture_file)
177
+ with tempfile.TemporaryDirectory(prefix="dcc-mcp-renderdoc-") as directory:
178
+ xml_file = Path(directory) / "capture.xml"
179
+ _run(
180
+ [
181
+ "convert",
182
+ "--filename",
183
+ str(capture),
184
+ "--output",
185
+ str(xml_file),
186
+ "--convert-format",
187
+ "xml",
188
+ ],
189
+ timeout_secs=180,
190
+ command=command,
191
+ )
192
+ details = parse_capture_xml(str(xml_file), representative_limit=representative_limit)
193
+ return {"capture_file": str(capture), "size_bytes": capture.stat().st_size, **details}
194
+
195
+
196
+ def _prepare_output(output_file: str, expected_suffixes: Iterable[str]) -> Path:
197
+ path = Path(output_file).expanduser().resolve()
198
+ if path.suffix.lower() not in set(expected_suffixes):
199
+ choices = ", ".join(sorted(expected_suffixes))
200
+ raise RenderDocError(f"Output must use one of these extensions: {choices}")
201
+ path.parent.mkdir(parents=True, exist_ok=True)
202
+ return path
203
+
204
+
205
+ def export_thumbnail(
206
+ capture_file: str,
207
+ output_file: str,
208
+ *,
209
+ max_size: int = 0,
210
+ command: Optional[str] = None,
211
+ ) -> dict[str, Any]:
212
+ capture = _require_capture(capture_file)
213
+ output = _prepare_output(output_file, {".bmp", ".jpg", ".png", ".tga"})
214
+ _run(
215
+ ["thumb", "--out", str(output), "--max-size", str(max_size), str(capture)],
216
+ timeout_secs=120,
217
+ command=command,
218
+ )
219
+ if not output.is_file():
220
+ raise RenderDocError("RenderDoc reported success but did not create the thumbnail")
221
+ return {
222
+ "capture_file": str(capture),
223
+ "output_file": str(output),
224
+ "size_bytes": output.stat().st_size,
225
+ }
226
+
227
+
228
+ def export_timeline(
229
+ capture_file: str,
230
+ output_file: str,
231
+ *,
232
+ command: Optional[str] = None,
233
+ ) -> dict[str, Any]:
234
+ capture = _require_capture(capture_file)
235
+ output = _prepare_output(output_file, {".json"})
236
+ _run(
237
+ [
238
+ "convert",
239
+ "--filename",
240
+ str(capture),
241
+ "--output",
242
+ str(output),
243
+ "--convert-format",
244
+ "chrome.json",
245
+ ],
246
+ timeout_secs=180,
247
+ command=command,
248
+ )
249
+ if not output.is_file():
250
+ raise RenderDocError("RenderDoc reported success but did not create the timeline")
251
+ return {
252
+ "capture_file": str(capture),
253
+ "output_file": str(output),
254
+ "size_bytes": output.stat().st_size,
255
+ }
@@ -0,0 +1,78 @@
1
+ """Standalone RenderDoc MCP server lifecycle."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import signal
7
+ import sys
8
+ import threading
9
+ from pathlib import Path
10
+ from typing import Optional
11
+
12
+ from dcc_mcp_core import DccServerOptions
13
+ from dcc_mcp_core.server_base import DccServerBase
14
+
15
+ from .__version__ import __version__
16
+ from .runtime import RenderDocError, get_version
17
+
18
+ DEFAULT_PORT = 8765
19
+ _server: Optional["RenderDocMcpServer"] = None
20
+
21
+
22
+ class RenderDocMcpServer(DccServerBase):
23
+ """Headless DCC-MCP adapter backed by renderdoccmd."""
24
+
25
+ def __init__(self, port: int = DEFAULT_PORT) -> None:
26
+ os.environ.setdefault("DCC_MCP_PYTHON_EXECUTABLE", sys.executable)
27
+ options = DccServerOptions.from_env(
28
+ "renderdoc",
29
+ Path(__file__).resolve().parent / "skills",
30
+ port=port,
31
+ server_name="dcc-mcp-renderdoc",
32
+ server_version=__version__,
33
+ )
34
+ super().__init__(options=options)
35
+
36
+ def _version_string(self) -> str:
37
+ try:
38
+ return get_version()["version_output"]
39
+ except RenderDocError:
40
+ return "RenderDoc CLI unavailable"
41
+
42
+
43
+ def start_server(port: Optional[int] = None) -> RenderDocMcpServer:
44
+ global _server
45
+ if _server is None or not _server.is_running:
46
+ selected_port = (
47
+ port
48
+ if port is not None
49
+ else int(os.environ.get("DCC_MCP_RENDERDOC_PORT", DEFAULT_PORT))
50
+ )
51
+ _server = RenderDocMcpServer(selected_port)
52
+ _server.register_builtin_actions()
53
+ _server.start()
54
+ return _server
55
+
56
+
57
+ def stop_server() -> None:
58
+ global _server
59
+ if _server is not None:
60
+ _server.stop()
61
+ _server = None
62
+
63
+
64
+ def main() -> None:
65
+ """Run until interrupted."""
66
+ stopped = threading.Event()
67
+ signal.signal(signal.SIGINT, lambda *_: stopped.set())
68
+ if hasattr(signal, "SIGTERM"):
69
+ signal.signal(signal.SIGTERM, lambda *_: stopped.set())
70
+ start_server()
71
+ try:
72
+ stopped.wait()
73
+ finally:
74
+ stop_server()
75
+
76
+
77
+ if __name__ == "__main__":
78
+ main()
@@ -0,0 +1,25 @@
1
+ ---
2
+ name: renderdoc-analysis
3
+ description: >-
4
+ Domain skill — Inspect an existing RenderDoc capture and export its embedded thumbnail or Chrome
5
+ trace. Use for offline graphics triage and automation artifacts. Not for launching a capture —
6
+ use renderdoc-capture.
7
+ license: MIT
8
+ compatibility: "RenderDoc 1.45+; dcc-mcp-core 0.19+"
9
+ allowed-tools: "python"
10
+ metadata:
11
+ dcc-mcp:
12
+ dcc: renderdoc
13
+ layer: domain
14
+ version: "0.1.0"
15
+ search-hint: "RenderDoc inspect rdc chunks thumbnail Chrome trace graphics analysis"
16
+ tags: "renderdoc,analysis,thumbnail,timeline,graphics-debugging"
17
+ tools: tools.yaml
18
+ depends: "dcc-diagnostics"
19
+ ---
20
+
21
+ # RenderDoc Analysis
22
+
23
+ Inspect before exporting. These tools never modify the input `.rdc`; exports require an explicit
24
+ destination path and create its parent directory when needed.
25
+
@@ -0,0 +1,15 @@
1
+ from dcc_mcp_core.skill import skill_entry, skill_success
2
+
3
+ from dcc_mcp_renderdoc.runtime import export_thumbnail
4
+
5
+
6
+ @skill_entry
7
+ def main(capture_file: str, output_file: str, max_size: int = 0, **_kwargs):
8
+ result = export_thumbnail(capture_file, output_file, max_size=max_size)
9
+ return skill_success("RenderDoc thumbnail exported.", **result)
10
+
11
+
12
+ if __name__ == "__main__":
13
+ from dcc_mcp_core.skill import run_main
14
+
15
+ run_main(main)
@@ -0,0 +1,15 @@
1
+ from dcc_mcp_core.skill import skill_entry, skill_success
2
+
3
+ from dcc_mcp_renderdoc.runtime import export_timeline
4
+
5
+
6
+ @skill_entry
7
+ def main(capture_file: str, output_file: str, **_kwargs):
8
+ result = export_timeline(capture_file, output_file)
9
+ return skill_success("RenderDoc Chrome timeline exported.", **result)
10
+
11
+
12
+ if __name__ == "__main__":
13
+ from dcc_mcp_core.skill import run_main
14
+
15
+ run_main(main)
@@ -0,0 +1,15 @@
1
+ from dcc_mcp_core.skill import skill_entry, skill_success
2
+
3
+ from dcc_mcp_renderdoc.runtime import inspect_capture
4
+
5
+
6
+ @skill_entry
7
+ def main(capture_file: str, representative_limit: int = 20, **_kwargs):
8
+ result = inspect_capture(capture_file, representative_limit=representative_limit)
9
+ return skill_success("RenderDoc capture inspected.", **result)
10
+
11
+
12
+ if __name__ == "__main__":
13
+ from dcc_mcp_core.skill import run_main
14
+
15
+ run_main(main)
@@ -0,0 +1,63 @@
1
+ tools:
2
+ - name: inspect_capture
3
+ description: Convert one .rdc capture to temporary XML and summarize stable header and chunk metadata. Use for offline triage. Not for GPU performance conclusions or high-level drawcall reconstruction.
4
+ input_schema:
5
+ type: object
6
+ required: [capture_file]
7
+ properties:
8
+ capture_file: {type: string, minLength: 1, maxLength: 1000}
9
+ representative_limit: {type: integer, minimum: 0, maximum: 100, default: 20}
10
+ additionalProperties: false
11
+ output_schema: {type: object}
12
+ read_only: true
13
+ destructive: false
14
+ idempotent: true
15
+ execution: async
16
+ affinity: any
17
+ timeout_hint_secs: 180
18
+ source_file: scripts/inspect_capture.py
19
+ next-tools:
20
+ on-success: [renderdoc_analysis__export_thumbnail, renderdoc_analysis__export_timeline]
21
+ on-failure: [renderdoc_capture__get_version, dcc_diagnostics__audit_log]
22
+ - name: export_thumbnail
23
+ description: Export the embedded capture thumbnail as PNG, JPG, BMP, or TGA. Use for visual review artifacts. Not for replaying or rendering arbitrary capture resources.
24
+ input_schema:
25
+ type: object
26
+ required: [capture_file, output_file]
27
+ properties:
28
+ capture_file: {type: string, minLength: 1, maxLength: 1000}
29
+ output_file: {type: string, minLength: 1, maxLength: 1000}
30
+ max_size: {type: integer, minimum: 0, maximum: 16384, default: 0}
31
+ additionalProperties: false
32
+ output_schema: {type: object}
33
+ read_only: false
34
+ destructive: false
35
+ idempotent: true
36
+ execution: async
37
+ affinity: any
38
+ timeout_hint_secs: 120
39
+ source_file: scripts/export_thumbnail.py
40
+ next-tools:
41
+ on-success: [renderdoc_analysis__inspect_capture]
42
+ on-failure: [renderdoc_capture__get_version, dcc_diagnostics__audit_log]
43
+ - name: export_timeline
44
+ description: Export capture events as Chrome profiler JSON. Use for timeline tooling and CI artifacts. Not for authoritative GPU duration measurement.
45
+ input_schema:
46
+ type: object
47
+ required: [capture_file, output_file]
48
+ properties:
49
+ capture_file: {type: string, minLength: 1, maxLength: 1000}
50
+ output_file: {type: string, minLength: 1, maxLength: 1000}
51
+ additionalProperties: false
52
+ output_schema: {type: object}
53
+ read_only: false
54
+ destructive: false
55
+ idempotent: true
56
+ execution: async
57
+ affinity: any
58
+ timeout_hint_secs: 180
59
+ source_file: scripts/export_timeline.py
60
+ next-tools:
61
+ on-success: [renderdoc_analysis__inspect_capture]
62
+ on-failure: [renderdoc_capture__get_version, dcc_diagnostics__audit_log]
63
+
@@ -0,0 +1,25 @@
1
+ ---
2
+ name: renderdoc-capture
3
+ description: >-
4
+ Domain skill — Discover RenderDoc and launch an explicit game or graphics executable under
5
+ capture. Use for repeatable local or CI frame capture. Not for reading an existing capture —
6
+ use renderdoc-analysis.
7
+ license: MIT
8
+ compatibility: "RenderDoc 1.45+; dcc-mcp-core 0.19+"
9
+ allowed-tools: "python"
10
+ metadata:
11
+ dcc-mcp:
12
+ dcc: renderdoc
13
+ layer: domain
14
+ version: "0.1.0"
15
+ search-hint: "RenderDoc capture launch executable game graphics frame rdc"
16
+ tags: "renderdoc,capture,graphics-debugging,game-development"
17
+ tools: tools.yaml
18
+ depends: "dcc-diagnostics"
19
+ ---
20
+
21
+ # RenderDoc Capture
22
+
23
+ Call `get_version` before capture. `capture_program` starts only the explicit executable and
24
+ arguments, never a shell. The target must trigger a RenderDoc frame capture.
25
+
@@ -0,0 +1,34 @@
1
+ from dcc_mcp_core.skill import skill_entry, skill_success
2
+
3
+ from dcc_mcp_renderdoc.runtime import capture_program
4
+
5
+
6
+ @skill_entry
7
+ def main(
8
+ executable: str,
9
+ output_template: str,
10
+ arguments=None,
11
+ working_directory=None,
12
+ wait_for_exit: bool = True,
13
+ api_validation: bool = False,
14
+ hook_children: bool = False,
15
+ timeout_secs: int = 300,
16
+ **_kwargs,
17
+ ):
18
+ result = capture_program(
19
+ executable,
20
+ output_template,
21
+ arguments=arguments,
22
+ working_directory=working_directory,
23
+ wait_for_exit=wait_for_exit,
24
+ api_validation=api_validation,
25
+ hook_children=hook_children,
26
+ timeout_secs=timeout_secs,
27
+ )
28
+ return skill_success(f"Created {len(result['captures'])} RenderDoc capture(s).", **result)
29
+
30
+
31
+ if __name__ == "__main__":
32
+ from dcc_mcp_core.skill import run_main
33
+
34
+ run_main(main)
@@ -0,0 +1,14 @@
1
+ from dcc_mcp_core.skill import skill_entry, skill_success
2
+
3
+ from dcc_mcp_renderdoc.runtime import get_version
4
+
5
+
6
+ @skill_entry
7
+ def main(**_kwargs):
8
+ return skill_success("RenderDoc CLI is available.", **get_version())
9
+
10
+
11
+ if __name__ == "__main__":
12
+ from dcc_mcp_core.skill import run_main
13
+
14
+ run_main(main)
@@ -0,0 +1,45 @@
1
+ tools:
2
+ - name: get_version
3
+ description: Resolve renderdoccmd and return its version output. Use before capture or analysis to verify the runtime. Not for capture-file inspection.
4
+ input_schema: {type: object, properties: {}, additionalProperties: false}
5
+ output_schema: {type: object}
6
+ read_only: true
7
+ destructive: false
8
+ idempotent: true
9
+ execution: sync
10
+ affinity: any
11
+ timeout_hint_secs: 30
12
+ source_file: scripts/get_version.py
13
+ next-tools:
14
+ on-success: [renderdoc_capture__capture_program]
15
+ on-failure: [dcc_diagnostics__audit_log]
16
+ - name: capture_program
17
+ description: Launch one explicit executable under RenderDoc and return newly created .rdc files. Use for controlled game or graphics test capture. Not for shell commands or attaching to an existing process.
18
+ input_schema:
19
+ type: object
20
+ required: [executable, output_template]
21
+ properties:
22
+ executable: {type: string, minLength: 1, maxLength: 1000}
23
+ output_template: {type: string, minLength: 1, maxLength: 1000}
24
+ arguments:
25
+ type: array
26
+ maxItems: 100
27
+ items: {type: string, maxLength: 2000}
28
+ working_directory: {type: string, maxLength: 1000}
29
+ wait_for_exit: {type: boolean, default: true}
30
+ api_validation: {type: boolean, default: false}
31
+ hook_children: {type: boolean, default: false}
32
+ timeout_secs: {type: integer, minimum: 1, maximum: 3600, default: 300}
33
+ additionalProperties: false
34
+ output_schema: {type: object}
35
+ read_only: false
36
+ destructive: false
37
+ idempotent: false
38
+ execution: async
39
+ affinity: any
40
+ timeout_hint_secs: 3600
41
+ source_file: scripts/capture_program.py
42
+ next-tools:
43
+ on-success: [renderdoc_analysis__inspect_capture, renderdoc_analysis__export_thumbnail]
44
+ on-failure: [dcc_diagnostics__audit_log]
45
+
@@ -0,0 +1,142 @@
1
+ """Capture and inspect one real OpenGL frame through the MCP endpoint."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import os
8
+ import time
9
+ import urllib.request
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from dcc_mcp_renderdoc.server import RenderDocMcpServer
14
+
15
+
16
+ def _post(url: str, method: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
17
+ payload: dict[str, Any] = {"jsonrpc": "2.0", "id": 1, "method": method}
18
+ if params is not None:
19
+ payload["params"] = params
20
+ request = urllib.request.Request(
21
+ url,
22
+ data=json.dumps(payload).encode(),
23
+ headers={
24
+ "Accept": "application/json, text/event-stream",
25
+ "Content-Type": "application/json",
26
+ },
27
+ )
28
+ with urllib.request.urlopen(request, timeout=60) as response:
29
+ return json.loads(response.read())
30
+
31
+
32
+ def _content(response: dict[str, Any]) -> dict[str, Any]:
33
+ if response.get("error") or response.get("result", {}).get("isError"):
34
+ raise RuntimeError(json.dumps(response, indent=2))
35
+ result = response.get("result", {})
36
+ structured = result.get("structuredContent")
37
+ if isinstance(structured, dict):
38
+ return structured
39
+ if result.get("content"):
40
+ return json.loads(result["content"][0]["text"])
41
+ return result
42
+
43
+
44
+ def _call(url: str, name: str, arguments: dict[str, Any] | None = None) -> dict[str, Any]:
45
+ return _content(_post(url, "tools/call", {"name": name, "arguments": arguments or {}}))
46
+
47
+
48
+ def _wait(url: str, envelope: dict[str, Any]) -> dict[str, Any]:
49
+ job_id = envelope.get("job_id")
50
+ if not job_id:
51
+ return envelope
52
+ deadline = time.monotonic() + 300
53
+ while time.monotonic() < deadline:
54
+ status = _call(url, "jobs_get_status", {"job_id": job_id, "include_result": True})
55
+ if status.get("status") == "completed":
56
+ return status
57
+ if status.get("status") in {"failed", "cancelled", "interrupted"}:
58
+ raise RuntimeError(json.dumps(status, indent=2))
59
+ time.sleep(0.1)
60
+ raise TimeoutError(f"RenderDoc MCP job {job_id} did not finish")
61
+
62
+
63
+ def _tool(url: str, suffix: str) -> str:
64
+ names: list[str] = []
65
+ cursor = None
66
+ for _ in range(20):
67
+ response = _post(url, "tools/list", {"cursor": cursor} if cursor else None)
68
+ result = response.get("result", {})
69
+ names.extend(tool["name"] for tool in result.get("tools", []))
70
+ cursor = result.get("nextCursor")
71
+ if not cursor:
72
+ break
73
+ matches = [name for name in names if name == suffix or name.endswith(f"__{suffix}")]
74
+ if len(matches) != 1:
75
+ raise RuntimeError(f"Expected one tool ending in {suffix!r}, found {matches!r}")
76
+ return matches[0]
77
+
78
+
79
+ def main() -> None:
80
+ parser = argparse.ArgumentParser()
81
+ parser.add_argument("--renderdoc", required=True)
82
+ parser.add_argument("--program", required=True)
83
+ parser.add_argument("--artifacts", type=Path, default=Path("artifacts"))
84
+ args = parser.parse_args()
85
+
86
+ artifacts = args.artifacts.resolve()
87
+ artifacts.mkdir(parents=True, exist_ok=True)
88
+ os.environ["DCC_MCP_RENDERDOC_CMD"] = str(Path(args.renderdoc).resolve())
89
+ program = str(Path(args.program).resolve())
90
+
91
+ server = RenderDocMcpServer(port=0)
92
+ try:
93
+ server.register_builtin_actions()
94
+ server.start(install_atexit_hook=False)
95
+ url = server.mcp_url
96
+ _call(url, "load_skill", {"skill_name": "renderdoc-capture"})
97
+ _call(url, "load_skill", {"skill_name": "renderdoc-analysis"})
98
+ version = _wait(url, _call(url, _tool(url, "get_version")))
99
+ capture_job = _call(
100
+ url,
101
+ _tool(url, "capture_program"),
102
+ {
103
+ "executable": program,
104
+ "output_template": str(artifacts / "smoke"),
105
+ "arguments": [str(artifacts / "smoke")],
106
+ "working_directory": str(Path(program).parent),
107
+ },
108
+ )
109
+ _wait(url, capture_job)
110
+ captures = sorted(artifacts.glob("*.rdc"))
111
+ if not captures:
112
+ raise RuntimeError("MCP capture tool did not create an .rdc file")
113
+ capture = str(captures[-1])
114
+ inspection = _wait(
115
+ url,
116
+ _call(url, _tool(url, "inspect_capture"), {"capture_file": capture}),
117
+ )
118
+ _wait(
119
+ url,
120
+ _call(
121
+ url,
122
+ _tool(url, "export_thumbnail"),
123
+ {"capture_file": capture, "output_file": str(artifacts / "smoke.png")},
124
+ ),
125
+ )
126
+ _wait(
127
+ url,
128
+ _call(
129
+ url,
130
+ _tool(url, "export_timeline"),
131
+ {"capture_file": capture, "output_file": str(artifacts / "smoke.json")},
132
+ ),
133
+ )
134
+ assert (artifacts / "smoke.png").stat().st_size > 0
135
+ assert (artifacts / "smoke.json").stat().st_size > 0
136
+ print(json.dumps({"version": version, "inspection_job": inspection}))
137
+ finally:
138
+ server.stop()
139
+
140
+
141
+ if __name__ == "__main__":
142
+ main()
@@ -0,0 +1,71 @@
1
+ #include <GL/gl.h>
2
+ #include <GL/glx.h>
3
+ #include <X11/Xlib.h>
4
+ #include <dlfcn.h>
5
+ #include <stdbool.h>
6
+ #include <stdio.h>
7
+ #include <unistd.h>
8
+
9
+ #include "renderdoc_app.h"
10
+
11
+ #define GLX_CONTEXT_MAJOR_VERSION_ARB 0x2091
12
+ #define GLX_CONTEXT_MINOR_VERSION_ARB 0x2092
13
+ #define GLX_CONTEXT_PROFILE_MASK_ARB 0x9126
14
+ #define GLX_CONTEXT_CORE_PROFILE_BIT_ARB 0x00000001
15
+
16
+ typedef GLXContext (*CreateContextAttribs)(Display *, GLXFBConfig, GLXContext, Bool, const int *);
17
+
18
+ int main(int argc, char **argv) {
19
+ Display *display = XOpenDisplay(NULL);
20
+ int count = 0;
21
+ int attributes[] = {GLX_X_RENDERABLE, True, GLX_DRAWABLE_TYPE, GLX_WINDOW_BIT,
22
+ GLX_RENDER_TYPE, GLX_RGBA_BIT, GLX_DOUBLEBUFFER, True, None};
23
+ GLXFBConfig *configs = display ? glXChooseFBConfig(display, DefaultScreen(display), attributes, &count) : NULL;
24
+ XVisualInfo *visual = count > 0 ? glXGetVisualFromFBConfig(display, configs[0]) : NULL;
25
+ CreateContextAttribs create_context =
26
+ (CreateContextAttribs)glXGetProcAddressARB((const GLubyte *)"glXCreateContextAttribsARB");
27
+ if (!display || !visual || !create_context) return 2;
28
+
29
+ Window root = RootWindow(display, visual->screen);
30
+ Colormap color_map = XCreateColormap(display, root, visual->visual, AllocNone);
31
+ XSetWindowAttributes window_attributes = {0};
32
+ window_attributes.colormap = color_map;
33
+ window_attributes.event_mask = ExposureMask;
34
+ Window window = XCreateWindow(display, root, 0, 0, 320, 200, 0, visual->depth, InputOutput,
35
+ visual->visual, CWColormap | CWEventMask, &window_attributes);
36
+ int context_attributes[] = {GLX_CONTEXT_MAJOR_VERSION_ARB, 3, GLX_CONTEXT_MINOR_VERSION_ARB, 2,
37
+ GLX_CONTEXT_PROFILE_MASK_ARB, GLX_CONTEXT_CORE_PROFILE_BIT_ARB, None};
38
+ GLXContext context = create_context(display, configs[0], NULL, True, context_attributes);
39
+ XFree(configs);
40
+ if (!context) return 2;
41
+ XMapWindow(display, window);
42
+ glXMakeCurrent(display, window, context);
43
+
44
+ pRENDERDOC_GetAPI get_api = (pRENDERDOC_GetAPI)dlsym(RTLD_DEFAULT, "RENDERDOC_GetAPI");
45
+ RENDERDOC_API_1_1_2 *api = NULL;
46
+ if (!get_api || !get_api(eRENDERDOC_API_Version_1_1_2, (void **)&api)) return 3;
47
+ if (argc > 1) api->SetCaptureFilePathTemplate(argv[1]);
48
+
49
+ glViewport(0, 0, 320, 200);
50
+ glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
51
+ glClear(GL_COLOR_BUFFER_BIT);
52
+ glXSwapBuffers(display, window);
53
+ glFinish();
54
+
55
+ api->StartFrameCapture(NULL, NULL);
56
+ if (!api->IsFrameCapturing()) return 4;
57
+ glViewport(0, 0, 320, 200);
58
+ glClearColor(0.1f, 0.4f, 0.8f, 1.0f);
59
+ glClear(GL_COLOR_BUFFER_BIT);
60
+ glXSwapBuffers(display, window);
61
+ glFinish();
62
+ if (!api->EndFrameCapture(NULL, NULL)) return 5;
63
+ usleep(200000);
64
+
65
+ glXMakeCurrent(display, None, NULL);
66
+ glXDestroyContext(display, context);
67
+ XDestroyWindow(display, window);
68
+ XCloseDisplay(display);
69
+ printf("captured=%u\n", api->GetNumCaptures());
70
+ return 0;
71
+ }
@@ -0,0 +1,15 @@
1
+ from pathlib import Path
2
+
3
+ import pytest
4
+
5
+ from tools.download_latest_renderdoc import _safe_destination, latest_version
6
+
7
+
8
+ def test_latest_version_uses_numeric_ordering():
9
+ html = "/stable/1.9/renderdoc_1.9.tar.gz /stable/1.45/renderdoc_1.45.tar.gz"
10
+ assert latest_version(html) == "1.45"
11
+
12
+
13
+ def test_archive_member_must_remain_below_destination(tmp_path: Path):
14
+ with pytest.raises(RuntimeError, match="escapes destination"):
15
+ _safe_destination(tmp_path, "../outside")
@@ -0,0 +1,22 @@
1
+ from pathlib import Path
2
+
3
+ from dcc_mcp_renderdoc.server import RenderDocMcpServer
4
+
5
+
6
+ def test_server_constructs_with_headless_contract():
7
+ server = RenderDocMcpServer(port=0)
8
+ try:
9
+ assert server._options.server_name == "dcc-mcp-renderdoc"
10
+ assert server._options.dcc_name == "renderdoc"
11
+ finally:
12
+ server.stop()
13
+
14
+
15
+ def test_bundled_skills_and_release_workflow_exist():
16
+ root = Path(__file__).parents[1]
17
+ skills = root / "src" / "dcc_mcp_renderdoc" / "skills"
18
+ assert {path.name for path in skills.iterdir() if path.is_dir()} == {
19
+ "renderdoc-analysis",
20
+ "renderdoc-capture",
21
+ }
22
+ assert (root / ".github" / "workflows" / "release.yml").is_file()
@@ -0,0 +1,57 @@
1
+ from pathlib import Path
2
+ from subprocess import CompletedProcess
3
+
4
+ import pytest
5
+
6
+ from dcc_mcp_renderdoc import runtime
7
+
8
+
9
+ def test_parse_capture_xml_summarizes_stable_fields(tmp_path: Path):
10
+ xml_file = tmp_path / "capture.xml"
11
+ xml_file.write_text(
12
+ """<rdc><header><driver id="2">Vulkan</driver><machineIdent>123</machineIdent>
13
+ <thumbnail width="640" height="360" /></header><chunks version="17">
14
+ <chunk name="vkCmdDraw"/><chunk name="vkCmdDraw"/><chunk name="Present"/>
15
+ </chunks></rdc>""",
16
+ encoding="utf-8",
17
+ )
18
+
19
+ result = runtime.parse_capture_xml(str(xml_file), representative_limit=2)
20
+
21
+ assert result["driver"] == {"id": "2", "name": "Vulkan"}
22
+ assert result["thumbnail"] == {"width": 640, "height": 360}
23
+ assert result["chunk_count"] == 3
24
+ assert result["chunk_frequencies"][0] == {"name": "vkCmdDraw", "count": 2}
25
+ assert result["representative_chunks"] == ["vkCmdDraw", "vkCmdDraw"]
26
+
27
+
28
+ def test_resolve_renderdoccmd_prefers_explicit_file(tmp_path: Path):
29
+ executable = tmp_path / "renderdoccmd"
30
+ executable.touch()
31
+ assert runtime.resolve_renderdoccmd(str(executable)) == executable.resolve()
32
+
33
+
34
+ def test_capture_program_uses_argument_vector_and_reports_new_capture(tmp_path, monkeypatch):
35
+ target = tmp_path / "game.exe"
36
+ target.touch()
37
+ observed = {}
38
+
39
+ def fake_run(arguments, **_kwargs):
40
+ observed["arguments"] = arguments
41
+ (tmp_path / "capture_frame1.rdc").touch()
42
+ return CompletedProcess(arguments, 0, "captured", "")
43
+
44
+ monkeypatch.setattr(runtime, "_run", fake_run)
45
+ result = runtime.capture_program(
46
+ str(target),
47
+ str(tmp_path / "capture"),
48
+ arguments=["--scene", "demo"],
49
+ )
50
+
51
+ assert observed["arguments"][-3:] == [str(target.resolve()), "--scene", "demo"]
52
+ assert result["captures"] == [str((tmp_path / "capture_frame1.rdc").resolve())]
53
+
54
+
55
+ def test_invalid_capture_is_rejected(tmp_path: Path):
56
+ with pytest.raises(runtime.RenderDocError, match="not .rdc"):
57
+ runtime.inspect_capture(str(tmp_path / "missing.rdc"))
@@ -0,0 +1 @@
1
+ """Repository tooling helpers."""
@@ -0,0 +1,89 @@
1
+ """Download the latest stable RenderDoc bundle from the official builds page."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import re
7
+ import shutil
8
+ import sys
9
+ import tarfile
10
+ import urllib.request
11
+ import zipfile
12
+ from pathlib import Path
13
+
14
+ BUILDS_URL = "https://renderdoc.org/builds"
15
+
16
+
17
+ def latest_version(html: str) -> str:
18
+ versions = set(re.findall(r"/stable/([0-9]+\.[0-9]+)/", html))
19
+ if not versions:
20
+ raise RuntimeError("No stable RenderDoc versions found on the official builds page")
21
+ return max(versions, key=lambda value: tuple(int(part) for part in value.split(".")))
22
+
23
+
24
+ def _safe_destination(root: Path, member_name: str) -> Path:
25
+ destination = (root / member_name).resolve()
26
+ try:
27
+ destination.relative_to(root.resolve())
28
+ except ValueError as exc:
29
+ raise RuntimeError(f"Archive member escapes destination: {member_name}") from exc
30
+ return destination
31
+
32
+
33
+ def extract_archive(archive: Path, destination: Path) -> None:
34
+ destination.mkdir(parents=True, exist_ok=True)
35
+ if zipfile.is_zipfile(archive):
36
+ with zipfile.ZipFile(archive) as bundle:
37
+ for member in bundle.infolist():
38
+ _safe_destination(destination, member.filename)
39
+ bundle.extractall(destination)
40
+ return
41
+ with tarfile.open(archive, "r:gz") as bundle:
42
+ for member in bundle.getmembers():
43
+ _safe_destination(destination, member.name)
44
+ if member.issym() or member.islnk():
45
+ raise RuntimeError(f"Archive links are not accepted: {member.name}")
46
+ bundle.extractall(destination)
47
+
48
+
49
+ def download_latest(output: Path, platform: str = sys.platform) -> Path:
50
+ request = urllib.request.Request(BUILDS_URL, headers={"User-Agent": "dcc-mcp-renderdoc"})
51
+ with urllib.request.urlopen(request, timeout=30) as response:
52
+ version = latest_version(response.read().decode("utf-8"))
53
+ if platform == "win32":
54
+ archive_name = f"RenderDoc_{version}_64.zip"
55
+ elif platform.startswith("linux"):
56
+ archive_name = f"renderdoc_{version}.tar.gz"
57
+ else:
58
+ raise RuntimeError(f"RenderDoc has no supported desktop bundle for {platform}")
59
+
60
+ version_dir = output.resolve() / version
61
+ command_name = "renderdoccmd.exe" if platform == "win32" else "renderdoccmd"
62
+ existing = next(version_dir.rglob(command_name), None) if version_dir.exists() else None
63
+ if existing:
64
+ return existing.resolve()
65
+
66
+ output.mkdir(parents=True, exist_ok=True)
67
+ archive = output / archive_name
68
+ url = f"https://renderdoc.org/stable/{version}/{archive_name}"
69
+ with urllib.request.urlopen(url, timeout=120) as response, archive.open("wb") as stream:
70
+ shutil.copyfileobj(response, stream)
71
+ extract_archive(archive, version_dir)
72
+ archive.unlink()
73
+ command = next(version_dir.rglob(command_name), None)
74
+ if command is None:
75
+ raise RuntimeError(f"Downloaded bundle did not contain {command_name}")
76
+ if platform.startswith("linux"):
77
+ command.chmod(command.stat().st_mode | 0o111)
78
+ return command.resolve()
79
+
80
+
81
+ def main() -> None:
82
+ parser = argparse.ArgumentParser()
83
+ parser.add_argument("--output", type=Path, default=Path(".renderdoc-bin"))
84
+ args = parser.parse_args()
85
+ print(download_latest(args.output))
86
+
87
+
88
+ if __name__ == "__main__":
89
+ main()
@@ -0,0 +1,8 @@
1
+ from pathlib import Path
2
+
3
+ from dcc_mcp_core import validate_skill
4
+
5
+ skills_root = Path(__file__).parents[1] / "src" / "dcc_mcp_renderdoc" / "skills"
6
+ reports = [validate_skill(str(path)) for path in skills_root.iterdir() if path.is_dir()]
7
+ assert all(report.is_clean for report in reports), [report.issues for report in reports]
8
+ print(f"validated {len(reports)} bundled skills")