davinci-resolve-mcp 2.67.1 → 2.68.1

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,270 @@
1
+ """Resolve Scripts-menu capability probe: what does THIS edition's API expose?
2
+
3
+ Run from **Workspace ▸ Scripts ▸ resolve_capability_probe**, on whichever
4
+ edition you want to characterise.
5
+
6
+ The free edition gates *features* (noise reduction, Magic Mask, Super Scale,
7
+ some codecs, some resolutions). What it does to the *scripting API* is a
8
+ separate question and is not documented anywhere authoritative: a method may be
9
+ absent, or present and always return False, or present and work. Those three
10
+ cases need completely different handling in a connector, and guessing which is
11
+ which is how you ship a tool that lies about what it did.
12
+
13
+ So this asks the running application, on the edition in front of you.
14
+
15
+ **Strictly read-only.** It enumerates methods, calls only getters, and creates,
16
+ modifies and deletes nothing. Safe to run on a real project — though it reads
17
+ whatever project is open, so use a scratch one if you would rather not have your
18
+ timeline names in the report.
19
+
20
+ Writes to ``~/.config/davinci-resolve-mcp/capability-probe-<edition>.json`` and
21
+ prints a summary. Self-contained: no repository imports.
22
+ """
23
+
24
+ import json
25
+ import os
26
+ import sys
27
+ import time
28
+
29
+ REPORT_DIR = os.path.expanduser("~/.config/davinci-resolve-mcp")
30
+
31
+ #: Methods worth knowing about, grouped by the object they live on. Chosen for
32
+ #: what a connector actually needs, not for completeness.
33
+ SURFACE = {
34
+ "Resolve": [
35
+ "GetProductName", "GetVersionString", "GetCurrentPage", "OpenPage",
36
+ "GetProjectManager", "GetMediaStorage", "ImportRenderPreset",
37
+ "ExportRenderPreset", "ImportBurnInPreset", "Quit",
38
+ ],
39
+ "ProjectManager": [
40
+ "GetCurrentProject", "GetProjectListInCurrentFolder", "GetCurrentFolder",
41
+ "GetCurrentDatabase", "GetDatabaseList", "CreateProject", "LoadProject",
42
+ "SaveProject", "CloseProject", "DeleteProject", "ExportProject",
43
+ "ImportProject", "CreateFolder", "ArchiveProject",
44
+ ],
45
+ "Project": [
46
+ "GetName", "GetTimelineCount", "GetTimelineByIndex", "GetCurrentTimeline",
47
+ "SetCurrentTimeline", "GetMediaPool", "GetGallery", "GetSetting",
48
+ "SetSetting", "GetRenderFormats", "GetRenderCodecs",
49
+ "GetCurrentRenderFormatAndCodec", "GetRenderPresetList", "AddRenderJob",
50
+ "StartRendering", "IsRenderingInProgress", "GetRenderJobList",
51
+ "GetQuickExportRenderPresets", "RefreshLUTList", "GetUniqueId",
52
+ ],
53
+ "MediaPool": [
54
+ "GetRootFolder", "GetCurrentFolder", "AddSubFolder", "ImportMedia",
55
+ "CreateEmptyTimeline", "CreateTimelineFromClips", "AppendToTimeline",
56
+ "DeleteTimelines", "DeleteClips", "ImportTimelineFromFile",
57
+ "GetTimelineMatte", "RefreshFolders",
58
+ ],
59
+ "Timeline": [
60
+ "GetName", "SetName", "GetStartFrame", "GetEndFrame", "GetTrackCount",
61
+ "GetItemListInTrack", "GetTrackName", "GetIsTrackEnabled",
62
+ "AddTrack", "DeleteTrack", "AddMarker", "GetMarkers",
63
+ "DuplicateTimeline", "Export", "GetCurrentTimecode", "SetCurrentTimecode",
64
+ "InsertFusionTitleIntoTimeline", "InsertTitleIntoTimeline",
65
+ "CreateSubtitlesFromAudio", "DetectSceneCuts", "GetSetting",
66
+ ],
67
+ "TimelineItem": [
68
+ "GetName", "GetDuration", "GetStart", "GetEnd", "GetProperty",
69
+ "SetProperty", "GetNumNodes", "SetLUT", "GetLUT", "SetCDL",
70
+ "AddMarker", "GetMarkers", "GetFusionCompCount", "AddFusionComp",
71
+ "SetClipEnabled", "GetMediaPoolItem", "CreateMagicMask",
72
+ "SmartReframe", "GetNodeLabel",
73
+ ],
74
+ "MediaPoolItem": [
75
+ "GetName", "GetClipProperty", "SetClipProperty", "GetMetadata",
76
+ "SetMetadata", "GetMediaId", "AddMarker", "GetMarkers", "LinkProxyMedia",
77
+ "TranscribeAudio", "ClearTranscription",
78
+ ],
79
+ }
80
+
81
+ #: Getters that are safe to actually call, to separate "method exists" from
82
+ #: "method works". A method can be present and still be inert on this edition.
83
+ SAFE_CALLS = {
84
+ "Resolve": ["GetProductName", "GetVersionString", "GetCurrentPage"],
85
+ "ProjectManager": ["GetProjectListInCurrentFolder", "GetCurrentFolder"],
86
+ "Project": [
87
+ "GetName", "GetTimelineCount", "GetRenderFormats",
88
+ "GetCurrentRenderFormatAndCodec", "GetRenderPresetList",
89
+ "IsRenderingInProgress", "GetRenderJobList",
90
+ ],
91
+ "MediaPool": ["GetRootFolder", "GetCurrentFolder"],
92
+ "Timeline": [
93
+ "GetName", "GetStartFrame", "GetEndFrame", "GetCurrentTimecode", "GetMarkers",
94
+ ],
95
+ }
96
+
97
+
98
+ def acquire_resolve():
99
+ candidate = globals().get("resolve")
100
+ if candidate is not None:
101
+ return candidate
102
+ bmd = globals().get("bmd")
103
+ if bmd is not None:
104
+ try:
105
+ return bmd.scriptapp("Resolve")
106
+ except Exception:
107
+ return None
108
+ try:
109
+ import DaVinciResolveScript as script_module
110
+ return script_module.scriptapp("Resolve")
111
+ except Exception:
112
+ return None
113
+
114
+
115
+ def classify(obj, names):
116
+ """Which of `names` exist on `obj`."""
117
+ if obj is None:
118
+ return {"reachable": False, "note": "object not sampled in this context",
119
+ "present": [], "missing": sorted(names)}
120
+ present, missing = [], []
121
+ for name in names:
122
+ (present if callable(getattr(obj, name, None)) else missing).append(name)
123
+ return {"reachable": True, "present": sorted(present), "missing": sorted(missing)}
124
+
125
+
126
+ def try_calls(obj, names):
127
+ """Call safe getters; record value shape or the failure. Never mutates."""
128
+ out = {}
129
+ if obj is None:
130
+ return out
131
+ for name in names:
132
+ fn = getattr(obj, name, None)
133
+ if not callable(fn):
134
+ out[name] = {"ok": False, "why": "absent"}
135
+ continue
136
+ try:
137
+ value = fn()
138
+ except Exception as exc:
139
+ out[name] = {"ok": False, "why": "raised: %s" % type(exc).__name__}
140
+ continue
141
+ if value is None:
142
+ out[name] = {"ok": False, "why": "returned None"}
143
+ elif isinstance(value, bool):
144
+ # A bool getter answering False is a real answer, not a failure.
145
+ # Scoring it as one is how "nothing is rendering" reads as "broken".
146
+ out[name] = {"ok": True, "type": "bool", "value": value}
147
+ elif isinstance(value, dict):
148
+ out[name] = {"ok": True, "type": "dict", "size": len(value),
149
+ "sample": sorted(list(value)[:8])}
150
+ elif isinstance(value, (list, tuple)):
151
+ out[name] = {"ok": True, "type": "list", "size": len(value),
152
+ "sample": [str(v)[:40] for v in list(value)[:8]]}
153
+ else:
154
+ out[name] = {"ok": True, "type": type(value).__name__, "value": str(value)[:120]}
155
+ return out
156
+
157
+
158
+ def main():
159
+ resolve = acquire_resolve()
160
+ if resolve is None:
161
+ print("Could not get the resolve object. Run this from Workspace > Scripts.")
162
+ return None
163
+
164
+ report = {"probed_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
165
+ "interpreter": sys.executable}
166
+ try:
167
+ product = resolve.GetProductName()
168
+ report["product"] = product
169
+ report["version"] = resolve.GetVersionString()
170
+ report["edition"] = "studio" if "studio" in str(product).lower() else "free"
171
+ except Exception as exc:
172
+ report["product_error"] = type(exc).__name__
173
+ report["edition"] = "unknown"
174
+
175
+ manager = getattr(resolve, "GetProjectManager", lambda: None)()
176
+ project = getattr(manager, "GetCurrentProject", lambda: None)() if manager else None
177
+ pool = getattr(project, "GetMediaPool", lambda: None)() if project else None
178
+ timeline = getattr(project, "GetCurrentTimeline", lambda: None)() if project else None
179
+
180
+ item = None
181
+ if timeline is not None:
182
+ try:
183
+ for index in range(1, int(timeline.GetTrackCount("video") or 0) + 1):
184
+ items = timeline.GetItemListInTrack("video", index) or []
185
+ if items:
186
+ item = items[0]
187
+ break
188
+ except Exception:
189
+ item = None
190
+ media_item = None
191
+ if pool is not None:
192
+ try:
193
+ clips = pool.GetRootFolder().GetClipList() or []
194
+ media_item = clips[0] if clips else None
195
+ except Exception:
196
+ media_item = None
197
+
198
+ objects = {
199
+ "Resolve": resolve, "ProjectManager": manager, "Project": project,
200
+ "MediaPool": pool, "Timeline": timeline, "TimelineItem": item,
201
+ "MediaPoolItem": media_item,
202
+ }
203
+ report["surface"] = {k: classify(objects.get(k), v) for k, v in SURFACE.items()}
204
+ report["calls"] = {k: try_calls(objects.get(k), v) for k, v in SAFE_CALLS.items()}
205
+ report["context"] = {
206
+ "project_open": project is not None,
207
+ "timeline_open": timeline is not None,
208
+ "timeline_item_sampled": item is not None,
209
+ "media_item_sampled": media_item is not None,
210
+ }
211
+
212
+ # The render matrix is where free/Studio differ most visibly.
213
+ if project is not None:
214
+ try:
215
+ formats = project.GetRenderFormats() or {}
216
+ matrix = {}
217
+ for label in sorted(formats)[:40]:
218
+ try:
219
+ codecs = project.GetRenderCodecs(formats[label]) or {}
220
+ matrix[label] = sorted(codecs)
221
+ except Exception:
222
+ matrix[label] = ["<error>"]
223
+ report["render_matrix"] = matrix
224
+ report["render_format_count"] = len(formats)
225
+ report["render_pair_count"] = sum(len(v) for v in matrix.values())
226
+ except Exception as exc:
227
+ report["render_matrix_error"] = type(exc).__name__
228
+
229
+ edition = report.get("edition", "unknown")
230
+ path = os.path.join(REPORT_DIR, "capability-probe-%s.json" % edition)
231
+ try:
232
+ if not os.path.isdir(REPORT_DIR):
233
+ os.makedirs(REPORT_DIR)
234
+ with open(path, "w") as handle:
235
+ json.dump(report, handle, indent=2, sort_keys=True)
236
+ written = path
237
+ except (OSError, IOError) as exc:
238
+ written = "could not write (%s)" % type(exc).__name__
239
+
240
+ print("=" * 70)
241
+ print("Resolve capability probe - %s %s (%s)" % (
242
+ report.get("product", "?"), report.get("version", "?"), edition))
243
+ print("=" * 70)
244
+ for group, result in report["surface"].items():
245
+ if not result.get("reachable"):
246
+ print(" %-16s not sampled (%s)" % (group, result.get("note")))
247
+ continue
248
+ total = len(result["present"]) + len(result["missing"])
249
+ print(" %-16s %d/%d present" % (group, len(result["present"]), total))
250
+ if result["missing"]:
251
+ print(" missing: %s" % ", ".join(result["missing"]))
252
+ print()
253
+ failed = [(g, n, r["why"]) for g, calls in report["calls"].items()
254
+ for n, r in calls.items() if not r["ok"]]
255
+ print(" safe getter calls: %d ok, %d not" % (
256
+ sum(1 for g, c in report["calls"].items() for r in c.values() if r["ok"]), len(failed)))
257
+ for group, name, why in failed:
258
+ print(" %s.%s -> %s" % (group, name, why))
259
+ if "render_format_count" in report:
260
+ print()
261
+ print(" render matrix: %d formats, %d format/codec pairs" % (
262
+ report["render_format_count"], report["render_pair_count"]))
263
+ print()
264
+ print(" context: %s" % json.dumps(report["context"]))
265
+ print(" report: %s" % written)
266
+ print("=" * 70)
267
+ return report
268
+
269
+
270
+ main()
@@ -85,7 +85,7 @@ if not logging.getLogger().handlers:
85
85
  handlers=[logging.StreamHandler()],
86
86
  )
87
87
 
88
- VERSION = "2.67.1"
88
+ VERSION = "2.68.1"
89
89
  logger = logging.getLogger("davinci-resolve-mcp")
90
90
  logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
91
91
  logger.info(f"Detected platform: {get_platform()}")
@@ -234,6 +234,11 @@ mcp.tool = _tool_with_default_annotations
234
234
 
235
235
  resolve = None
236
236
  dvr_script = None
237
+ #: `dvr_script` may be None when Resolve is not installed; every use passes it
238
+ #: to `connect_resolve()`, which treats None as "not available" and returns None.
239
+ _OPTIONAL_DEPENDENCY_CONTRACT = (
240
+ "DaVinciResolveScript: always routed through connect_resolve(), which is None-tolerant"
241
+ )
237
242
 
238
243
  try:
239
244
  import DaVinciResolveScript as dvr_script # type: ignore