davinci-resolve-mcp 2.68.0 → 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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,54 @@
2
2
 
3
3
  Release history for the DaVinci Resolve MCP Server. The latest release is summarized in the root README; older entries live here to keep the README focused.
4
4
 
5
+ ## What's New in v2.68.1
6
+
7
+ Bug fix. A missing tool argument now returns the structured error envelope the
8
+ rest of the surface uses, instead of escaping as a raw `KeyError`.
9
+
10
+ ### Fixed
11
+
12
+ - **A missing argument reached the client as an unroutable crash.** Actions read
13
+ `p["track_type"]` directly; when the key was absent the resulting `KeyError`
14
+ escaped the action, escaped the tool, and arrived as
15
+ `ToolError: Error executing tool timeline: 'track_type'` — no code, no
16
+ category, no remediation. Walking the whole surface found **99 of 512 declared
17
+ actions** failing this way across 16 tools (`timeline`, `graph`, `render`,
18
+ `media_pool`, `media_storage`, `fusion_comp`, `folder`, `gallery_stills`,
19
+ `layout_presets`, `project_settings`, `project_manager_*`, `render_presets`,
20
+ `resolve_control`, `timeline_markers`). They now return, for example:
21
+
22
+ {"error": {"message": "'track_type' is required",
23
+ "code": "MISSING_TRACK_TYPE", "category": "invalid_input",
24
+ "retryable": false, "remediation": "...", "state": {...}}}
25
+
26
+ `retryable: false` matters as much as the code: an input error the caller must
27
+ fix was previously reported through a path that defaulted to retryable, which
28
+ sends an agent into a loop it cannot win.
29
+
30
+ The mechanism is a params dict whose missing keys raise a dedicated
31
+ `KeyError` subclass, caught at the tool boundary. Catching plain `KeyError`
32
+ there would have been simpler and wrong — it cannot tell a missing argument
33
+ from an internal lookup failure on some other dict, so our own bugs would have
34
+ been relabelled as the caller's mistake. `contracts.validate` remains the
35
+ preferred tool where a type, range or enum also needs checking, and the actions
36
+ already using it are unchanged.
37
+
38
+ ### Added
39
+
40
+ - `tests/test_tool_argument_validation.py` walks **every declared action** with an
41
+ empty params dict and asserts none leaks a `KeyError`. Reading the source cannot
42
+ find this class — `p[...]` after a guard is correct and `p[...]` without one is a
43
+ bug — so the walk executes each branch against a stub Resolve. It fails on the
44
+ pre-fix tree and runs in 2 s.
45
+
46
+ ### Changed
47
+
48
+ - `test_doc_tool_counts` counts `@mcp.tool()` decorators from the parsed syntax
49
+ tree rather than by matching the text. A docstring explaining where the
50
+ decorator has to sit was counted as a 35th tool; prose that mentions a
51
+ decorator is not a tool.
52
+
5
53
  ## What's New in v2.68.0
6
54
 
7
55
  Six engines that let an agent judge its own output before shipping it — audio
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # DaVinci Resolve MCP Server
2
2
 
