davinci-resolve-mcp 3.2.2 → 3.3.0
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 +38 -0
- package/README.md +4 -4
- package/README.zh-CN.md +5 -5
- package/docs/SKILL.md +1 -1
- package/docs/install.md +2 -2
- package/docs/reference/api-coverage.md +6 -1
- package/docs/reference/typed-api-search.md +56 -0
- package/install.py +2 -2
- package/package.json +1 -1
- package/src/granular/common.py +1 -1
- package/src/granular/resolve_control.py +47 -0
- package/src/resolve_mcp_server.py +1 -1
- package/src/server.py +30 -3
- package/src/utils/typed_api_search.py +260 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,44 @@
|
|
|
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 v3.3.0 — ask the server what the native Resolve API contains
|
|
6
|
+
|
|
7
|
+
Contributed by @legionsound (#229), the second of the two branches queued in #207.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **`resolve_control`: `search_api`, `describe_api`, `api_surface`**, plus
|
|
12
|
+
granular twins `search_resolve_api`, `describe_resolve_api` and
|
|
13
|
+
`get_resolve_api_surface`. `resolve_control api_truth` already answered *what
|
|
14
|
+
is broken*; nothing answered *what exists*. #205 shipped Blackmagic's typed
|
|
15
|
+
`DaVinciResolveScript.pyi` and `scripts/audit_typed_api.py` could inventory
|
|
16
|
+
it, but only from a shell — an agent talking to this server had no way to ask.
|
|
17
|
+
This is the equivalent of Blackmagic's own `search_scripting_api`. Tool count
|
|
18
|
+
37/384 → 37/387.
|
|
19
|
+
- **All three are read-only and none needs a Resolve connection.** They parse
|
|
20
|
+
the stub that already ships in `docs/reference/`, so they answer with Resolve
|
|
21
|
+
closed, and they describe the stub checked into this repository — currently
|
|
22
|
+
21.1.0.14 — not whatever build happens to be installed. A missing stub is
|
|
23
|
+
reported as missing rather than guessed around.
|
|
24
|
+
- **Every result carries `referenced_in_this_server` and the files that
|
|
25
|
+
reference the method**, so a lookup doubles as a parity check: does the native
|
|
26
|
+
API have it, and do we wrap it? The flag counts executable syntax only —
|
|
27
|
+
attribute access, calls, `getattr(obj, "Name")` — never docstrings or
|
|
28
|
+
comments, built the same way `audit_typed_api.py` builds its source
|
|
29
|
+
references. A method named in prose is not coverage.
|
|
30
|
+
- **Ambiguity is reported, not guessed.** A bare `GetName` lists its candidates
|
|
31
|
+
instead of picking one. An invalid regex is refused rather than raised.
|
|
32
|
+
Results are capped with an explicit `truncated` flag rather than silently cut.
|
|
33
|
+
|
|
34
|
+
### Guards
|
|
35
|
+
|
|
36
|
+
- The parser independently reports **410 methods, 46 TypedDicts, 513 fields**,
|
|
37
|
+
and `tests/test_typed_api_search.py` asserts that equality against the
|
|
38
|
+
inventory recorded in `resolve-211-typed-api.md`. A future stub refresh that
|
|
39
|
+
changes the surface now fails the suite instead of drifting quietly. The same
|
|
40
|
+
file checks both interfaces agree on surface, search and describe, and that
|
|
41
|
+
neither needs a connection.
|
|
42
|
+
|
|
5
43
|
## What's New in v3.2.2 — an analysis root that is deleted is actually let go of
|
|
6
44
|
|
|
7
45
|
Contributed by @Dev-next-gen (#228), generalised to the second site and to the
|
package/README.md
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
English | [简体中文](README.zh-CN.md)
|
|
4
4
|
|
|
5
|
-
[](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
|
|
6
6
|
[](https://www.npmjs.com/package/davinci-resolve-mcp)
|
|
7
7
|
[](docs/reference/api-coverage.md)
|
|
8
|
-
[-blue.svg)](#server-modes)
|
|
9
9
|
[-18%20tools-blueviolet.svg)](#server-modes)
|
|
10
10
|
[](docs/reference/api-coverage.md#test-results)
|
|
11
11
|
[](https://www.blackmagicdesign.com/products/davinciresolve)
|
|
@@ -133,7 +133,7 @@ The command starts a loopback-only server and opens the control panel in your br
|
|
|
133
133
|
| Mode | Entry point | Tools | Best for |
|
|
134
134
|
|------|-------------|-------|----------|
|
|
135
135
|
| Compound | `src/server.py` | 37 | Default mode for most assistants. Related Resolve operations are grouped behind action parameters to keep context usage low. |
|
|
136
|
-
| Full / granular | `src/server.py --full` or `src/resolve_mcp_server.py` |
|
|
136
|
+
| Full / granular | `src/server.py --full` or `src/resolve_mcp_server.py` | 387 | Power users who want one MCP tool per Resolve API method. |
|
|
137
137
|
|
|
138
138
|
The compound server is recommended unless you specifically need the granular one-tool-per-method surface.
|
|
139
139
|
|
|
@@ -365,7 +365,7 @@ The default server is a local stdio process launched by your MCP client; it does
|
|
|
365
365
|
|
|
366
366
|
| Metric | Value |
|
|
367
367
|
|--------|-------|
|
|
368
|
-
| MCP Tools | **37** compound / **
|
|
368
|
+
| MCP Tools | **37** compound / **387** granular (live server) |
|
|
369
369
|
| Advanced (offline) tools | **18** — .drp/.drt/.drx + DB authoring, no Resolve running |
|
|
370
370
|
| Kernel Actions | **136** guarded workflow actions across 9 compound tools |
|
|
371
371
|
| API Methods Covered | **361/361** (100%) |
|
package/README.zh-CN.md
CHANGED
|
@@ -2,17 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md) | 简体中文
|
|
4
4
|
|
|
5
|
-
[](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
|
|
6
6
|
[](https://www.npmjs.com/package/davinci-resolve-mcp)
|
|
7
7
|
[](docs/reference/api-coverage.md)
|
|
8
|
-
[-blue.svg)](#服务器模式)
|
|
9
9
|
[-18%20tools-blueviolet.svg)](#服务器模式)
|
|
10
10
|
[](docs/reference/api-coverage.md#test-results)
|
|
11
11
|
[](https://www.blackmagicdesign.com/products/davinciresolve)
|
|
12
12
|
[](https://www.python.org/downloads/)
|
|
13
13
|
[](https://opensource.org/licenses/MIT)
|
|
14
14
|
|
|
15
|
-
> 本翻译对应 v3.
|
|
15
|
+
> 本翻译对应 v3.3.0 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
|
|
16
16
|
|
|
17
17
|
一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
|
|
18
18
|
|
|
@@ -88,7 +88,7 @@ venv/bin/python -m src.control_panel
|
|
|
88
88
|
| 模式 | 入口 | 工具数 | 适合谁 |
|
|
89
89
|
|------|------|--------|--------|
|
|
90
90
|
| Compound(复合) | `src/server.py` | 37 | 大多数助手的默认模式。相关的 Resolve 操作按 action 参数分组,压低上下文占用。 |
|
|
91
|
-
| Full / granular(细粒度) | `src/server.py --full` 或 `src/resolve_mcp_server.py` |
|
|
91
|
+
| Full / granular(细粒度) | `src/server.py --full` 或 `src/resolve_mcp_server.py` | 387 | 想要"一个 Resolve API 方法 = 一个 MCP 工具"的重度用户。 |
|
|
92
92
|
|
|
93
93
|
除非你明确需要一方法一工具的细粒度界面,否则推荐复合模式。
|
|
94
94
|
|
|
@@ -226,7 +226,7 @@ DRX 调色写入**针对 Resolve Studio 做过实机校准**:调色参数默
|
|
|
226
226
|
|
|
227
227
|
| 指标 | 数值 |
|
|
228
228
|
|------|------|
|
|
229
|
-
| MCP 工具 | **37** 复合 / **
|
|
229
|
+
| MCP 工具 | **37** 复合 / **387** 细粒度(实时服务器) |
|
|
230
230
|
| Advanced(离线)工具 | **18**——.drp/.drt/.drx + 数据库创作,无需 Resolve 运行 |
|
|
231
231
|
| 内核 action | 9 个复合工具下 **136** 个带护栏的工作流 action |
|
|
232
232
|
| API 方法覆盖 | **361/361**(100%) |
|
package/docs/SKILL.md
CHANGED
|
@@ -353,7 +353,7 @@ to the user as verified.
|
|
|
353
353
|
| Mode | Entry point | Tool count | Use when |
|
|
354
354
|
|---|---|---|---|
|
|
355
355
|
| Compound (default) | `src/server.py` | 37 tools | Most workflows — keeps context lean |
|
|
356
|
-
| Granular (full) | `src/server.py --full` |
|
|
356
|
+
| Granular (full) | `src/server.py --full` | 387 tools | Power users needing one tool per API method |
|
|
357
357
|
|
|
358
358
|
Resolve 21.1 adds [twelve read-only discovery controls](reference/resolve211-read-controls.md)
|
|
359
359
|
for edition, presets, audio formats/codecs, normalization modes, speed, fades
|
package/docs/install.md
CHANGED
|
@@ -144,7 +144,7 @@ The MCP server comes in two modes:
|
|
|
144
144
|
| Mode | File | Tools | Best For |
|
|
145
145
|
|------|------|-------|----------|
|
|
146
146
|
| **Compound** (default) | `src/server.py` | 37 | Most users — fast, clean, low context usage |
|
|
147
|
-
| **Full** | `src/resolve_mcp_server.py` |
|
|
147
|
+
| **Full** | `src/resolve_mcp_server.py` | 387 | Power users who want one tool per API method |
|
|
148
148
|
|
|
149
149
|
The compound server's `timeline_item` tool includes dedicated actions for common workflows:
|
|
150
150
|
|
|
@@ -159,7 +159,7 @@ The compound server's `timeline_item` tool includes dedicated actions for common
|
|
|
159
159
|
|
|
160
160
|
The installer uses the compound server by default. To use the full server:
|
|
161
161
|
```bash
|
|
162
|
-
python src/server.py --full # Launch full
|
|
162
|
+
python src/server.py --full # Launch full 387-tool server
|
|
163
163
|
# Or point your MCP config directly at src/resolve_mcp_server.py
|
|
164
164
|
```
|
|
165
165
|
|
|
@@ -25,7 +25,7 @@ Every non-deprecated method in the bundled legacy README is represented. This
|
|
|
25
25
|
does not claim complete coverage of the newer Resolve 21.1 typed API. The
|
|
26
26
|
default compound server exposes **37 tools** that group related operations by
|
|
27
27
|
action parameter, keeping LLM context windows lean. The full granular server
|
|
28
|
-
provides **
|
|
28
|
+
provides **387 individual tools** for power users. The legacy coverage spans
|
|
29
29
|
13 API object classes. MCP-level kernel actions are tracked separately in
|
|
30
30
|
[Kernel Action Coverage](../kernels/README.md).
|
|
31
31
|
|
|
@@ -39,6 +39,11 @@ Parity with Blackmagic's own MCP surfaced one more gap: it exposes
|
|
|
39
39
|
node but never list, install or remove LUT files. The `lut` tool and its
|
|
40
40
|
granular twins close that. See [LUT file controls](lut-file-controls.md).
|
|
41
41
|
|
|
42
|
+
The typed stub shipped in #205 is now queryable from the server itself, the
|
|
43
|
+
way Blackmagic's MCP exposes `search_scripting_api` — and each result also says
|
|
44
|
+
whether this server wraps the method, so a lookup doubles as a parity check.
|
|
45
|
+
See [Querying the typed API](typed-api-search.md).
|
|
46
|
+
|
|
42
47
|
The 34th compound tool is `timeline_versioning` (C6) — an MCP-level workflow
|
|
43
48
|
tool, not a wrapper around a Resolve API method. It surfaces the
|
|
44
49
|
version-on-mutate hook that auto-archives the working timeline before any
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Querying the typed API from inside the server
|
|
2
|
+
|
|
3
|
+
`resolve_control api_truth` answers *what is broken*. Nothing answered *what
|
|
4
|
+
exists*. PR #205 landed Blackmagic's `DaVinciResolveScript.pyi` in
|
|
5
|
+
`docs/reference/`, and `scripts/audit_typed_api.py` can turn it into a full
|
|
6
|
+
inventory — but only from a shell. An agent talking to this server had no way
|
|
7
|
+
to ask what the native API contains.
|
|
8
|
+
|
|
9
|
+
Blackmagic's own MCP exposes `search_scripting_api` for this. These are the
|
|
10
|
+
equivalent, with one addition that matters more here.
|
|
11
|
+
|
|
12
|
+
## The addition: coverage, not just existence
|
|
13
|
+
|
|
14
|
+
Every result carries `referenced_in_this_server` and the source files that
|
|
15
|
+
reference it. That turns a lookup into a parity check — *does the native API
|
|
16
|
+
have it, and do we wrap it?* — which is the question the whole 21.1 audit was
|
|
17
|
+
built to answer, and it was previously only answerable by grepping.
|
|
18
|
+
|
|
19
|
+
The flag counts **executable syntax only**: attribute access, calls, and
|
|
20
|
+
`getattr(obj, "Name")`. A method named in a docstring or a comment is not
|
|
21
|
+
coverage. Treating prose as coverage is the specific mistake this repo's audits
|
|
22
|
+
exist to avoid, so the flag is built the same way `scripts/audit_typed_api.py`
|
|
23
|
+
builds its source references.
|
|
24
|
+
|
|
25
|
+
## Actions and tools
|
|
26
|
+
|
|
27
|
+
Compound `resolve_control`, none of which need a Resolve connection:
|
|
28
|
+
|
|
29
|
+
- `search_api(pattern, kind?, limit?)` — case-insensitive regex over
|
|
30
|
+
class-qualified method names, signatures, TypedDict names, field names and
|
|
31
|
+
descriptions. `kind` is `all`, `methods` or `options`. Results are capped and
|
|
32
|
+
the response declares `truncated` rather than silently cutting.
|
|
33
|
+
- `describe_api(symbol)` — one `Class.Method`, an unambiguous bare method name,
|
|
34
|
+
or a TypedDict name. An ambiguous bare name lists the candidates instead of
|
|
35
|
+
guessing.
|
|
36
|
+
- `api_surface()` — counts and the object list.
|
|
37
|
+
|
|
38
|
+
Granular twins: `search_resolve_api`, `describe_resolve_api`,
|
|
39
|
+
`get_resolve_api_surface`. Tool count 384 → 387. The granular tools carry the read-only annotation, since
|
|
40
|
+
for the granular server the MCP annotation is the signal a client reads.
|
|
41
|
+
|
|
42
|
+
## Self-consistency
|
|
43
|
+
|
|
44
|
+
The parser reports **410 methods, 46 TypedDicts, 513 fields**, independently
|
|
45
|
+
matching the inventory published in `resolve-211-typed-api.md` and the
|
|
46
|
+
disposition ledgers built during the 21.1 audit. That agreement is asserted in
|
|
47
|
+
`tests/test_typed_api_search.py`, so a stub refresh that changes the surface
|
|
48
|
+
will fail the suite rather than drift quietly.
|
|
49
|
+
|
|
50
|
+
## Scope
|
|
51
|
+
|
|
52
|
+
This reads the stub shipped in this repository; it does not query a running
|
|
53
|
+
Resolve, and it will report a missing stub rather than guessing. It therefore
|
|
54
|
+
describes the API of the Resolve version whose stub is checked in — 21.1.0.14 —
|
|
55
|
+
not whatever build happens to be installed. Refreshing the stub is the existing
|
|
56
|
+
documented process in `resolve-211-typed-api.md`.
|
package/install.py
CHANGED
|
@@ -37,7 +37,7 @@ from src.utils.update_check import (
|
|
|
37
37
|
|
|
38
38
|
# ─── Version ──────────────────────────────────────────────────────────────────
|
|
39
39
|
|
|
40
|
-
VERSION = "3.
|
|
40
|
+
VERSION = "3.3.0"
|
|
41
41
|
# Only hard floor: mcp[cli] requires Python 3.10+. There is no upper bound —
|
|
42
42
|
# Resolve's scripting bridge loads into newer interpreters on recent builds
|
|
43
43
|
# (Python 3.14 verified against Resolve Studio 20.3.2). Older Resolve builds
|
|
@@ -1543,7 +1543,7 @@ def verify_resolve_connection(python_path, api_path, lib_path):
|
|
|
1543
1543
|
|
|
1544
1544
|
def print_banner():
|
|
1545
1545
|
title = f"DaVinci Resolve MCP Server — Installer v{VERSION}"
|
|
1546
|
-
subtitle = "37 compound ·
|
|
1546
|
+
subtitle = "37 compound · 387 full · 3 platforms"
|
|
1547
1547
|
print()
|
|
1548
1548
|
print(bold(" ╔══════════════════════════════════════════════════════╗"))
|
|
1549
1549
|
print(bold(f" ║{title:^54}║"))
|
package/package.json
CHANGED
package/src/granular/common.py
CHANGED
|
@@ -87,7 +87,7 @@ if not logging.getLogger().handlers:
|
|
|
87
87
|
handlers=[logging.StreamHandler()],
|
|
88
88
|
)
|
|
89
89
|
|
|
90
|
-
VERSION = "3.
|
|
90
|
+
VERSION = "3.3.0"
|
|
91
91
|
logger = logging.getLogger("davinci-resolve-mcp")
|
|
92
92
|
logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
|
|
93
93
|
logger.info(f"Detected platform: {get_platform()}")
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"""Resolve control resources, inspection helpers, and app-level tools."""
|
|
2
2
|
|
|
3
3
|
from src.granular.common import * # noqa: F401,F403
|
|
4
|
+
import os
|
|
5
|
+
from src.utils import typed_api_search
|
|
4
6
|
|
|
5
7
|
resolve = ResolveProxy()
|
|
6
8
|
|
|
@@ -719,3 +721,48 @@ def export_user_preferences_preset(preset_name: str, export_path: str) -> Dict[s
|
|
|
719
721
|
return missing
|
|
720
722
|
result = resolve.ExportUserPreferencesPreset(preset_name, export_path)
|
|
721
723
|
return {"success": bool(result), "preset_name": preset_name, "export_path": export_path}
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
_PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
@mcp.tool(annotations=READ_ONLY_TOOL)
|
|
730
|
+
def search_resolve_api(pattern: str, kind: str = "all", limit: int = 50) -> Dict[str, Any]:
|
|
731
|
+
"""Search the shipped Resolve 21.1 typed API stub. Needs no connection.
|
|
732
|
+
|
|
733
|
+
`api_truth` answers what is broken; this answers what exists. Each hit also
|
|
734
|
+
reports whether THIS server references the method and in which files, so a
|
|
735
|
+
search doubles as a parity check.
|
|
736
|
+
|
|
737
|
+
Args:
|
|
738
|
+
pattern: Case-insensitive regular expression, e.g. "marker" or "(Get|Set)Setting".
|
|
739
|
+
kind: 'all' (default), 'methods', or 'options' for TypedDicts only.
|
|
740
|
+
limit: Maximum results per section (capped at 200).
|
|
741
|
+
"""
|
|
742
|
+
try:
|
|
743
|
+
return typed_api_search.search(_PROJECT_DIR, pattern, kind=kind, limit=limit)
|
|
744
|
+
except typed_api_search.TypedApiError as exc:
|
|
745
|
+
return {"error": str(exc)}
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
@mcp.tool(annotations=READ_ONLY_TOOL)
|
|
749
|
+
def describe_resolve_api(symbol: str) -> Dict[str, Any]:
|
|
750
|
+
"""Full typed detail for one Resolve API symbol. Needs no connection.
|
|
751
|
+
|
|
752
|
+
Args:
|
|
753
|
+
symbol: 'Class.Method' (e.g. "Project.GetName"), a bare method name when
|
|
754
|
+
it is unambiguous, or a TypedDict name (e.g. "RenderSettings").
|
|
755
|
+
"""
|
|
756
|
+
try:
|
|
757
|
+
return typed_api_search.describe(_PROJECT_DIR, symbol)
|
|
758
|
+
except typed_api_search.TypedApiError as exc:
|
|
759
|
+
return {"error": str(exc)}
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
@mcp.tool(annotations=READ_ONLY_TOOL)
|
|
763
|
+
def get_resolve_api_surface() -> Dict[str, Any]:
|
|
764
|
+
"""Counts and object list for the shipped typed stub. Needs no connection."""
|
|
765
|
+
try:
|
|
766
|
+
return typed_api_search.summary(_PROJECT_DIR)
|
|
767
|
+
except typed_api_search.TypedApiError as exc:
|
|
768
|
+
return {"error": str(exc)}
|
|
@@ -34,7 +34,7 @@ from src.utils.update_check import start_background_update_check
|
|
|
34
34
|
if __name__ == "__main__":
|
|
35
35
|
try:
|
|
36
36
|
start_background_update_check(VERSION, project_dir, logger)
|
|
37
|
-
logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION} (
|
|
37
|
+
logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION} (387 granular tools)")
|
|
38
38
|
run_fastmcp_stdio(mcp)
|
|
39
39
|
except KeyboardInterrupt:
|
|
40
40
|
logger.info("Server shutdown requested")
|
package/src/server.py
CHANGED
|
@@ -11,7 +11,7 @@ Usage:
|
|
|
11
11
|
python src/server.py --full # Start the 377-tool granular server instead
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
-
VERSION = "3.
|
|
14
|
+
VERSION = "3.3.0"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -52,6 +52,7 @@ from src.utils.resolve211_edits import validate_edit_options, validate_transitio
|
|
|
52
52
|
|
|
53
53
|
# Platform-specific Resolve paths
|
|
54
54
|
from src.utils.cdl import normalize_cdl_payload
|
|
55
|
+
from src.utils import typed_api_search
|
|
55
56
|
from src.utils import lut_files
|
|
56
57
|
from src.utils import resolve_writes as _resolve_writes
|
|
57
58
|
from src.utils.mcp_stdio import run_fastmcp_stdio
|
|
@@ -16652,6 +16653,15 @@ def resolve_control(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
|
|
|
16652
16653
|
set_keyframe_mode(mode) -> {success}
|
|
16653
16654
|
quit() -> {success}
|
|
16654
16655
|
get_fairlight_presets() -> {presets}
|
|
16656
|
+
search_api(pattern, kind?, limit?) -> {methods, option_types, total_matches}
|
|
16657
|
+
— regex over the shipped Resolve 21.1 typed stub. Each hit says whether
|
|
16658
|
+
THIS server references the method, and where, so it doubles as a
|
|
16659
|
+
parity check. kind: 'all' (default) | 'methods' | 'options'.
|
|
16660
|
+
No connection needed.
|
|
16661
|
+
describe_api(symbol) -> {signatures, stub_line, description, source_files}
|
|
16662
|
+
— one 'Class.Method' or one TypedDict name. No connection needed.
|
|
16663
|
+
api_surface() -> {methods, option_types, option_fields, objects}
|
|
16664
|
+
— what the shipped stub contains. No connection needed.
|
|
16655
16665
|
set_high_priority() -> {success}
|
|
16656
16666
|
disable_background_tasks_for_current_session() -> {success} — Resolve 21+
|
|
16657
16667
|
list_user_preferences_presets() -> {presets} — Resolve 21.0.4+
|
|
@@ -16706,7 +16716,24 @@ def resolve_control(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
|
|
|
16706
16716
|
# that property is worth keeping: it is the one call that still answers when
|
|
16707
16717
|
# Resolve is down or unreachable. So the live build is used only if one is
|
|
16708
16718
|
# ALREADY connected, or if the caller names it. Never connect for this.
|
|
16709
|
-
if action == "
|
|
16719
|
+
if action == "search_api":
|
|
16720
|
+
try:
|
|
16721
|
+
return typed_api_search.search(
|
|
16722
|
+
project_dir, p.get("pattern"),
|
|
16723
|
+
kind=p.get("kind", "all"), limit=p.get("limit", 50))
|
|
16724
|
+
except typed_api_search.TypedApiError as exc:
|
|
16725
|
+
return _err(str(exc), code="TYPED_API_QUERY", category="invalid_input")
|
|
16726
|
+
elif action == "describe_api":
|
|
16727
|
+
try:
|
|
16728
|
+
return typed_api_search.describe(project_dir, p.get("symbol"))
|
|
16729
|
+
except typed_api_search.TypedApiError as exc:
|
|
16730
|
+
return _err(str(exc), code="TYPED_API_QUERY", category="invalid_input")
|
|
16731
|
+
elif action == "api_surface":
|
|
16732
|
+
try:
|
|
16733
|
+
return typed_api_search.summary(project_dir)
|
|
16734
|
+
except typed_api_search.TypedApiError as exc:
|
|
16735
|
+
return _err(str(exc), code="TYPED_API_QUERY", category="invalid_input")
|
|
16736
|
+
elif action == "api_truth":
|
|
16710
16737
|
facts = lookup_api_truth(p.get("query"))
|
|
16711
16738
|
live_version = p.get("resolve_version")
|
|
16712
16739
|
if not live_version and resolve is not None:
|
|
@@ -17139,7 +17166,7 @@ def resolve_control(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
|
|
|
17139
17166
|
if err:
|
|
17140
17167
|
return _err(err)
|
|
17141
17168
|
return {"success": bool(r.ExportUserPreferencesPreset(clean["name"], clean["path"]))}
|
|
17142
|
-
return _unknown(action, ["is_studio","get_keyboard_presets","get_current_keyboard_preset","launch","runtime_mode","get_version","api_truth","check_version_support","verification_stats","report_issue","job_status","list_jobs","get_execution_trace","get_execution","list_recent_executions","begin_execution","end_execution","export_execution_report","clear_executions","inspect_operation","list_lifecycle_hooks","mcp_update_status","set_mcp_update_policy","ignore_mcp_update","snooze_mcp_update","clear_mcp_update_preferences","get_page","open_page","get_keyframe_mode","set_keyframe_mode","quit","get_fairlight_presets","set_high_priority","disable_background_tasks_for_current_session","list_user_preferences_presets","save_user_preferences_preset","load_user_preferences_preset","delete_user_preferences_preset","import_user_preferences_preset","export_user_preferences_preset","open_control_panel","control_panel_status","close_control_panel","save_state","restore_state"])
|
|
17169
|
+
return _unknown(action, ["is_studio","get_keyboard_presets","get_current_keyboard_preset","launch","runtime_mode","get_version","search_api","describe_api","api_surface","api_truth","check_version_support","verification_stats","report_issue","job_status","list_jobs","get_execution_trace","get_execution","list_recent_executions","begin_execution","end_execution","export_execution_report","clear_executions","inspect_operation","list_lifecycle_hooks","mcp_update_status","set_mcp_update_policy","ignore_mcp_update","snooze_mcp_update","clear_mcp_update_preferences","get_page","open_page","get_keyframe_mode","set_keyframe_mode","quit","get_fairlight_presets","set_high_priority","disable_background_tasks_for_current_session","list_user_preferences_presets","save_user_preferences_preset","load_user_preferences_preset","delete_user_preferences_preset","import_user_preferences_preset","export_user_preferences_preset","open_control_panel","control_panel_status","close_control_panel","save_state","restore_state"])
|
|
17143
17170
|
|
|
17144
17171
|
|
|
17145
17172
|
# ─── V2 C4: Per-field corrections with provenance + changelog ────────────────
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""Query the shipped Resolve 21.1 typed stub from inside the server.
|
|
2
|
+
|
|
3
|
+
PR #205 landed Blackmagic's `DaVinciResolveScript.pyi` in `docs/reference/`, and
|
|
4
|
+
`scripts/audit_typed_api.py` can turn it into a full inventory — but only from a
|
|
5
|
+
shell. An agent talking to this server had no way to ask what the native API
|
|
6
|
+
contains. `resolve_control api_truth` answers "what is broken"; nothing answered
|
|
7
|
+
"what exists".
|
|
8
|
+
|
|
9
|
+
The official Blackmagic MCP exposes `search_scripting_api` for this. This is the
|
|
10
|
+
equivalent, with one addition that matters more here: each result says whether
|
|
11
|
+
**this server** references the method, and where. That turns a lookup into a
|
|
12
|
+
parity check — "does the native API have it, and do we wrap it?" — which is the
|
|
13
|
+
question that drove the whole 21.1 audit.
|
|
14
|
+
|
|
15
|
+
Parsing is `ast`-based and reuses the same shapes as the audit script. Source
|
|
16
|
+
references are executable syntax only: a method named in a docstring or comment
|
|
17
|
+
is not counted as coverage.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import ast
|
|
21
|
+
import os
|
|
22
|
+
import re
|
|
23
|
+
from functools import lru_cache
|
|
24
|
+
from typing import Any, Dict, List, Optional
|
|
25
|
+
|
|
26
|
+
STUB_RELATIVE = os.path.join("docs", "reference", "DaVinciResolveScript.pyi")
|
|
27
|
+
MAX_RESULTS = 200
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TypedApiError(ValueError):
|
|
31
|
+
"""The stub is missing, unparseable, or the query was unusable."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def stub_path(project_dir: str) -> str:
|
|
35
|
+
return os.path.join(project_dir, STUB_RELATIVE)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@lru_cache(maxsize=4)
|
|
39
|
+
def _inventory(path: str, mtime: float) -> Dict[str, Any]:
|
|
40
|
+
"""Parse the stub into methods and TypedDicts. Cached on path plus mtime."""
|
|
41
|
+
with open(path, "r", encoding="utf-8") as handle:
|
|
42
|
+
text = handle.read()
|
|
43
|
+
methods: Dict[str, Any] = {}
|
|
44
|
+
option_types: Dict[str, Any] = {}
|
|
45
|
+
for cls in ast.parse(text).body:
|
|
46
|
+
if not isinstance(cls, ast.ClassDef):
|
|
47
|
+
continue
|
|
48
|
+
for node in cls.body:
|
|
49
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and not node.name.startswith("_"):
|
|
50
|
+
signature = f"{node.name}({ast.unparse(node.args)})"
|
|
51
|
+
if node.returns is not None:
|
|
52
|
+
signature += f" -> {ast.unparse(node.returns)}"
|
|
53
|
+
row = methods.setdefault(f"{cls.name}.{node.name}", {
|
|
54
|
+
"object": cls.name,
|
|
55
|
+
"method": node.name,
|
|
56
|
+
"signatures": [],
|
|
57
|
+
"stub_line": node.lineno,
|
|
58
|
+
"description": ast.get_docstring(node),
|
|
59
|
+
})
|
|
60
|
+
row["signatures"].append(signature)
|
|
61
|
+
if any(ast.unparse(base).endswith("TypedDict") for base in cls.bases):
|
|
62
|
+
fields: Dict[str, Any] = {}
|
|
63
|
+
for index, node in enumerate(cls.body):
|
|
64
|
+
if isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name):
|
|
65
|
+
doc = None
|
|
66
|
+
if index + 1 < len(cls.body):
|
|
67
|
+
following = cls.body[index + 1]
|
|
68
|
+
if (isinstance(following, ast.Expr)
|
|
69
|
+
and isinstance(following.value, ast.Constant)
|
|
70
|
+
and isinstance(following.value.value, str)):
|
|
71
|
+
doc = following.value.value
|
|
72
|
+
fields[node.target.id] = {"type": ast.unparse(node.annotation),
|
|
73
|
+
"stub_line": node.lineno,
|
|
74
|
+
"description": doc}
|
|
75
|
+
option_types[cls.name] = fields
|
|
76
|
+
if not methods:
|
|
77
|
+
raise TypedApiError("The typed stub contains no public class methods; refusing an empty inventory")
|
|
78
|
+
return {"methods": methods, "option_types": option_types}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def load(project_dir: str) -> Dict[str, Any]:
|
|
82
|
+
path = stub_path(project_dir)
|
|
83
|
+
if not os.path.isfile(path):
|
|
84
|
+
raise TypedApiError(
|
|
85
|
+
f"Typed API stub not found at {STUB_RELATIVE}. It ships with this "
|
|
86
|
+
"repository; a source checkout is required for API search."
|
|
87
|
+
)
|
|
88
|
+
return _inventory(path, os.path.getmtime(path))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@lru_cache(maxsize=4)
|
|
92
|
+
def _wrapped_names(project_dir: str, fingerprint: str) -> Dict[str, List[str]]:
|
|
93
|
+
"""Map native method name -> source files that actually call or read it.
|
|
94
|
+
|
|
95
|
+
Executable syntax only. A name that appears solely in a docstring or comment
|
|
96
|
+
is not coverage, and counting it as such is exactly the mistake this whole
|
|
97
|
+
audit existed to avoid.
|
|
98
|
+
"""
|
|
99
|
+
found: Dict[str, set] = {}
|
|
100
|
+
src_root = os.path.join(project_dir, "src")
|
|
101
|
+
for current, _dirs, files in os.walk(src_root):
|
|
102
|
+
for filename in files:
|
|
103
|
+
if not filename.endswith(".py"):
|
|
104
|
+
continue
|
|
105
|
+
absolute = os.path.join(current, filename)
|
|
106
|
+
try:
|
|
107
|
+
with open(absolute, "r", encoding="utf-8", errors="replace") as handle:
|
|
108
|
+
tree = ast.parse(handle.read())
|
|
109
|
+
except (OSError, SyntaxError):
|
|
110
|
+
continue
|
|
111
|
+
relative = os.path.relpath(absolute, project_dir).replace(os.sep, "/")
|
|
112
|
+
for node in ast.walk(tree):
|
|
113
|
+
name = None
|
|
114
|
+
if isinstance(node, ast.Attribute):
|
|
115
|
+
name = node.attr
|
|
116
|
+
elif (isinstance(node, ast.Call) and isinstance(node.func, ast.Name)
|
|
117
|
+
and node.func.id == "getattr" and len(node.args) > 1
|
|
118
|
+
and isinstance(node.args[1], ast.Constant)
|
|
119
|
+
and isinstance(node.args[1].value, str)):
|
|
120
|
+
name = node.args[1].value
|
|
121
|
+
if name:
|
|
122
|
+
found.setdefault(name, set()).add(relative)
|
|
123
|
+
return {name: sorted(paths) for name, paths in found.items()}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _source_fingerprint(project_dir: str) -> str:
|
|
127
|
+
"""Cheap change detector for the source tree: newest mtime plus file count."""
|
|
128
|
+
newest = 0.0
|
|
129
|
+
count = 0
|
|
130
|
+
for current, _dirs, files in os.walk(os.path.join(project_dir, "src")):
|
|
131
|
+
for filename in files:
|
|
132
|
+
if filename.endswith(".py"):
|
|
133
|
+
count += 1
|
|
134
|
+
try:
|
|
135
|
+
newest = max(newest, os.path.getmtime(os.path.join(current, filename)))
|
|
136
|
+
except OSError:
|
|
137
|
+
pass
|
|
138
|
+
return f"{count}:{newest}"
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def search(project_dir: str, pattern: str, *, kind: str = "all",
|
|
142
|
+
limit: int = 50) -> Dict[str, Any]:
|
|
143
|
+
"""Case-insensitive regex over method names, signatures, types and docs."""
|
|
144
|
+
if not isinstance(pattern, str) or not pattern.strip():
|
|
145
|
+
raise TypedApiError("pattern is required")
|
|
146
|
+
if kind not in ("all", "methods", "options"):
|
|
147
|
+
raise TypedApiError(f"kind must be all, methods or options, not {kind!r}")
|
|
148
|
+
try:
|
|
149
|
+
expression = re.compile(pattern, re.IGNORECASE)
|
|
150
|
+
except re.error as exc:
|
|
151
|
+
raise TypedApiError(f"pattern is not a valid regular expression: {exc}")
|
|
152
|
+
try:
|
|
153
|
+
limit = max(1, min(int(limit), MAX_RESULTS))
|
|
154
|
+
except (TypeError, ValueError):
|
|
155
|
+
raise TypedApiError("limit must be an integer")
|
|
156
|
+
|
|
157
|
+
inventory = load(project_dir)
|
|
158
|
+
wrapped = _wrapped_names(project_dir, _source_fingerprint(project_dir))
|
|
159
|
+
|
|
160
|
+
methods: List[Dict[str, Any]] = []
|
|
161
|
+
if kind in ("all", "methods"):
|
|
162
|
+
for key, row in sorted(inventory["methods"].items()):
|
|
163
|
+
haystack = " ".join([key] + row["signatures"] + [row["description"] or ""])
|
|
164
|
+
if not expression.search(haystack):
|
|
165
|
+
continue
|
|
166
|
+
references = wrapped.get(row["method"], [])
|
|
167
|
+
methods.append({
|
|
168
|
+
"symbol": key,
|
|
169
|
+
"object": row["object"],
|
|
170
|
+
"signatures": row["signatures"],
|
|
171
|
+
"stub_line": row["stub_line"],
|
|
172
|
+
"description": row["description"],
|
|
173
|
+
"referenced_in_this_server": bool(references),
|
|
174
|
+
"source_files": references[:5],
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
options: List[Dict[str, Any]] = []
|
|
178
|
+
if kind in ("all", "options"):
|
|
179
|
+
for type_name, fields in sorted(inventory["option_types"].items()):
|
|
180
|
+
matched = {
|
|
181
|
+
field: detail for field, detail in fields.items()
|
|
182
|
+
if expression.search(" ".join([type_name, field, detail["type"],
|
|
183
|
+
detail["description"] or ""]))
|
|
184
|
+
}
|
|
185
|
+
if expression.search(type_name):
|
|
186
|
+
matched = dict(fields)
|
|
187
|
+
if matched:
|
|
188
|
+
options.append({
|
|
189
|
+
"type": type_name,
|
|
190
|
+
"field_count": len(fields),
|
|
191
|
+
"matched_fields": matched,
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
total = len(methods) + len(options)
|
|
195
|
+
return {
|
|
196
|
+
"pattern": pattern,
|
|
197
|
+
"kind": kind,
|
|
198
|
+
"total_matches": total,
|
|
199
|
+
"truncated": total > limit,
|
|
200
|
+
"methods": methods[:limit],
|
|
201
|
+
"option_types": options[:limit],
|
|
202
|
+
"stub": STUB_RELATIVE,
|
|
203
|
+
"note": ("referenced_in_this_server is executable syntax only — a name that "
|
|
204
|
+
"appears solely in a docstring or comment is not counted."),
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def describe(project_dir: str, symbol: str) -> Dict[str, Any]:
|
|
209
|
+
"""Full detail for one `Class.Method` or one TypedDict name."""
|
|
210
|
+
if not isinstance(symbol, str) or not symbol.strip():
|
|
211
|
+
raise TypedApiError("symbol is required")
|
|
212
|
+
inventory = load(project_dir)
|
|
213
|
+
key = symbol.strip()
|
|
214
|
+
|
|
215
|
+
if key in inventory["option_types"]:
|
|
216
|
+
fields = inventory["option_types"][key]
|
|
217
|
+
return {"symbol": key, "kind": "option_type", "field_count": len(fields),
|
|
218
|
+
"fields": fields, "stub": STUB_RELATIVE}
|
|
219
|
+
|
|
220
|
+
row = inventory["methods"].get(key)
|
|
221
|
+
if row is None:
|
|
222
|
+
candidates = sorted(k for k in inventory["methods"] if k.split(".", 1)[1] == key)
|
|
223
|
+
if len(candidates) == 1:
|
|
224
|
+
key, row = candidates[0], inventory["methods"][candidates[0]]
|
|
225
|
+
elif candidates:
|
|
226
|
+
raise TypedApiError(
|
|
227
|
+
f"{symbol!r} exists on more than one object: {', '.join(candidates)}. "
|
|
228
|
+
"Qualify it, e.g. 'Project.GetName'."
|
|
229
|
+
)
|
|
230
|
+
else:
|
|
231
|
+
raise TypedApiError(
|
|
232
|
+
f"{symbol!r} is not in the Resolve 21.1 typed stub. Use search to "
|
|
233
|
+
"find the right name."
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
wrapped = _wrapped_names(project_dir, _source_fingerprint(project_dir))
|
|
237
|
+
references = wrapped.get(row["method"], [])
|
|
238
|
+
return {
|
|
239
|
+
"symbol": key,
|
|
240
|
+
"kind": "method",
|
|
241
|
+
"object": row["object"],
|
|
242
|
+
"signatures": row["signatures"],
|
|
243
|
+
"stub_line": row["stub_line"],
|
|
244
|
+
"description": row["description"],
|
|
245
|
+
"referenced_in_this_server": bool(references),
|
|
246
|
+
"source_files": references,
|
|
247
|
+
"stub": STUB_RELATIVE,
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def summary(project_dir: str) -> Dict[str, Any]:
|
|
252
|
+
inventory = load(project_dir)
|
|
253
|
+
field_total = sum(len(fields) for fields in inventory["option_types"].values())
|
|
254
|
+
return {
|
|
255
|
+
"stub": STUB_RELATIVE,
|
|
256
|
+
"methods": len(inventory["methods"]),
|
|
257
|
+
"option_types": len(inventory["option_types"]),
|
|
258
|
+
"option_fields": field_total,
|
|
259
|
+
"objects": sorted({row["object"] for row in inventory["methods"].values()}),
|
|
260
|
+
}
|