dcc-mcp-renderdoc 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dcc_mcp_renderdoc/__init__.py +6 -0
- dcc_mcp_renderdoc/__version__.py +3 -0
- dcc_mcp_renderdoc/runtime.py +255 -0
- dcc_mcp_renderdoc/server.py +78 -0
- dcc_mcp_renderdoc/skills/renderdoc-analysis/SKILL.md +25 -0
- dcc_mcp_renderdoc/skills/renderdoc-analysis/metadata/depends.md +2 -0
- dcc_mcp_renderdoc/skills/renderdoc-analysis/scripts/export_thumbnail.py +15 -0
- dcc_mcp_renderdoc/skills/renderdoc-analysis/scripts/export_timeline.py +15 -0
- dcc_mcp_renderdoc/skills/renderdoc-analysis/scripts/inspect_capture.py +15 -0
- dcc_mcp_renderdoc/skills/renderdoc-analysis/tools.yaml +63 -0
- dcc_mcp_renderdoc/skills/renderdoc-capture/SKILL.md +25 -0
- dcc_mcp_renderdoc/skills/renderdoc-capture/metadata/depends.md +2 -0
- dcc_mcp_renderdoc/skills/renderdoc-capture/scripts/capture_program.py +34 -0
- dcc_mcp_renderdoc/skills/renderdoc-capture/scripts/get_version.py +14 -0
- dcc_mcp_renderdoc/skills/renderdoc-capture/tools.yaml +45 -0
- dcc_mcp_renderdoc-0.2.0.dist-info/METADATA +79 -0
- dcc_mcp_renderdoc-0.2.0.dist-info/RECORD +20 -0
- dcc_mcp_renderdoc-0.2.0.dist-info/WHEEL +4 -0
- dcc_mcp_renderdoc-0.2.0.dist-info/entry_points.txt +5 -0
- dcc_mcp_renderdoc-0.2.0.dist-info/licenses/LICENSE +22 -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,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,20 @@
|
|
|
1
|
+
dcc_mcp_renderdoc/__init__.py,sha256=PtvTUY4_RsBHMB5Vlh8dc-JruF3l_jD1B0rhmVrDs8Y,163
|
|
2
|
+
dcc_mcp_renderdoc/__version__.py,sha256=VDsBswzfU7gNsBkIaxxV6tsi0ixlms3WjN7HYgZJZ2g,46
|
|
3
|
+
dcc_mcp_renderdoc/runtime.py,sha256=G4N-fJ4QaOYkbfmKcYJsSopOLPsMGAsk4tWvMs9sezk,9135
|
|
4
|
+
dcc_mcp_renderdoc/server.py,sha256=pts-cHn_wyr64cDticPOIfhs2Dgs5DAp62TSPwvO-YM,2075
|
|
5
|
+
dcc_mcp_renderdoc/skills/renderdoc-analysis/SKILL.md,sha256=isfpOP96bGnJbanUKVfsK-sYIGO14sLRNSOFz6mz_Po,837
|
|
6
|
+
dcc_mcp_renderdoc/skills/renderdoc-analysis/tools.yaml,sha256=9UvHc9_oPv7Vsgo9EUun50v_0Uw26-Hasxm-o8WjIqA,2623
|
|
7
|
+
dcc_mcp_renderdoc/skills/renderdoc-analysis/metadata/depends.md,sha256=oPXRkRwg5Q0bNsyQbM15Zo0kW0kf2HngdhKNVoM61f4,19
|
|
8
|
+
dcc_mcp_renderdoc/skills/renderdoc-analysis/scripts/export_thumbnail.py,sha256=j8Gm5QCZPpD-6kCuROkCnwwFuqmmF9-DxDon8ieVi_s,443
|
|
9
|
+
dcc_mcp_renderdoc/skills/renderdoc-analysis/scripts/export_timeline.py,sha256=RIgRXH3ZDt0TbM43bpq-pYZnqHPpxjuKRuI2jBh_L8E,409
|
|
10
|
+
dcc_mcp_renderdoc/skills/renderdoc-analysis/scripts/inspect_capture.py,sha256=-20dNoM7DoD0_B-CENLscipjYAdv30LGMfZEbdbtAHc,446
|
|
11
|
+
dcc_mcp_renderdoc/skills/renderdoc-capture/SKILL.md,sha256=W9u3CxjCXBNRH_JWLD0Roj9iRo_WR_z_zLD-AGsOD0E,826
|
|
12
|
+
dcc_mcp_renderdoc/skills/renderdoc-capture/tools.yaml,sha256=vYyif3bExXaG6foVsr_stUCW3OSmOay0hN0bAqKhxc8,1878
|
|
13
|
+
dcc_mcp_renderdoc/skills/renderdoc-capture/metadata/depends.md,sha256=oPXRkRwg5Q0bNsyQbM15Zo0kW0kf2HngdhKNVoM61f4,19
|
|
14
|
+
dcc_mcp_renderdoc/skills/renderdoc-capture/scripts/capture_program.py,sha256=ny4KUNouJD7fUYhh8PWDEoP_Npr4lezcZx8aZQ3Blpw,870
|
|
15
|
+
dcc_mcp_renderdoc/skills/renderdoc-capture/scripts/get_version.py,sha256=qog2c4GEykHZDTvWMGa7s8KxgZ1Skez9qek1TTytogs,311
|
|
16
|
+
dcc_mcp_renderdoc-0.2.0.dist-info/METADATA,sha256=5erLYp2CAce_dVmIJMV8O6W1McRxgdPyw4kIour8K3k,2873
|
|
17
|
+
dcc_mcp_renderdoc-0.2.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
18
|
+
dcc_mcp_renderdoc-0.2.0.dist-info/entry_points.txt,sha256=R0g0vPcVzarQHA-R65uVHu_R8xU0s4JRVaJ9r-aktIs,137
|
|
19
|
+
dcc_mcp_renderdoc-0.2.0.dist-info/licenses/LICENSE,sha256=tI9ZnJarjIkjC-Mje0HidFJgMMAhrK9qV-JdFSshBZw,1066
|
|
20
|
+
dcc_mcp_renderdoc-0.2.0.dist-info/RECORD,,
|
|
@@ -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
|
+
|