3
- [![Version](https://img.shields.io/badge/version-2.68.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
3
+ [![Version](https://img.shields.io/badge/version-2.68.1-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
4
4
  [![npm](https://img.shields.io/npm/v/davinci-resolve-mcp.svg?label=npm&color=CB3837)](https://www.npmjs.com/package/davinci-resolve-mcp)
5
5
  [![API Coverage](https://img.shields.io/badge/API%20Coverage-100%25-brightgreen.svg)](docs/reference/api-coverage.md)
6
6
  [![Tools](https://img.shields.io/badge/MCP%20Tools-34%20(341%20full)-blue.svg)](#server-modes)
package/install.py CHANGED
@@ -36,7 +36,7 @@ from src.utils.update_check import (
36
36
 
37
37
  # ─── Version ──────────────────────────────────────────────────────────────────
38
38
 
39
- VERSION = "2.68.0"
39
+ VERSION = "2.68.1"
40
40
  # Only hard floor: mcp[cli] requires Python 3.10+. There is no upper bound —
41
41
  # Resolve's scripting bridge loads into newer interpreters on recent builds
42
42
  # (Python 3.14 verified against Resolve Studio 20.3.2). Older Resolve builds
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "davinci-resolve-mcp",
3
- "version": "2.68.0",
3
+ "version": "2.68.1",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -85,7 +85,7 @@ if not logging.getLogger().handlers:
85
85
  handlers=[logging.StreamHandler()],
86
86
  )
87
87
 
88
- VERSION = "2.68.0"
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()}")
package/src/server.py CHANGED
@@ -11,11 +11,13 @@ Usage:
11
11
  python src/server.py --full # Start the 341-tool granular server instead
12
12
  """
13
13
 
14
- VERSION = "2.68.0"
14
+ VERSION = "2.68.1"
15
15
 
16
16
  import base64
17
17
  import os
18
18
  import sys
19
+ import functools
20
+ import inspect
19
21
  import json
20
22
  import logging
21
23
  import math
@@ -1225,6 +1227,120 @@ _CATEGORY_RETRYABLE_DEFAULT: Dict[str, bool] = {
1225
1227
  _RETRYABLE_UNSET = object()
1226
1228
 
1227
1229
 
1230
+ class _MissingParam(KeyError):
1231
+ """A tool action subscripted a parameter the caller did not supply.
1232
+
1233
+ Subclasses `KeyError` so any existing `except KeyError` in the codebase keeps
1234
+ behaving as it did; what it adds is the ability to tell a *missing argument*
1235
+ apart from a KeyError on some other dict — a Resolve settings map, a parsed
1236
+ JSON body — which would otherwise be reported to the caller as "you forgot an
1237
+ argument" when the real fault is ours.
1238
+ """
1239
+
1240
+
1241
+ class _Params(dict):
1242
+ """The params dict, wired so a missing key is a diagnosable refusal.
1243
+
1244
+ Actions have historically reached into `p["track_type"]` directly. When the
1245
+ key is absent that raises a bare `KeyError`, which escapes the action, escapes
1246
+ the tool, and reaches the client as
1247
+ `ToolError: Error executing tool timeline: 'track_type'` — no code, no
1248
+ category, no remediation, and nothing an agent can route on. Measured across
1249
+ the whole surface: **99 of 512 declared actions** failed that way.
1250
+
1251
+ Two ways to fix that were available. Rewriting all 99 sites to use
1252
+ `contracts.validate` gives the best individual errors but is 99 chances to get
1253
+ a spec wrong, and leaves the 100th site to be written next month. Catching
1254
+ `KeyError` at the tool boundary closes the class in one place but cannot tell
1255
+ which dict raised, so an unrelated internal KeyError would be mislabelled as
1256
+ the caller's mistake.
1257
+
1258
+ This is the third option: keep the boundary catch, but make the *parameters*
1259
+ raise something only they can raise. False positives become impossible by
1260
+ construction.
1261
+
1262
+ `contracts.validate` remains the better tool wherever a type, a range or an
1263
+ enum also needs checking, and the actions that already use it keep doing so.
1264
+ This is deliberately **not** a migration of the 99 — rewriting them would add
1265
+ 99 opportunities to write a spec wrong without improving the defect being
1266
+ fixed here, and it would still leave the next new action unguarded. What is
1267
+ lost by not migrating is bounded: a *wrong* value still reaches Resolve, which
1268
+ answers None or False, so the result is a truthful failure rather than a
1269
+ misleading success.
1270
+ """
1271
+
1272
+ def __missing__(self, key):
1273
+ raise _MissingParam(key)
1274
+
1275
+
1276
+ def _params(params: Optional[Dict[str, Any]]) -> "_Params":
1277
+ """Every tool body starts here, so every action gets the diagnosable dict.
1278
+
1279
+ Note `_Params(params or {})` rather than a truthiness dance: an empty
1280
+ `_Params` is falsy like any empty dict, so `p = params or {}` would have
1281
+ quietly handed back a *plain* dict in exactly the no-arguments case this
1282
+ exists to serve.
1283
+ """
1284
+ return _Params(params or {})
1285
+
1286
+
1287
+ def _missing_param_error(exc: _MissingParam, action: str) -> Dict[str, Any]:
1288
+ """Turn a missing parameter into the envelope the rest of the surface uses."""
1289
+ name = exc.args[0] if exc.args else "parameter"
1290
+ slug = re.sub(r"[^A-Z0-9]+", "_", str(name).upper()).strip("_") or "PARAMETER"
1291
+ return _err(
1292
+ f"'{name}' is required",
1293
+ code=f"MISSING_{slug}",
1294
+ category="invalid_input",
1295
+ remediation=f"pass {name} in params, e.g. params={{\"{name}\": ...}}",
1296
+ state={"action": action, "missing": str(name)},
1297
+ )
1298
+
1299
+
1300
+ def _guarded_action_name(args, kwargs) -> str:
1301
+ """The `action` this call was for, wherever it was passed."""
1302
+ if "action" in kwargs:
1303
+ return str(kwargs["action"])
1304
+ return str(args[0]) if args else "?"
1305
+
1306
+
1307
+ def _guard_missing_params(fn):
1308
+ """Tool decorator: report a missing parameter instead of leaking a KeyError.
1309
+
1310
+ Sits *under* `@mcp.tool()` so FastMCP registers the guarded callable.
1311
+ `functools.wraps` keeps the name, docstring and signature FastMCP builds the
1312
+ tool schema from.
1313
+
1314
+ Two things this has to preserve, both learned by breaking them:
1315
+
1316
+ - **Async tools must stay coroutine functions.** `media_analysis` is `async`,
1317
+ and a synchronous wrapper around it returned an un-awaited coroutine that
1318
+ FastMCP could not use — the tool stopped answering entirely, taking its 71
1319
+ actions with it. `inspect.iscoroutinefunction` decides which wrapper to
1320
+ build, and a test now pins that async tools are still async afterwards.
1321
+ - **The signature is not (action, params).** `media_analysis` also takes
1322
+ `ctx: Optional[Context]`, so the wrapper forwards `*args, **kwargs` rather
1323
+ than naming parameters it does not know about.
1324
+ """
1325
+ if inspect.iscoroutinefunction(fn):
1326
+ @functools.wraps(fn)
1327
+ async def wrapper(*args, **kwargs):
1328
+ try:
1329
+ return await fn(*args, **kwargs)
1330
+ except _MissingParam as exc:
1331
+ return _missing_param_error(exc, _guarded_action_name(args, kwargs))
1332
+ else:
1333
+ @functools.wraps(fn)
1334
+ def wrapper(*args, **kwargs):
1335
+ try:
1336
+ return fn(*args, **kwargs)
1337
+ except _MissingParam as exc:
1338
+ return _missing_param_error(exc, _guarded_action_name(args, kwargs))
1339
+
1340
+ wrapper.__wrapped_by_missing_param_guard__ = True
1341
+ return wrapper
1342
+
1343
+
1228
1344
  def _err(message, *, code=None, category=None, retryable=_RETRYABLE_UNSET,
1229
1345
  remediation=None, reason=None, state=None):
1230
1346
  """Return a structured error envelope.
@@ -12687,6 +12803,7 @@ def _setup_clear_defaults(keys: Any, dry_run: bool) -> Dict[str, Any]:
12687
12803
 
12688
12804
 
12689
12805
  @mcp.tool()
12806
+ @_guard_missing_params
12690
12807
  def setup(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
12691
12808
  """Configure MCP conversational defaults and setup preferences.
12692
12809
 
@@ -12700,7 +12817,7 @@ def setup(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any
12700
12817
  media_analysis.*: analysis, metadata, marker, reporting, and workflow defaults
12701
12818
  updates.*: MCP update policy, interval, and snooze defaults
12702
12819
  """
12703
- p = params or {}
12820
+ p = _params(params)
12704
12821
  if action in {"schema", "capabilities", "options"}:
12705
12822
  return {
12706
12823
  "actions": ["schema", "get_defaults", "set_defaults", "clear_defaults"],
@@ -12840,6 +12957,7 @@ def setup(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any
12840
12957
 
12841
12958
 
12842
12959
  @mcp.tool()
12960
+ @_guard_missing_params
12843
12961
  def resolve_control(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
12844
12962
  """App-level DaVinci Resolve operations.
12845
12963
 
@@ -12882,7 +13000,7 @@ def resolve_control(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
12882
13000
  restore_state(state_token) -> {success, restored: {...}}
12883
13001
  — Returns Resolve to a previously-saved state.
12884
13002
  """
12885
- p = params or {}
13003
+ p = _params(params)
12886
13004
 
12887
13005
  # api_truth is a static knowledge lookup — no Resolve connection needed.
12888
13006
  if action == "api_truth":
@@ -13859,6 +13977,7 @@ def _close_control_panel() -> Dict[str, Any]:
13859
13977
  # ═══════════════════════════════════════════════════════════════════════════════
13860
13978
 
13861
13979
  @mcp.tool()
13980
+ @_guard_missing_params
13862
13981
  def layout_presets(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
13863
13982
  """Manage DaVinci Resolve UI layout presets.
13864
13983
 
@@ -13870,7 +13989,7 @@ def layout_presets(action: str, params: Optional[Dict[str, Any]] = None) -> Dict
13870
13989
  import_preset(path, name?) -> {success}
13871
13990
  delete(name) -> {success}
13872
13991
  """
13873
- p = params or {}
13992
+ p = _params(params)
13874
13993
  r = get_resolve()
13875
13994
  if r is None:
13876
13995
  return _err("Could not connect to DaVinci Resolve. It was not running and auto-launch failed. Check that Resolve Studio is installed.")
@@ -13905,6 +14024,7 @@ def layout_presets(action: str, params: Optional[Dict[str, Any]] = None) -> Dict
13905
14024
  # ═══════════════════════════════════════════════════════════════════════════════
13906
14025
 
13907
14026
  @mcp.tool()
14027
+ @_guard_missing_params
13908
14028
  def render_presets(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
13909
14029
  """Import/export render and burn-in presets.
13910
14030
 
@@ -13914,7 +14034,7 @@ def render_presets(action: str, params: Optional[Dict[str, Any]] = None) -> Dict
13914
14034
  import_burnin(path) -> {success}
13915
14035
  export_burnin(name, path) -> {success}
13916
14036
  """
13917
- p = params or {}
14037
+ p = _params(params)
13918
14038
  r = get_resolve()
13919
14039
  if r is None:
13920
14040
  return _err("Could not connect to DaVinci Resolve. It was not running and auto-launch failed. Check that Resolve Studio is installed.")
@@ -14815,6 +14935,7 @@ def _project_lint_live(r, pm) -> Dict[str, Any]:
14815
14935
 
14816
14936
 
14817
14937
  @mcp.tool()
14938
+ @_guard_missing_params
14818
14939
  def project_manager(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
14819
14940
  """Manage DaVinci Resolve projects.
14820
14941
 
@@ -14856,7 +14977,7 @@ def project_manager(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
14856
14977
  settings are applied in dependency order; markers only added if absent. Hooks run only
14857
14978
  when run_hooks=true (executes shell from the spec — opt-in).
14858
14979
  """
14859
- p = params or {}
14980
+ p = _params(params)
14860
14981
  r = get_resolve()
14861
14982
  if r is None:
14862
14983
  return _err("Could not connect to DaVinci Resolve. It was not running and auto-launch failed. Check that Resolve Studio is installed.")
@@ -14963,6 +15084,7 @@ def project_manager(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
14963
15084
  # ═══════════════════════════════════════════════════════════════════════════════
14964
15085
 
14965
15086
  @mcp.tool()
15087
+ @_guard_missing_params
14966
15088
  def project_manager_folders(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
14967
15089
  """Navigate and manage project folders in the Project Manager.
14968
15090
 
@@ -14975,7 +15097,7 @@ def project_manager_folders(action: str, params: Optional[Dict[str, Any]] = None
14975
15097
  goto_root() -> {success}
14976
15098
  goto_parent() -> {success}
14977
15099
  """
14978
- p = params or {}
15100
+ p = _params(params)
14979
15101
  r = get_resolve()
14980
15102
  if r is None:
14981
15103
  return _err("Could not connect to DaVinci Resolve. It was not running and auto-launch failed. Check that Resolve Studio is installed.")
@@ -15007,6 +15129,7 @@ def project_manager_folders(action: str, params: Optional[Dict[str, Any]] = None
15007
15129
  # ═══════════════════════════════════════════════════════════════════════════════
15008
15130
 
15009
15131
  @mcp.tool()
15132
+ @_guard_missing_params
15010
15133
  def project_manager_cloud(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
15011
15134
  """Cloud project operations (requires DaVinci Resolve cloud infrastructure).
15012
15135
 
@@ -15016,7 +15139,7 @@ def project_manager_cloud(action: str, params: Optional[Dict[str, Any]] = None)
15016
15139
  import_project(path, settings) -> {success}
15017
15140
  restore(folder_path, settings) -> {success}
15018
15141
  """
15019
- p = params or {}
15142
+ p = _params(params)
15020
15143
  r = get_resolve()
15021
15144
  if r is None:
15022
15145
  return _err("Could not connect to DaVinci Resolve. It was not running and auto-launch failed. Check that Resolve Studio is installed.")
@@ -15045,6 +15168,7 @@ def project_manager_cloud(action: str, params: Optional[Dict[str, Any]] = None)
15045
15168
  # ═══════════════════════════════════════════════════════════════════════════════
15046
15169
 
15047
15170
  @mcp.tool()
15171
+ @_guard_missing_params
15048
15172
  def project_manager_database(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
15049
15173
  """Manage DaVinci Resolve project databases.
15050
15174
 
@@ -15053,7 +15177,7 @@ def project_manager_database(action: str, params: Optional[Dict[str, Any]] = Non
15053
15177
  list() -> {databases}
15054
15178
  set_current(db_info) -> {success} — db_info: {DbType, DbName}
15055
15179
  """
15056
- p = params or {}
15180
+ p = _params(params)
15057
15181
  r = get_resolve()
15058
15182
  if r is None:
15059
15183
  return _err("Could not connect to DaVinci Resolve. It was not running and auto-launch failed. Check that Resolve Studio is installed.")
@@ -15076,6 +15200,7 @@ def project_manager_database(action: str, params: Optional[Dict[str, Any]] = Non
15076
15200
  # ═══════════════════════════════════════════════════════════════════════════════
15077
15201
 
15078
15202
  @mcp.tool()
15203
+ @_guard_missing_params
15079
15204
  def project_settings(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
15080
15205
  """Project metadata, settings, and color groups.
15081
15206
 
@@ -15100,7 +15225,7 @@ def project_settings(action: str, params: Optional[Dict[str, Any]] = None) -> Di
15100
15225
  apply_fairlight_preset(preset_name) -> {success}
15101
15226
  generate_speech(speech_generation_settings, timecode?) -> {success, new, new_id} — Resolve 21+, AI Speech Generator; creates new audio media (confirm-gated)
15102
15227
  """
15103
- p = params or {}
15228
+ p = _params(params)
15104
15229
  _, proj, err = _check()
15105
15230
  if err:
15106
15231
  return err
@@ -15853,6 +15978,7 @@ def _export_render_boundary_report(proj, p: Dict[str, Any]):
15853
15978
 
15854
15979
 
15855
15980
  @mcp.tool()
15981
+ @_guard_missing_params
15856
15982
  def render(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
15857
15983
  """Render pipeline: jobs, presets, formats, codecs, and rendering.
15858
15984
 
@@ -15901,7 +16027,7 @@ def render(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, An
15901
16027
  resolved against the live matrix, so an intent unavailable on this
15902
16028
  machine/license fails with the actual available lists.
15903
16029
  """
15904
- p = params or {}
16030
+ p = _params(params)
15905
16031
  _, proj, err = _check()
15906
16032
  if err:
15907
16033
  return err
@@ -16044,6 +16170,7 @@ def render(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, An
16044
16170
  # ═══════════════════════════════════════════════════════════════════════════════
16045
16171
 
16046
16172
  @mcp.tool()
16173
+ @_guard_missing_params
16047
16174
  def media_storage(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
16048
16175
  """Browse storage volumes and import media into the Media Pool.
16049
16176
 
@@ -16060,7 +16187,7 @@ def media_storage(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[
16060
16187
  add_clip_mattes(clip_id, paths, stereo_eye?) -> {success}
16061
16188
  add_timeline_mattes(paths) -> {items}
16062
16189
  """
16063
- p = params or {}
16190
+ p = _params(params)
16064
16191
  r = get_resolve()
16065
16192
  if r is None:
16066
16193
  return _err("Could not connect to DaVinci Resolve. It was not running and auto-launch failed. Check that Resolve Studio is installed.")
@@ -16111,6 +16238,7 @@ def media_storage(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[
16111
16238
  # ═══════════════════════════════════════════════════════════════════════════════
16112
16239
 
16113
16240
  @mcp.tool()
16241
+ @_guard_missing_params
16114
16242
  @_destructive_op("media_pool")
16115
16243
  def media_pool(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
16116
16244
  """Manage the Media Pool: folders, clips, timelines, import/export.
@@ -16198,7 +16326,7 @@ def media_pool(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str
16198
16326
  copy_clip_annotations(source_clip_id, target_clip_ids, include_markers?, include_flags?, include_clip_color?, dry_run?) -> {success, results}
16199
16327
  media_pool_boundary_report(depth?, clip_ids?, selected?) -> {capabilities, media_pool, items?}
16200
16328
  """
16201
- p = params or {}
16329
+ p = _params(params)
16202
16330
  _, proj, mp, err = _get_mp()
16203
16331
  if err:
16204
16332
  return err
@@ -16575,6 +16703,7 @@ def media_pool(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str
16575
16703
  # ═══════════════════════════════════════════════════════════════════════════════
16576
16704
 
16577
16705
  @mcp.tool()
16706
+ @_guard_missing_params
16578
16707
  def folder(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
16579
16708
  """Operations on Media Pool folders.
16580
16709
 
@@ -16593,7 +16722,7 @@ def folder(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, An
16593
16722
  analyze_for_slate(path?, marker_color?) -> {success} — Resolve 21+, AI Slate ID Extra
16594
16723
  remove_motion_blur(path?, deblur_option?) -> {success, created} — Resolve 21+; renders NEW media (confirm-gated)
16595
16724
  """
16596
- p = params or {}
16725
+ p = _params(params)
16597
16726
  _, _, mp, err = _get_mp()
16598
16727
  if err:
16599
16728
  return err
@@ -16716,6 +16845,7 @@ def folder(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, An
16716
16845
  # ═══════════════════════════════════════════════════════════════════════════════
16717
16846
 
16718
16847
  @mcp.tool()
16848
+ @_guard_missing_params
16719
16849
  def media_pool_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
16720
16850
  """Operations on a media pool clip. Identify clip by clip_id.
16721
16851
 
@@ -16767,7 +16897,7 @@ def media_pool_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
16767
16897
  verified empirically on Resolve Studio 20.3.2.9. If BMD changes the
16768
16898
  behavior the clip will still be selected — editor double-clicks to load.
16769
16899
  """
16770
- p = params or {}
16900
+ p = _params(params)
16771
16901
  _, _, mp, err = _get_mp()
16772
16902
  if err:
16773
16903
  return err
@@ -17080,6 +17210,7 @@ def media_pool_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
17080
17210
  # ═══════════════════════════════════════════════════════════════════════════════
17081
17211
 
17082
17212
  @mcp.tool()
17213
+ @_guard_missing_params
17083
17214
  def media_pool_item_markers(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
17084
17215
  """Markers and flags on media pool clips. Identify clip by clip_id.
17085
17216
 
@@ -17100,7 +17231,7 @@ def media_pool_item_markers(action: str, params: Optional[Dict[str, Any]] = None
17100
17231
  monitor_growing_file(clip_id) -> {success}
17101
17232
  replace_clip_preserve_sub_clip(clip_id, path) -> {success}
17102
17233
  """
17103
- p = params or {}
17234
+ p = _params(params)
17104
17235
  _, _, mp, err = _get_mp()
17105
17236
  if err:
17106
17237
  return err
@@ -17212,6 +17343,7 @@ def _opt_number(value: Any) -> Optional[float]:
17212
17343
 
17213
17344
 
17214
17345
  @mcp.tool()
17346
+ @_guard_missing_params
17215
17347
  async def media_analysis(action: str, params: Optional[Dict[str, Any]] = None, ctx: Optional[Context] = None) -> Dict[str, Any]:
17216
17348
  """Project-scoped media analysis and guarded metadata publishing.
17217
17349
 
@@ -17308,7 +17440,7 @@ async def media_analysis(action: str, params: Optional[Dict[str, Any]] = None, c
17308
17440
  vision-dependent metadata to Resolve. Works with any MCP client whose chat
17309
17441
  model is vision-capable; no sampling/createMessage support required.
17310
17442
  """
17311
- p = _media_analysis_apply_setup_defaults(action, dict(params or {}))
17443
+ p = _params(_media_analysis_apply_setup_defaults(action, dict(params or {})))
17312
17444
 
17313
17445
  # First-run frame-sampling prompt: if the user has never chosen a sampling
17314
17446
  # mode (and didn't pass one this call), ask before spending any vision
@@ -18384,6 +18516,7 @@ async def media_analysis(action: str, params: Optional[Dict[str, Any]] = None, c
18384
18516
  # ═══════════════════════════════════════════════════════════════════════════════
18385
18517
 
18386
18518
  @mcp.tool()
18519
+ @_guard_missing_params
18387
18520
  def timeline_versioning(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
18388
18521
  """Timeline version-on-mutate, archive, rollback, and brain-edit history (C6).
18389
18522
 
@@ -18431,7 +18564,7 @@ def timeline_versioning(action: str, params: Optional[Dict[str, Any]] = None) ->
18431
18564
  if ctx is None:
18432
18565
  return _err("No current project / can't resolve project root")
18433
18566
  resolve_h, project_h, project_root, project_name = ctx
18434
- p = params or {}
18567
+ p = _params(params)
18435
18568
 
18436
18569
  if action == "begin_run":
18437
18570
  return _analysis_runs.begin_run(
@@ -18756,6 +18889,7 @@ def _edit_engine_linked_audio_tracks(
18756
18889
 
18757
18890
 
18758
18891
  @mcp.tool()
18892
+ @_guard_missing_params
18759
18893
  @_destructive_op("edit_engine")
18760
18894
  def edit_engine(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
18761
18895
  """Evidence-driven edit loops (Phase E): selects assembly, tighten, swap.
@@ -18797,7 +18931,7 @@ def edit_engine(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
18797
18931
  timeline (version-archived first).
18798
18932
  - list_plans(limit?) / get_plan(plan_id)
18799
18933
  """
18800
- p = params or {}
18934
+ p = _params(params)
18801
18935
 
18802
18936
  def _project_context(*, need_resolve: bool) -> Tuple[Optional[Any], Optional[Any], Optional[str], Optional[Dict[str, Any]]]:
18803
18937
  # An explicit analysis_root always wins — the evidence DB the caller
@@ -19562,6 +19696,7 @@ _TIMELINE_ACTIONS = [
19562
19696
 
19563
19697
 
19564
19698
  @mcp.tool()
19699
+ @_guard_missing_params
19565
19700
  @_destructive_op("timeline")
19566
19701
  def timeline(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
19567
19702
  """Timeline operations: tracks, clips, import/export, generators, titles.
@@ -19747,7 +19882,7 @@ def timeline(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str,
19747
19882
  For long-form per-action guidance and a worked example, call:
19748
19883
  timeline(action="action_help", params={"name": "<action>"})
19749
19884
  """
19750
- p = params or {}
19885
+ p = _params(params)
19751
19886
  # action_help is pull-on-demand metadata; no Resolve connection needed.
19752
19887
  if action == "action_help":
19753
19888
  return _action_help("timeline", p)
@@ -20148,7 +20283,7 @@ def timeline(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str,
20148
20283
  result["ignored_state_keys"] = ignored_state
20149
20284
  return result
20150
20285
  elif action == "extract_source_frame_ranges":
20151
- p = params or {}
20286
+ p = _params(params)
20152
20287
  handles = int(p.get("handles", 24))
20153
20288
  gap_max = int(p.get("gap_max", 30))
20154
20289
  audio_ext = tuple(
@@ -20335,6 +20470,7 @@ def timeline(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str,
20335
20470
  # ═══════════════════════════════════════════════════════════════════════════════
20336
20471
 
20337
20472
  @mcp.tool()
20473
+ @_guard_missing_params
20338
20474
  @_destructive_op("timeline_markers")
20339
20475
  def timeline_markers(action: str, params: Optional[Dict[str, Any]] = None) -> Any:
20340
20476
  """Markers and playhead operations on the current timeline.
@@ -20370,7 +20506,7 @@ def timeline_markers(action: str, params: Optional[Dict[str, Any]] = None) -> An
20370
20506
  export_review_report(scope?, include_capabilities?) -> {title, annotations, capabilities?}
20371
20507
  annotation_boundary_report(scope?) -> {capabilities, annotations}
20372
20508
  """
20373
- p = params or {}
20509
+ p = _params(params)
20374
20510
  _, tl, err = _get_tl()
20375
20511
  if err:
20376
20512
  return err
@@ -20455,6 +20591,7 @@ def timeline_markers(action: str, params: Optional[Dict[str, Any]] = None) -> An
20455
20591
  # ═══════════════════════════════════════════════════════════════════════════════
20456
20592
 
20457
20593
  @mcp.tool()
20594
+ @_guard_missing_params
20458
20595
  @_destructive_op("timeline_ai")
20459
20596
  def timeline_ai(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
20460
20597
  """AI and analysis operations on the current timeline.
@@ -20468,7 +20605,7 @@ def timeline_ai(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
20468
20605
  grab_still() -> {success}
20469
20606
  grab_all_stills(source?) -> {count}
20470
20607
  """
20471
- p = params or {}
20608
+ p = _params(params)
20472
20609
  _, tl, err = _get_tl()
20473
20610
  if err:
20474
20611
  return err
@@ -20509,6 +20646,7 @@ def timeline_ai(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
20509
20646
  # ═══════════════════════════════════════════════════════════════════════════════
20510
20647
 
20511
20648
  @mcp.tool()
20649
+ @_guard_missing_params
20512
20650
  @_destructive_op("timeline_item")
20513
20651
  def timeline_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
20514
20652
  """Properties, transforms, speed, keyframes, and metadata for a timeline item.
@@ -20560,7 +20698,7 @@ def timeline_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[
20560
20698
 
20561
20699
  Default: track_type="video", track_index=1, item_index=0
20562
20700
  """
20563
- p = params or {}
20701
+ p = _params(params)
20564
20702
  tl, item, err = _get_item(p)
20565
20703
  if err:
20566
20704
  return err
@@ -20748,6 +20886,7 @@ def timeline_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[
20748
20886
  # ═══════════════════════════════════════════════════════════════════════════════
20749
20887
 
20750
20888
  @mcp.tool()
20889
+ @_guard_missing_params
20751
20890
  @_destructive_op("timeline_item_markers")
20752
20891
  def timeline_item_markers(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
20753
20892
  """Markers, flags, and clip color on timeline items. Identify by track_type, track_index, item_index (item_index is 0-BASED: 0 = first clip; track_index is 1-based).
@@ -20770,7 +20909,7 @@ def timeline_item_markers(action: str, params: Optional[Dict[str, Any]] = None)
20770
20909
 
20771
20910
  Default: track_type="video", track_index=1, item_index=0
20772
20911
  """
20773
- p = params or {}
20912
+ p = _params(params)
20774
20913
  _, item, err = _get_item(p)
20775
20914
  if err:
20776
20915
  return err
@@ -20825,6 +20964,7 @@ def timeline_item_markers(action: str, params: Optional[Dict[str, Any]] = None)
20825
20964
  # ═══════════════════════════════════════════════════════════════════════════════
20826
20965
 
20827
20966
  @mcp.tool()
20967
+ @_guard_missing_params
20828
20968
  @_destructive_op("timeline_item_fusion")
20829
20969
  def timeline_item_fusion(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
20830
20970
  """Fusion composition operations on timeline items. Identify by track_type, track_index, item_index (item_index is 0-BASED: 0 = first clip; track_index is 1-based).
@@ -20845,7 +20985,7 @@ def timeline_item_fusion(action: str, params: Optional[Dict[str, Any]] = None) -
20845
20985
 
20846
20986
  Default: track_type="video", track_index=1, item_index=0
20847
20987
  """
20848
- p = params or {}
20988
+ p = _params(params)
20849
20989
  _, item, err = _get_item(p)
20850
20990
  if err:
20851
20991
  return err
@@ -22126,6 +22266,7 @@ def _grade_evidence_base(proj, item, p: Dict[str, Any]) -> Dict[str, Any]:
22126
22266
  }
22127
22267
 
22128
22268
  @mcp.tool()
22269
+ @_guard_missing_params
22129
22270
  @_destructive_op("timeline_item_color")
22130
22271
  def timeline_item_color(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
22131
22272
  """Color grading, versions, LUTs, cache, and AI tools on timeline items. Identify by track_type, track_index, item_index (item_index is 0-BASED: 0 = first clip; track_index is 1-based).
@@ -22216,7 +22357,7 @@ def timeline_item_color(action: str, params: Optional[Dict[str, Any]] = None) ->
22216
22357
  For long-form per-action guidance and a worked example, call:
22217
22358
  timeline_item_color(action="action_help", params={"name": "<action>"})
22218
22359
  """
22219
- p = params or {}
22360
+ p = _params(params)
22220
22361
  if action == "action_help":
22221
22362
  return _action_help("timeline_item_color", p)
22222
22363
  _, item, err = _get_item(p)
@@ -22331,6 +22472,7 @@ def timeline_item_color(action: str, params: Optional[Dict[str, Any]] = None) ->
22331
22472
  # ═══════════════════════════════════════════════════════════════════════════════
22332
22473
 
22333
22474
  @mcp.tool()
22475
+ @_guard_missing_params
22334
22476
  @_destructive_op("timeline_item_takes")
22335
22477
  def timeline_item_takes(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
22336
22478
  """Take management on timeline items. Identify by track_type, track_index, item_index (item_index is 0-BASED: 0 = first clip; track_index is 1-based).
@@ -22346,7 +22488,7 @@ def timeline_item_takes(action: str, params: Optional[Dict[str, Any]] = None) ->
22346
22488
 
22347
22489
  Default: track_type="video", track_index=1, item_index=0
22348
22490
  """
22349
- p = params or {}
22491
+ p = _params(params)
22350
22492
  _, item, err = _get_item(p)
22351
22493
  if err:
22352
22494
  return err
@@ -22379,6 +22521,7 @@ def timeline_item_takes(action: str, params: Optional[Dict[str, Any]] = None) ->
22379
22521
  # ═══════════════════════════════════════════════════════════════════════════════
22380
22522
 
22381
22523
  @mcp.tool()
22524
+ @_guard_missing_params
22382
22525
  def gallery(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
22383
22526
  """Gallery album management.
22384
22527
 
@@ -22394,7 +22537,7 @@ def gallery(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, A
22394
22537
 
22395
22538
  album_index is 0-based into the still albums list.
22396
22539
  """
22397
- p = params or {}
22540
+ p = _params(params)
22398
22541
  _, proj, err = _check()
22399
22542
  if err:
22400
22543
  return err
@@ -22444,6 +22587,7 @@ def gallery(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, A
22444
22587
  # ═══════════════════════════════════════════════════════════════════════════════
22445
22588
 
22446
22589
  @mcp.tool()
22590
+ @_guard_missing_params
22447
22591
  def gallery_stills(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
22448
22592
  """Manage stills in gallery albums (best results on Color page).
22449
22593
 
@@ -22464,7 +22608,7 @@ def gallery_stills(action: str, params: Optional[Dict[str, Any]] = None) -> Dict
22464
22608
  File data is inlined in the response (DRX as text, images as base64).
22465
22609
  cleanup (default true) deletes exported files from disk after inlining.
22466
22610
  """
22467
- p = params or {}
22611
+ p = _params(params)
22468
22612
  _, proj, err = _check()
22469
22613
  if err:
22470
22614
  return err
@@ -22597,6 +22741,7 @@ def gallery_stills(action: str, params: Optional[Dict[str, Any]] = None) -> Dict
22597
22741
  # ═══════════════════════════════════════════════════════════════════════════════
22598
22742
 
22599
22743
  @mcp.tool()
22744
+ @_guard_missing_params
22600
22745
  @_destructive_op("graph")
22601
22746
  def graph(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
22602
22747
  """Node graph operations (color grading nodes). Source can be timeline, timeline item, or color group.
@@ -22636,7 +22781,7 @@ def graph(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any
22636
22781
  For long-form per-action guidance and a worked example, call:
22637
22782
  graph(action="action_help", params={"name": "<action>"})
22638
22783
  """
22639
- p = params or {}
22784
+ p = _params(params)
22640
22785
  if action == "action_help":
22641
22786
  return _action_help("graph", p)
22642
22787
  source = p.get("source", "timeline")
@@ -22739,6 +22884,7 @@ def graph(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any
22739
22884
  # ═══════════════════════════════════════════════════════════════════════════════
22740
22885
 
22741
22886
  @mcp.tool()
22887
+ @_guard_missing_params
22742
22888
  def color_group(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
22743
22889
  """Manage color groups and their node graphs.
22744
22890
 
@@ -22750,7 +22896,7 @@ def color_group(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
22750
22896
  get_pre_clip_graph(group_name) -> {available, num_nodes}
22751
22897
  get_post_clip_graph(group_name) -> {available, num_nodes}
22752
22898
  """
22753
- p = params or {}
22899
+ p = _params(params)
22754
22900
  _, proj, err = _check()
22755
22901
  if err:
22756
22902
  return err
@@ -23661,6 +23807,7 @@ def _fusion_get_text_plus(comp, p: Dict[str, Any]) -> Dict[str, Any]:
23661
23807
 
23662
23808
 
23663
23809
  @mcp.tool()
23810
+ @_guard_missing_params
23664
23811
  def fusion_comp(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
23665
23812
  """Fusion composition node graph operations.
23666
23813
 
@@ -23731,7 +23878,7 @@ def fusion_comp(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
23731
23878
  ColorCorrector, RectangleMask, EllipseMask, Tracker, MediaIn, MediaOut,
23732
23879
  Loader, Saver, Glow, FilmGrain, CornerPositioner, DeltaKeyer, UltraKeyer
23733
23880
  """
23734
- p = params or {}
23881
+ p = _params(params)
23735
23882
 
23736
23883
  if action == "bulk_set_inputs":
23737
23884
  return _fusion_comp_bulk_set_inputs(p)
@@ -24231,6 +24378,7 @@ def _validate_glsl_minimal(source: str) -> Dict[str, Any]:
24231
24378
 
24232
24379
 
24233
24380
  @mcp.tool()
24381
+ @_guard_missing_params
24234
24382
  def fuse_plugin(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
24235
24383
  """Author and install Fusion Fuse plugins (.fuse files).
24236
24384
 
@@ -24257,7 +24405,7 @@ def fuse_plugin(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
24257
24405
  docs/authoring/fuse-dctl-authoring.md for the per-kind option spec.
24258
24406
  list_templates() -> {kinds}
24259
24407
  """
24260
- p = params or {}
24408
+ p = _params(params)
24261
24409
 
24262
24410
  if action == "path":
24263
24411
  return {"fuses_dir": _fuses_dir()}
@@ -24474,6 +24622,7 @@ _DCTL_VALID_CATEGORIES = ("lut", "aces_idt", "aces_odt")
24474
24622
 
24475
24623
 
24476
24624
  @mcp.tool()
24625
+ @_guard_missing_params
24477
24626
  def dctl(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
24478
24627
  """Author and install DCTL files (Color page custom shaders + ACES transforms).
24479
24628
 
@@ -24510,7 +24659,7 @@ def dctl(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]
24510
24659
  pass that as the `category` argument to install().
24511
24660
  list_templates() -> {kinds, kind_categories}
24512
24661
  """
24513
- p = params or {}
24662
+ p = _params(params)
24514
24663
 
24515
24664
  def _category(default: str = "lut") -> Tuple[Optional[Dict[str, Any]], str]:
24516
24665
  cat = p.get("category", default)
@@ -25403,6 +25552,7 @@ def _extension_boundary_report(p: Dict[str, Any]) -> Dict[str, Any]:
25403
25552
 
25404
25553
 
25405
25554
  @mcp.tool()
25555
+ @_guard_missing_params
25406
25556
  def script_plugin(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
25407
25557
  """Author and install Resolve-page Lua/Python scripts (Workspace → Scripts menu).
25408
25558
 
@@ -25458,7 +25608,7 @@ def script_plugin(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[
25458
25608
  refresh_or_restart_required(extension_type, category?) -> {refresh_luts, restart_required}
25459
25609
  extension_boundary_report(include_template_matrix?) -> {capabilities, template_matrix, dry_run_probes}
25460
25610
  """
25461
- p = params or {}
25611
+ p = _params(params)
25462
25612
 
25463
25613
  if action == "extension_capabilities":
25464
25614
  return _extension_capabilities()