@xtremestuff/dsh-plugin-resolve 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +190 -2
- package/cordis.patch.yml +22 -0
- package/icon.svg +11 -0
- package/lib/index.js +561 -0
- package/package.json +52 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 xtremestuff
|
|
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.
|
package/README.md
CHANGED
|
@@ -1,3 +1,191 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-plugin-resolve
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH) bundle that bridges
|
|
4
|
+
**DaVinci Resolve** into DSH: it registers Resolve's MCP server through DSH's official
|
|
5
|
+
mcp-client bridge, so every DSH agent can inspect and drive Resolve through ordinary,
|
|
6
|
+
namespaced tools (`mcp__resolve__*`).
|
|
7
|
+
|
|
8
|
+
DaVinci Resolve Studio **21.1** ships a native MCP server (`ResolveMCP`, stdio). This bundle
|
|
9
|
+
registers that server automatically — and, when it is not available, falls back to the
|
|
10
|
+
community [`davinci-resolve-mcp`](https://github.com/samuelgursky/davinci-resolve-mcp) server
|
|
11
|
+
(37 compound / 390 granular tools over the official scripting API, Resolve 18.5+, free edition
|
|
12
|
+
reachable via its in-app bridge).
|
|
13
|
+
|
|
14
|
+
## What you get
|
|
15
|
+
|
|
16
|
+
| Backend | Source | Requires | Tools |
|
|
17
|
+
|---|---|---|---|
|
|
18
|
+
| `native` (default when present) | `ResolveMCP` bundled with Resolve Studio 21.1+ | Resolve **Studio** 21.1+, External scripting = Local | `get_resolve_status`, `get_whats_new`, `search_scripting_api`, `get_scripting_api`, `get_scripting_docs`, `launch_resolve`, `run_script`, `run_script_unsafe` |
|
|
19
|
+
| `community` (fallback) | `davinci-resolve-mcp` npm bootstrapper | Node.js (`npx`) + Resolve 18.5+ | 37 compound tools (projects, media pool, timeline editing, grading, Fusion, Fairlight, render, analysis) |
|
|
20
|
+
|
|
21
|
+
With `mode: auto` (the default) the bundle picks the native server when its executable exists
|
|
22
|
+
and falls back to the community server otherwise. If neither is available the plugin logs a
|
|
23
|
+
warning and registers nothing — the rest of DSH boots normally.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
Install `@xtremestuff/dsh-plugin-resolve` from the npm registry into your profile:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
dsh plugin --profile <name> add @xtremestuff/dsh-plugin-resolve
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
(`<name>` is the profile to extend, e.g. `web` or `desktop`.) Equivalently, use the DSH
|
|
34
|
+
Plugins page / `plugin_manager` tool (`install_bundle` with `@xtremestuff/dsh-plugin-resolve`),
|
|
35
|
+
or run `pnpm add @xtremestuff/dsh-plugin-resolve` inside `~/.dsh/profiles/<name>`.
|
|
36
|
+
|
|
37
|
+
The `dsh plugin` forwarder records the dependency and adds the bundle to the profile's
|
|
38
|
+
`dsh.profile.bundles` layer list automatically. After installation the bundle is enabled;
|
|
39
|
+
DSH reconciles the profile on the next start (or immediately in a live profile).
|
|
40
|
+
|
|
41
|
+
Verify:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
Plugins → @xtremestuff/dsh-plugin-resolve (enabled)
|
|
45
|
+
Settings → MCP servers (a "resolve" entry appears, registered by the plugin)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Then ask an agent:
|
|
49
|
+
|
|
50
|
+
> Use the Resolve MCP to report whether Resolve is running and its version. Tell me the name
|
|
51
|
+
> of the open project and how many timelines it contains. Do not change anything yet.
|
|
52
|
+
|
|
53
|
+
### Local or development install
|
|
54
|
+
|
|
55
|
+
To install from a checkout (e.g. while developing the bundle), pass the directory path to the
|
|
56
|
+
same command:
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
dsh plugin --profile <name> add /path/to/dsh-plugin-resolve
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
DSH records it as a `link:` dependency; edits to the directory are picked up without
|
|
63
|
+
re-publishing.
|
|
64
|
+
|
|
65
|
+
## Usage
|
|
66
|
+
|
|
67
|
+
There is nothing to invoke: once the bundle is enabled, Resolve's tools sit in every agent's
|
|
68
|
+
tool list as `mcp__resolve__<tool>` and the agent picks them up whenever your task involves
|
|
69
|
+
Resolve. Just have a project open in Resolve (or ask the agent to launch it) and talk.
|
|
70
|
+
|
|
71
|
+
### Native backend (Resolve Studio 21.1+)
|
|
72
|
+
|
|
73
|
+
The native server is a thin, deliberate surface: discover the scripting API, then act through
|
|
74
|
+
`run_script`. Typical flow per task:
|
|
75
|
+
|
|
76
|
+
1. **Status** — *"Is Resolve running, and which project is open?"*
|
|
77
|
+
(`mcp__resolve__get_resolve_status`; `mcp__resolve__launch_resolve` starts it if closed)
|
|
78
|
+
2. **Discover** — *"Which scripting API calls exist for adding markers to a timeline?"*
|
|
79
|
+
(`mcp__resolve__search_scripting_api` / `mcp__resolve__get_scripting_api`)
|
|
80
|
+
3. **Act** — the agent writes and runs a script against the live session
|
|
81
|
+
(`mcp__resolve__run_script`)
|
|
82
|
+
|
|
83
|
+
Examples:
|
|
84
|
+
|
|
85
|
+
> Add a blue marker at the start of every clip on the timeline named `Cut_v4`, then read back
|
|
86
|
+
> the marker count.
|
|
87
|
+
|
|
88
|
+
> Queue a ProRes 422 HQ render of the current timeline to `~/Movies/deliver` and report the job
|
|
89
|
+
> id.
|
|
90
|
+
|
|
91
|
+
> Rename every clip in the `Interviews` bin to `CAMERA_SCENE_TAKE` from its file metadata.
|
|
92
|
+
|
|
93
|
+
### Community backend
|
|
94
|
+
|
|
95
|
+
The compound server ships guarded workflow tools, so most tasks need no scripting at all — the
|
|
96
|
+
agent calls one named tool, and destructive operations run through a plan → review → confirm
|
|
97
|
+
gate:
|
|
98
|
+
|
|
99
|
+
> List all projects and open the one called `My Film`.
|
|
100
|
+
|
|
101
|
+
> Create a timeline called `Assembly Cut` from all clips in the current bin.
|
|
102
|
+
|
|
103
|
+
> Probe this timeline for gaps, overlaps, and missing media, and list what needs fixing.
|
|
104
|
+
|
|
105
|
+
> Build a ProRes 422 HQ render plan for this timeline, validate the settings, and queue the job.
|
|
106
|
+
|
|
107
|
+
> Copy the review markers from the timeline to the selected clip and export a review report.
|
|
108
|
+
|
|
109
|
+
### Working habits that pay off
|
|
110
|
+
|
|
111
|
+
- **Name things explicitly** — "the timeline named `Cut_v4`", not "this timeline".
|
|
112
|
+
- **Ask for a plan before execution** on anything destructive, and work on a **duplicated**
|
|
113
|
+
timeline until you trust the agent.
|
|
114
|
+
- **Verify in Resolve** after write actions — a successful tool call should be followed by a
|
|
115
|
+
read-back check that the intended clips, ranges, or tracks actually changed.
|
|
116
|
+
|
|
117
|
+
## Requirements
|
|
118
|
+
|
|
119
|
+
- **Native backend:** DaVinci Resolve **Studio** 21.1+ (the native MCP server is Studio-only),
|
|
120
|
+
with `Preferences → System → General → External scripting` set to **Local**.
|
|
121
|
+
Default executable locations:
|
|
122
|
+
|
|
123
|
+
| OS | Path |
|
|
124
|
+
|---|---|
|
|
125
|
+
| macOS | `/Applications/DaVinci Resolve/DaVinci Resolve.app/Contents/Applications/ResolveMCP` |
|
|
126
|
+
| Windows | `C:\Program Files\Blackmagic Design\DaVinci Resolve\ResolveMCP.exe` |
|
|
127
|
+
| Linux | `/opt/resolve/bin/ResolveMCP` |
|
|
128
|
+
|
|
129
|
+
- **Community backend:** Node.js ≥ 20.9 on `PATH` (the bundle launches
|
|
130
|
+
`npx -y davinci-resolve-mcp`) and a Python 3.10+ environment the bootstrapper can set up
|
|
131
|
+
(it installs a managed copy under your user application-data directory on first run).
|
|
132
|
+
|
|
133
|
+
## Configuration
|
|
134
|
+
|
|
135
|
+
All options are optional row config on the `resolve-mcp` entry in
|
|
136
|
+
[`cordis.patch.yml`](./cordis.patch.yml) (edit the installed copy, or overlay from your
|
|
137
|
+
profile's own `cordis.patch.yml`):
|
|
138
|
+
|
|
139
|
+
| Key | Default | Meaning |
|
|
140
|
+
|---|---|---|
|
|
141
|
+
| `mode` | `auto` | `auto` (native, else community), `native`, or `community` |
|
|
142
|
+
| `serverName` | `resolve` | Tool namespace: `mcp__<serverName>__<tool>`; must match `[A-Za-z0-9_-]{1,32}` |
|
|
143
|
+
| `nativeCommand` | per-OS default | Path to the `ResolveMCP` executable |
|
|
144
|
+
| `communityCommand` | `npx` | Bootstrapper executable (resolved against `PATH`) |
|
|
145
|
+
| `communityArgs` | `['-y', 'davinci-resolve-mcp']` | Bootstrapper arguments |
|
|
146
|
+
| `env` | — | Extra environment variables for the server process |
|
|
147
|
+
| `cwd` | — | Working directory for the server process |
|
|
148
|
+
| `toolCallTimeoutMs` | 60 000 | Per tool-call timeout |
|
|
149
|
+
| `startupTimeoutMs` | — | Budget for connection + initial tool discovery |
|
|
150
|
+
| `closeTimeoutMs` | 5 000 | Transport closure budget |
|
|
151
|
+
|
|
152
|
+
Example — force the community server with a pinned package version:
|
|
153
|
+
|
|
154
|
+
```yaml
|
|
155
|
+
- id: resolve-mcp
|
|
156
|
+
name: '@xtremestuff/dsh-plugin-resolve'
|
|
157
|
+
config:
|
|
158
|
+
mode: community
|
|
159
|
+
communityArgs: ['-y', 'davinci-resolve-mcp@4.19.3']
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## How it works
|
|
163
|
+
|
|
164
|
+
1. The bundle's [`cordis.patch.yml`](./cordis.patch.yml) mounts the plugin in
|
|
165
|
+
[`lib/index.js`](./lib/index.js).
|
|
166
|
+
2. The plugin picks the backend (native `ResolveMCP` or the community bootstrapper) and
|
|
167
|
+
registers its tools on the harness tool runtime, mirroring Blackmagic's own MCPB
|
|
168
|
+
distribution (`DaVinciResolve.mcpb`):
|
|
169
|
+
- **Native:** the tool catalog comes from a one-shot `--dump-tools` invocation at
|
|
170
|
+
activation (no persistent process); the server spawns lazily on the first tool
|
|
171
|
+
call, is killed after 5 minutes of idle, and respawns on demand.
|
|
172
|
+
- **Community:** the bootstrapper runs persistently and its tools are discovered over
|
|
173
|
+
a live MCP handshake at activation.
|
|
174
|
+
3. Tools are registered as `mcp__resolve__<tool>` with per-call timeouts and cancellation
|
|
175
|
+
support.
|
|
176
|
+
4. The registration is lifetime-scoped: disabling or uninstalling the bundle disposes the
|
|
177
|
+
transport and withdraws its tools.
|
|
178
|
+
|
|
179
|
+
## Safety notes
|
|
180
|
+
|
|
181
|
+
- `run_script` (native server) executes Python with the scripting API's privileges;
|
|
182
|
+
`run_script_unsafe` adds system access. Prefer the community server's guarded workflow tools
|
|
183
|
+
for destructive work — they implement plan → review → confirm gates and refuse
|
|
184
|
+
verified-destructive operations (e.g. `TimelineItem.CopyGrades`) until you acknowledge.
|
|
185
|
+
- Treat project names, client names, and file paths as data leaving your machine when your LLM
|
|
186
|
+
is a cloud model.
|
|
187
|
+
- Work on a duplicated timeline until you trust an agent with write access.
|
|
188
|
+
|
|
189
|
+
## License
|
|
190
|
+
|
|
191
|
+
MIT
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# dsh-plugin-resolve bundle patch.
|
|
2
|
+
#
|
|
3
|
+
# Mounts the Resolve bridge plugin. It registers DaVinci Resolve's MCP server
|
|
4
|
+
# with DSH's MCP server manager (`ctx.mcpManager`) for this bundle's lifetime,
|
|
5
|
+
# so Resolve's tools appear to DSH agents as `mcp__resolve__*`.
|
|
6
|
+
#
|
|
7
|
+
# Backend selection (config `mode`):
|
|
8
|
+
# auto (default) native ResolveMCP when present, else the community server
|
|
9
|
+
# native DaVinci Resolve Studio 21.1+ bundled `ResolveMCP` (stdio)
|
|
10
|
+
# community `davinci-resolve-mcp` npm bootstrapper (stdio, 37 compound /
|
|
11
|
+
# 390 granular tools, Resolve 18.5+, free edition via bridge)
|
|
12
|
+
#
|
|
13
|
+
# Zero-config by default. Override any field by editing this row, e.g.:
|
|
14
|
+
#
|
|
15
|
+
# config:
|
|
16
|
+
# mode: community
|
|
17
|
+
# serverName: resolve
|
|
18
|
+
# communityArgs: ['-y', 'davinci-resolve-mcp']
|
|
19
|
+
|
|
20
|
+
- insert:
|
|
21
|
+
- id: resolve-mcp
|
|
22
|
+
name: dsh-plugin-resolve
|
package/icon.svg
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
|
4
|
+
<stop offset="0" stop-color="#4D6BFE"/>
|
|
5
|
+
<stop offset="1" stop-color="#7B2FF7"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
</defs>
|
|
8
|
+
<rect x="4" y="4" width="56" height="56" rx="14" fill="#101318"/>
|
|
9
|
+
<circle cx="32" cy="32" r="20" fill="none" stroke="url(#g)" stroke-width="4"/>
|
|
10
|
+
<path d="M28 23 L43 32 L28 41 Z" fill="url(#g)"/>
|
|
11
|
+
</svg>
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-plugin-resolve — bridges DaVinci Resolve into DeepSeek Harness (DSH).
|
|
3
|
+
*
|
|
4
|
+
* This Cordis plugin registers DaVinci Resolve's MCP server with DSH's tool
|
|
5
|
+
* runtime, mirroring Blackmagic's own MCPB distribution for Claude
|
|
6
|
+
* (`DaVinciResolve.mcpb` shipped inside the Resolve app):
|
|
7
|
+
*
|
|
8
|
+
* - Native backend (Resolve Studio 21.1+ `ResolveMCP`): the tool catalog is
|
|
9
|
+
* obtained at boot with a one-shot `--dump-tools` invocation (no
|
|
10
|
+
* persistent process). The real server is spawned lazily on the first
|
|
11
|
+
* tool call, killed after an idle timeout, and respawned on demand.
|
|
12
|
+
* - Community backend (`davinci-resolve-mcp` npm bootstrapper): no
|
|
13
|
+
* `--dump-tools` support, so the server runs persistently and its tools
|
|
14
|
+
* are discovered over a live MCP handshake at activation.
|
|
15
|
+
*
|
|
16
|
+
* Tools are registered on the harness tool registry as
|
|
17
|
+
* `mcp__resolve__<tool>` (e.g. `mcp__resolve__get_resolve_status`,
|
|
18
|
+
* `mcp__resolve__run_script`). The implementation is self-contained
|
|
19
|
+
* (node: builtins only), so a `link:` development install works without a
|
|
20
|
+
* node_modules next to the checkout and a registry install adds no
|
|
21
|
+
* transitive packages.
|
|
22
|
+
*
|
|
23
|
+
* The registration lives for this plugin's lifetime: disabling or
|
|
24
|
+
* uninstalling this bundle disposes the transport and withdraws its tools.
|
|
25
|
+
*
|
|
26
|
+
* Row config (all optional):
|
|
27
|
+
* mode 'auto' | 'native' | 'community' (default 'auto')
|
|
28
|
+
* serverName tool namespace (default 'resolve')
|
|
29
|
+
* nativeCommand ResolveMCP executable path (default per platform)
|
|
30
|
+
* communityCommand bootstrapper executable (default 'npx')
|
|
31
|
+
* communityArgs bootstrapper arguments (default ['-y', 'davinci-resolve-mcp'])
|
|
32
|
+
* env extra environment for the server (object of strings)
|
|
33
|
+
* cwd working directory for the server (string)
|
|
34
|
+
* toolCallTimeoutMs per tool-call timeout (number, ms)
|
|
35
|
+
*
|
|
36
|
+
* @module dsh-plugin-resolve
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { spawn, spawnSync } from 'node:child_process'
|
|
40
|
+
import { statSync } from 'node:fs'
|
|
41
|
+
|
|
42
|
+
/** Cordis plugin name used by loader diagnostics. */
|
|
43
|
+
export const name = 'dsh-plugin-resolve'
|
|
44
|
+
|
|
45
|
+
/** Services required by this plugin (tool registration). */
|
|
46
|
+
export const inject = ['tools']
|
|
47
|
+
|
|
48
|
+
/** Default `ResolveMCP` executable locations shipped with Resolve Studio 21.1+. */
|
|
49
|
+
const NATIVE_DEFAULTS = {
|
|
50
|
+
darwin: '/Applications/DaVinci Resolve/DaVinci Resolve.app/Contents/Applications/ResolveMCP',
|
|
51
|
+
win32: 'C:\\Program Files\\Blackmagic Design\\DaVinci Resolve\\ResolveMCP.exe',
|
|
52
|
+
linux: '/opt/resolve/bin/ResolveMCP',
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Default community-server launch: the npm bootstrapper over stdio. */
|
|
56
|
+
const COMMUNITY_DEFAULT_COMMAND = 'npx'
|
|
57
|
+
const COMMUNITY_DEFAULT_ARGS = ['-y', 'davinci-resolve-mcp']
|
|
58
|
+
|
|
59
|
+
/** MCP `serverName` pattern enforced by DSH's tool naming. */
|
|
60
|
+
const SERVER_NAME_PATTERN = /^[A-Za-z0-9_-]{1,32}$/
|
|
61
|
+
|
|
62
|
+
/** Budgets, aligned with Blackmagic's MCPB wrapper. */
|
|
63
|
+
const DUMP_TIMEOUT_MS = 10_000
|
|
64
|
+
const INIT_TIMEOUT_MS = 30_000
|
|
65
|
+
const IDLE_TIMEOUT_MS = 5 * 60 * 1000
|
|
66
|
+
const DEFAULT_TOOL_CALL_TIMEOUT_MS = 60_000
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Resolve a bare executable name against `PATH`.
|
|
70
|
+
* @param {string} command - absolute/relative path or bare command name.
|
|
71
|
+
* @returns {string | undefined} the usable path, or undefined when not found.
|
|
72
|
+
*/
|
|
73
|
+
function resolveExecutable(command) {
|
|
74
|
+
if (command.includes('/') || command.includes('\\')) {
|
|
75
|
+
return isExecutable(command) ? command : undefined
|
|
76
|
+
}
|
|
77
|
+
const pathEnv = process.env.PATH ?? ''
|
|
78
|
+
for (const dir of pathEnv.split(':')) {
|
|
79
|
+
if (!dir) continue
|
|
80
|
+
const candidate = `${dir}/${command}`
|
|
81
|
+
if (isExecutable(candidate)) return candidate
|
|
82
|
+
}
|
|
83
|
+
return undefined
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @param {string} path
|
|
88
|
+
* @returns {boolean} true when the path is an existing regular file.
|
|
89
|
+
*/
|
|
90
|
+
function isExecutable(path) {
|
|
91
|
+
try {
|
|
92
|
+
return statSync(path).isFile()
|
|
93
|
+
} catch {
|
|
94
|
+
return false
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Rewrite an MCP input schema into DSH's supported JSON-Schema subset.
|
|
100
|
+
* DSH rejects `type` arrays (e.g. `["integer","string"]` on the native
|
|
101
|
+
* server's thumbnail frame-offset/timecode params). A single-type array
|
|
102
|
+
* collapses to that type; a true union drops `type` so the parameter is
|
|
103
|
+
* left unconstrained for DSH while the MCP server validates it.
|
|
104
|
+
* @param {unknown} node
|
|
105
|
+
* @returns {unknown} a sanitized copy of the schema node.
|
|
106
|
+
*/
|
|
107
|
+
function sanitizeSchema(node) {
|
|
108
|
+
if (!node || typeof node !== 'object' || Array.isArray(node)) return node
|
|
109
|
+
const out = { ...node }
|
|
110
|
+
if (Array.isArray(out.type)) {
|
|
111
|
+
const supported = out.type.filter((t) => typeof t === 'string')
|
|
112
|
+
if (supported.length === 1) out.type = supported[0]
|
|
113
|
+
else delete out.type
|
|
114
|
+
}
|
|
115
|
+
if (out.properties && typeof out.properties === 'object' && !Array.isArray(out.properties)) {
|
|
116
|
+
out.properties = Object.fromEntries(
|
|
117
|
+
Object.entries(out.properties).map(([key, value]) => [key, sanitizeSchema(value)]),
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
if (out.items !== undefined) out.items = sanitizeSchema(out.items)
|
|
121
|
+
return out
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Pick the backend and its stdio launch spec for the configured mode.
|
|
126
|
+
* @param {object} config - resolved row config.
|
|
127
|
+
* @returns {{kind: 'native'|'community', command: string, args: string[]} | undefined}
|
|
128
|
+
*/
|
|
129
|
+
function pickBackend(config) {
|
|
130
|
+
const mode = config.mode ?? 'auto'
|
|
131
|
+
if (mode !== 'auto' && mode !== 'native' && mode !== 'community') {
|
|
132
|
+
throw new Error(`dsh-plugin-resolve: unknown mode "${mode}" (expected 'auto', 'native', or 'community')`)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (mode === 'native' || mode === 'auto') {
|
|
136
|
+
const command = config.nativeCommand ?? NATIVE_DEFAULTS[process.platform]
|
|
137
|
+
const resolved = command ? resolveExecutable(command) : undefined
|
|
138
|
+
if (resolved) return { kind: 'native', command: resolved, args: [] }
|
|
139
|
+
if (mode === 'native') return undefined
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (mode === 'community' || mode === 'auto') {
|
|
143
|
+
const command = config.communityCommand ?? COMMUNITY_DEFAULT_COMMAND
|
|
144
|
+
const resolved = command ? resolveExecutable(command) : undefined
|
|
145
|
+
if (resolved) {
|
|
146
|
+
const args = Array.isArray(config.communityArgs) ? config.communityArgs : COMMUNITY_DEFAULT_ARGS
|
|
147
|
+
return { kind: 'community', command: resolved, args }
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return undefined
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Shared stdio JSON-RPC machinery for one server child process.
|
|
156
|
+
* Newline-delimited JSON-RPC over stdin/stdout; pending responses are
|
|
157
|
+
* tracked by id, and client lines may be queued until the child finishes
|
|
158
|
+
* its internal handshake.
|
|
159
|
+
*/
|
|
160
|
+
class StdioJsonRpc {
|
|
161
|
+
/**
|
|
162
|
+
* @param {object} opts
|
|
163
|
+
* @param {string} opts.command
|
|
164
|
+
* @param {string[]} opts.args
|
|
165
|
+
* @param {Record<string, string>} [opts.env]
|
|
166
|
+
* @param {string} [opts.cwd]
|
|
167
|
+
* @param {(message: string) => void} [opts.onStderr]
|
|
168
|
+
* @param {(code: number|null, signal: string) => void} [opts.onExit]
|
|
169
|
+
*/
|
|
170
|
+
constructor(opts) {
|
|
171
|
+
this.child = spawn(opts.command, opts.args, {
|
|
172
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
173
|
+
env: opts.env ? { ...process.env, ...opts.env } : process.env,
|
|
174
|
+
cwd: opts.cwd,
|
|
175
|
+
})
|
|
176
|
+
this.nextId = 1
|
|
177
|
+
/** @type {Map<number, {resolve: (v: any) => void, reject: (e: Error) => void}>} */
|
|
178
|
+
this.pending = new Map()
|
|
179
|
+
this.queued = []
|
|
180
|
+
this.buffer = ''
|
|
181
|
+
this.initialized = false
|
|
182
|
+
this.closed = false
|
|
183
|
+
this.child.on('exit', (code, signal) => {
|
|
184
|
+
this.#settleAll(`server exited (code=${code} signal=${signal})`)
|
|
185
|
+
if (opts.onExit) opts.onExit(code, signal)
|
|
186
|
+
})
|
|
187
|
+
this.child.on('error', (error) => {
|
|
188
|
+
this.#settleAll(`server process error: ${String(error)}`)
|
|
189
|
+
})
|
|
190
|
+
this.child.stdout.on('data', (chunk) => this.#onData(chunk))
|
|
191
|
+
if (opts.onStderr) this.child.stderr.on('data', (chunk) => opts.onStderr(String(chunk)))
|
|
192
|
+
else this.child.stderr.on('data', () => {}) // drain: a full stderr pipe deadlocks the server
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** @param {Buffer|String} chunk */
|
|
196
|
+
#onData(chunk) {
|
|
197
|
+
this.buffer += chunk.toString()
|
|
198
|
+
let newline
|
|
199
|
+
while ((newline = this.buffer.indexOf('\n')) >= 0) {
|
|
200
|
+
const line = this.buffer.slice(0, newline).trim()
|
|
201
|
+
this.buffer = this.buffer.slice(newline + 1)
|
|
202
|
+
if (!line) continue
|
|
203
|
+
let message
|
|
204
|
+
try {
|
|
205
|
+
message = JSON.parse(line)
|
|
206
|
+
} catch {
|
|
207
|
+
continue
|
|
208
|
+
}
|
|
209
|
+
if (message.id === '__wrapper_init__') {
|
|
210
|
+
this.initialized = true
|
|
211
|
+
this.#flushQueue()
|
|
212
|
+
continue
|
|
213
|
+
}
|
|
214
|
+
if (message.id !== undefined && this.pending.has(message.id)) {
|
|
215
|
+
const waiter = this.pending.get(message.id)
|
|
216
|
+
this.pending.delete(message.id)
|
|
217
|
+
if (message.error !== undefined) waiter.reject(new Error(message.error.message ?? 'MCP RPC error'))
|
|
218
|
+
else waiter.resolve(message.result)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Send a raw JSON-RPC line, queueing it until the child is initialized. */
|
|
224
|
+
sendLine(line) {
|
|
225
|
+
if (this.closed) return
|
|
226
|
+
if (this.initialized) this.child.stdin.write(line + '\n')
|
|
227
|
+
else this.queued.push(line)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Perform the internal handshake (Blackmagic `__wrapper_init__` convention). */
|
|
231
|
+
start() {
|
|
232
|
+
// Written directly (not queued): the init response is what unlocks the queue.
|
|
233
|
+
this.child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id: '__wrapper_init__', method: 'initialize', params: {} }) + '\n')
|
|
234
|
+
this.child.stdin.write(JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }) + '\n')
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
#flushQueue() {
|
|
238
|
+
const lines = this.queued
|
|
239
|
+
this.queued = []
|
|
240
|
+
for (const line of lines) this.child.stdin.write(line + '\n')
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Send a JSON-RPC request and await its response.
|
|
245
|
+
* @param {string} method
|
|
246
|
+
* @param {object} [params]
|
|
247
|
+
* @param {number} [timeoutMs]
|
|
248
|
+
*/
|
|
249
|
+
request(method, params, timeoutMs = 15_000) {
|
|
250
|
+
if (this.closed) return Promise.reject(new Error('MCP transport is closed'))
|
|
251
|
+
const id = this.nextId++
|
|
252
|
+
this.sendLine(JSON.stringify({ jsonrpc: '2.0', id, method, params: params ?? {} }))
|
|
253
|
+
return new Promise((resolve, reject) => {
|
|
254
|
+
const timer = setTimeout(() => {
|
|
255
|
+
if (this.pending.delete(id)) reject(new Error(`MCP request "${method}" timed out after ${timeoutMs}ms`))
|
|
256
|
+
}, timeoutMs)
|
|
257
|
+
this.pending.set(id, {
|
|
258
|
+
resolve: (value) => { clearTimeout(timer); resolve(value) },
|
|
259
|
+
reject: (error) => { clearTimeout(timer); reject(error) },
|
|
260
|
+
})
|
|
261
|
+
})
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** @param {string} reason */
|
|
265
|
+
#settleAll(reason) {
|
|
266
|
+
if (this.closed && this.pending.size === 0) return
|
|
267
|
+
for (const [id, waiter] of this.pending) {
|
|
268
|
+
this.pending.delete(id)
|
|
269
|
+
waiter.reject(new Error(reason))
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Kill the child process and settle every pending request. */
|
|
274
|
+
dispose() {
|
|
275
|
+
this.closed = true
|
|
276
|
+
this.#settleAll('MCP transport disposed')
|
|
277
|
+
try { this.child.stdin.end() } catch { /* already gone */ }
|
|
278
|
+
this.child.kill()
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Lazy stdio server following Blackmagic's MCPB wrapper: the process is
|
|
284
|
+
* spawned on first use, killed after an idle timeout, and respawned on
|
|
285
|
+
* demand. Callers get a stable `callTool` surface.
|
|
286
|
+
*/
|
|
287
|
+
class LazyStdioServer {
|
|
288
|
+
/**
|
|
289
|
+
* @param {object} opts
|
|
290
|
+
* @param {string} opts.command
|
|
291
|
+
* @param {string[]} opts.args
|
|
292
|
+
* @param {Record<string, string>} [opts.env]
|
|
293
|
+
* @param {string} [opts.cwd]
|
|
294
|
+
* @param {(message: string) => void} [opts.onLog]
|
|
295
|
+
* @param {number} [opts.idleTimeoutMs]
|
|
296
|
+
* @param {number} [opts.initTimeoutMs]
|
|
297
|
+
*/
|
|
298
|
+
constructor(opts) {
|
|
299
|
+
this.opts = opts
|
|
300
|
+
this.rpc = null
|
|
301
|
+
this.idleTimer = null
|
|
302
|
+
this.initTimer = null
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
#resetIdleTimer() {
|
|
306
|
+
if (this.idleTimer) clearTimeout(this.idleTimer)
|
|
307
|
+
this.idleTimer = setTimeout(() => {
|
|
308
|
+
if (this.rpc) {
|
|
309
|
+
this.opts.onLog?.('ResolveMCP: idle timeout, stopping server')
|
|
310
|
+
this.#stopChild()
|
|
311
|
+
}
|
|
312
|
+
}, this.opts.idleTimeoutMs ?? IDLE_TIMEOUT_MS)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
#stopChild() {
|
|
316
|
+
this.rpc?.dispose()
|
|
317
|
+
this.rpc = null
|
|
318
|
+
if (this.idleTimer) { clearTimeout(this.idleTimer); this.idleTimer = null }
|
|
319
|
+
if (this.initTimer) { clearTimeout(this.initTimer); this.initTimer = null }
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Spawn (if needed) and initialize the child, waiting for readiness. */
|
|
323
|
+
async #ensureStarted() {
|
|
324
|
+
if (this.rpc && this.rpc.initialized) {
|
|
325
|
+
this.#resetIdleTimer()
|
|
326
|
+
return
|
|
327
|
+
}
|
|
328
|
+
if (this.rpc && !this.rpc.initialized) {
|
|
329
|
+
// Already spawning; wait for the in-flight handshake.
|
|
330
|
+
this.#resetIdleTimer()
|
|
331
|
+
await new Promise((resolve) => {
|
|
332
|
+
const check = () => (this.rpc?.initialized ? resolve() : setTimeout(check, 50))
|
|
333
|
+
check()
|
|
334
|
+
})
|
|
335
|
+
return
|
|
336
|
+
}
|
|
337
|
+
const rpc = new StdioJsonRpc({
|
|
338
|
+
command: this.opts.command,
|
|
339
|
+
args: this.opts.args,
|
|
340
|
+
env: { ...(this.opts.env ?? {}), BMD_IS_MCPB: '1' },
|
|
341
|
+
cwd: this.opts.cwd,
|
|
342
|
+
onStderr: (message) => this.opts.onLog?.(message.trimEnd()),
|
|
343
|
+
onExit: () => {
|
|
344
|
+
if (this.rpc === rpc) {
|
|
345
|
+
this.rpc = null
|
|
346
|
+
if (this.initTimer) { clearTimeout(this.initTimer); this.initTimer = null }
|
|
347
|
+
if (this.idleTimer) { clearTimeout(this.idleTimer); this.idleTimer = null }
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
})
|
|
351
|
+
this.rpc = rpc
|
|
352
|
+
rpc.start()
|
|
353
|
+
this.#resetIdleTimer()
|
|
354
|
+
await new Promise((resolve, reject) => {
|
|
355
|
+
this.initTimer = setTimeout(() => {
|
|
356
|
+
this.initTimer = null
|
|
357
|
+
reject(new Error(`MCP server did not initialize within ${INIT_TIMEOUT_MS / 1000}s`))
|
|
358
|
+
this.#stopChild()
|
|
359
|
+
}, this.opts.initTimeoutMs ?? INIT_TIMEOUT_MS)
|
|
360
|
+
const check = () => {
|
|
361
|
+
if (this.rpc !== rpc) { reject(new Error('MCP server process died during startup')); return }
|
|
362
|
+
if (rpc.initialized) {
|
|
363
|
+
if (this.initTimer) { clearTimeout(this.initTimer); this.initTimer = null }
|
|
364
|
+
resolve()
|
|
365
|
+
} else setTimeout(check, 25)
|
|
366
|
+
}
|
|
367
|
+
check()
|
|
368
|
+
})
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Call one tool, spawning the server lazily.
|
|
373
|
+
* @param {string} toolName
|
|
374
|
+
* @param {object} [args]
|
|
375
|
+
* @param {number} [timeoutMs]
|
|
376
|
+
* @param {AbortSignal} [signal]
|
|
377
|
+
*/
|
|
378
|
+
async callTool(toolName, args, timeoutMs = DEFAULT_TOOL_CALL_TIMEOUT_MS, signal) {
|
|
379
|
+
if (signal?.aborted) throw new Error('aborted')
|
|
380
|
+
await this.#ensureStarted()
|
|
381
|
+
const rpc = this.rpc
|
|
382
|
+
if (!rpc) throw new Error('MCP server unavailable')
|
|
383
|
+
this.#resetIdleTimer()
|
|
384
|
+
const promise = rpc.request('tools/call', { name: toolName, arguments: args ?? {} }, timeoutMs)
|
|
385
|
+
if (signal) signal.addEventListener('abort', () => {
|
|
386
|
+
// JSON-RPC has no cancel; drop the waiter so the caller's signal wins.
|
|
387
|
+
for (const [id, waiter] of rpc.pending) {
|
|
388
|
+
rpc.pending.delete(id)
|
|
389
|
+
waiter.reject(new Error('aborted'))
|
|
390
|
+
}
|
|
391
|
+
}, { once: true })
|
|
392
|
+
return promise
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** Stop the child (if running) and release timers. */
|
|
396
|
+
dispose() {
|
|
397
|
+
this.#stopChild()
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* One-shot `--dump-tools` probe of the native ResolveMCP binary
|
|
403
|
+
* (Blackmagic MCPB convention). Returns the live tool definitions without
|
|
404
|
+
* keeping a server process alive.
|
|
405
|
+
* @param {string} command
|
|
406
|
+
* @returns {{tools: any[], instructions?: string} | undefined}
|
|
407
|
+
*/
|
|
408
|
+
function dumpTools(command) {
|
|
409
|
+
try {
|
|
410
|
+
const result = spawnSync(command, ['--dump-tools'], {
|
|
411
|
+
env: { ...process.env, BMD_IS_MCPB: '1' },
|
|
412
|
+
timeout: DUMP_TIMEOUT_MS,
|
|
413
|
+
encoding: 'utf-8',
|
|
414
|
+
windowsHide: true,
|
|
415
|
+
})
|
|
416
|
+
if (result.status === 0 && result.stdout) {
|
|
417
|
+
const dump = JSON.parse(result.stdout)
|
|
418
|
+
return { tools: dump.tools ?? [], instructions: dump.instructions }
|
|
419
|
+
}
|
|
420
|
+
} catch {
|
|
421
|
+
// fall through: the caller decides how to recover
|
|
422
|
+
}
|
|
423
|
+
return undefined
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Extract the user-facing text from an MCP content array.
|
|
428
|
+
* @param {any} content
|
|
429
|
+
* @returns {string}
|
|
430
|
+
*/
|
|
431
|
+
function extractContentText(content) {
|
|
432
|
+
if (!Array.isArray(content)) return ''
|
|
433
|
+
return content
|
|
434
|
+
.map((part) => (part && part.type === 'text' && typeof part.text === 'string' ? part.text : ''))
|
|
435
|
+
.join('\n')
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Register DaVinci Resolve's MCP server with DSH.
|
|
440
|
+
*
|
|
441
|
+
* Fails safe: when no backend is available (Resolve not installed, free
|
|
442
|
+
* edition without the community server, no `npx`) the plugin logs a warning
|
|
443
|
+
* and registers nothing, so the rest of the harness boots normally.
|
|
444
|
+
*
|
|
445
|
+
* @param {import('@deepseek-ai/cordis').Context} ctx - plugin context.
|
|
446
|
+
* @param {object} [config] - resolved row config.
|
|
447
|
+
*/
|
|
448
|
+
export async function apply(ctx, config = {}) {
|
|
449
|
+
const log = ctx.logger
|
|
450
|
+
|
|
451
|
+
const serverName = String(config.serverName ?? 'resolve')
|
|
452
|
+
if (!SERVER_NAME_PATTERN.test(serverName)) {
|
|
453
|
+
throw new Error(`dsh-plugin-resolve: serverName "${serverName}" must match [A-Za-z0-9_-]{1,32}`)
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const backend = pickBackend(config)
|
|
457
|
+
if (!backend) {
|
|
458
|
+
const hint =
|
|
459
|
+
config.mode === 'native'
|
|
460
|
+
? 'Install DaVinci Resolve Studio 21.1+ (the native MCP server is Studio-only) and set config.nativeCommand if it lives elsewhere.'
|
|
461
|
+
: config.mode === 'community'
|
|
462
|
+
? 'Install Node.js (npx) or set config.communityCommand to the davinci-resolve-mcp bootstrapper.'
|
|
463
|
+
: 'Install DaVinci Resolve Studio 21.1+ (native ResolveMCP) or Node.js (community server via npx).'
|
|
464
|
+
log.warn(`dsh-plugin-resolve: no Resolve MCP backend found on this machine. ${hint} No server registered.`)
|
|
465
|
+
return
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const toolCallTimeoutMs = typeof config.toolCallTimeoutMs === 'number' ? config.toolCallTimeoutMs : DEFAULT_TOOL_CALL_TIMEOUT_MS
|
|
469
|
+
const env = config.env && typeof config.env === 'object' ? config.env : undefined
|
|
470
|
+
const cwd = typeof config.cwd === 'string' && config.cwd ? config.cwd : undefined
|
|
471
|
+
const onLog = (message) => log.debug(`resolve-mcp: ${message}`)
|
|
472
|
+
|
|
473
|
+
/** @type {LazyStdioServer|StdioJsonRpc|null} */
|
|
474
|
+
let server = null
|
|
475
|
+
/** @type {Array<{name: string}>} */
|
|
476
|
+
let tools = []
|
|
477
|
+
|
|
478
|
+
try {
|
|
479
|
+
if (backend.kind === 'native') {
|
|
480
|
+
// Blackmagic pattern: one-shot dump for the catalog, lazy process for calls.
|
|
481
|
+
const dump = dumpTools(backend.command)
|
|
482
|
+
if (dump && dump.tools.length > 0) {
|
|
483
|
+
tools = dump.tools
|
|
484
|
+
server = new LazyStdioServer({ command: backend.command, args: backend.args, env, cwd, onLog })
|
|
485
|
+
} else {
|
|
486
|
+
log.warn('dsh-plugin-resolve: --dump-tools unavailable; falling back to a persistent server for tool discovery.')
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (server === null) {
|
|
491
|
+
// Community backend (or native dump failure): persistent server + live discovery.
|
|
492
|
+
const rpc = new StdioJsonRpc({ command: backend.command, args: backend.args, env, cwd, onStderr: onLog })
|
|
493
|
+
rpc.start()
|
|
494
|
+
const initOk = await new Promise((resolve) => {
|
|
495
|
+
const timer = setTimeout(() => resolve(false), INIT_TIMEOUT_MS)
|
|
496
|
+
const check = () => (rpc.initialized ? (clearTimeout(timer), resolve(true)) : setTimeout(check, 25))
|
|
497
|
+
check()
|
|
498
|
+
})
|
|
499
|
+
if (!initOk) throw new Error('MCP server did not initialize within 30s')
|
|
500
|
+
const listed = await rpc.request('tools/list', {}, INIT_TIMEOUT_MS)
|
|
501
|
+
tools = listed?.tools ?? []
|
|
502
|
+
server = rpc
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/** @type {Array<() => void>} */
|
|
506
|
+
const disposers = []
|
|
507
|
+
for (const tool of tools) {
|
|
508
|
+
const publicName = `mcp__${serverName}__${tool.name}`
|
|
509
|
+
const definition = {
|
|
510
|
+
name: publicName,
|
|
511
|
+
description: tool.description ?? '',
|
|
512
|
+
parameters: sanitizeSchema(tool.inputSchema) ?? { type: 'object', properties: {} },
|
|
513
|
+
output: {
|
|
514
|
+
schema: {
|
|
515
|
+
type: 'object',
|
|
516
|
+
properties: {
|
|
517
|
+
content: { type: 'array', items: {} },
|
|
518
|
+
structuredContent: {},
|
|
519
|
+
},
|
|
520
|
+
required: ['content'],
|
|
521
|
+
additionalProperties: false,
|
|
522
|
+
},
|
|
523
|
+
render: (_args, value) => [{ type: 'text', text: extractContentText(value?.content) }],
|
|
524
|
+
},
|
|
525
|
+
execute: async (args, exec) => {
|
|
526
|
+
const result = await server.callTool(tool.name, args ?? {}, toolCallTimeoutMs, exec?.signal)
|
|
527
|
+
if (result?.isError === true) {
|
|
528
|
+
const text = Array.isArray(result.content)
|
|
529
|
+
? result.content.map((part) => (part?.type === 'text' ? part.text : '')).join('\n')
|
|
530
|
+
: 'tool reported an error'
|
|
531
|
+
throw new Error(text || 'tool reported an error')
|
|
532
|
+
}
|
|
533
|
+
return {
|
|
534
|
+
content: result?.content ?? [],
|
|
535
|
+
...(result?.structuredContent !== undefined ? { structuredContent: result.structuredContent } : {}),
|
|
536
|
+
}
|
|
537
|
+
},
|
|
538
|
+
}
|
|
539
|
+
disposers.push(ctx.tools.register(definition))
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Lifetime scope: uninstalling the bundle disposes the transport and
|
|
543
|
+
// withdraws every registered tool. `ctx.effect(execute, label)` runs
|
|
544
|
+
// `execute` at registration and collects whatever it *returns* as the
|
|
545
|
+
// teardown hook — so return the disposer from a thunk.
|
|
546
|
+
ctx.effect(() => () => {
|
|
547
|
+
for (const dispose of disposers.reverse()) {
|
|
548
|
+
try { dispose() } catch { /* already withdrawn */ }
|
|
549
|
+
}
|
|
550
|
+
server.dispose()
|
|
551
|
+
}, 'dsh-plugin-resolve.mcp')
|
|
552
|
+
|
|
553
|
+
log.info(
|
|
554
|
+
`dsh-plugin-resolve: registered ${tools.length} Resolve tools as mcp__${serverName}__* via ${backend.kind} backend ` +
|
|
555
|
+
`(${backend.command} ${backend.args.join(' ')}${backend.kind === 'native' ? ', lazy spawn on first call, idle kill after 5 min' : ''}).`,
|
|
556
|
+
)
|
|
557
|
+
} catch (error) {
|
|
558
|
+
server?.dispose()
|
|
559
|
+
log.warn(`dsh-plugin-resolve: could not start the Resolve MCP server (${backend.kind} backend at ${backend.command}): ${String(error)}. No tools registered.`)
|
|
560
|
+
}
|
|
561
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,54 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xtremestuff/dsh-plugin-resolve",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "DSH bundle that bridges DaVinci Resolve into DSH: registers Resolve 21.1+ native ResolveMCP (stdio) or the davinci-resolve-mcp community server with DSH's mcp-client bridge, exposing Resolve's tools as first-class agent tools.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./lib/index.js",
|
|
9
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
10
|
+
"./package.json": "./package.json"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"lib/",
|
|
14
|
+
"cordis.patch.yml",
|
|
15
|
+
"icon.svg",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"keywords": [
|
|
20
|
+
"dsh",
|
|
21
|
+
"deepseek-harness",
|
|
22
|
+
"bundle",
|
|
23
|
+
"plugin",
|
|
24
|
+
"davinci-resolve",
|
|
25
|
+
"mcp",
|
|
26
|
+
"video-editing",
|
|
27
|
+
"post-production"
|
|
28
|
+
],
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"author": "xtremestuff",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/xtremestuff/dsh-plugin-resolve.git"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://github.com/xtremestuff/dsh-plugin-resolve#readme",
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/xtremestuff/dsh-plugin-resolve/issues"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=20",
|
|
44
|
+
"dsh": ">=0.2.0-rc.2"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@deepseek-ai/cordis": "^4.1.0"
|
|
48
|
+
},
|
|
49
|
+
"dsh": {
|
|
50
|
+
"bundle": {
|
|
51
|
+
"patch": "./cordis.patch.yml"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|