dcc-mcp-openscreen 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,212 @@
1
+ name: Release
2
+
3
+ # Batched release cadence.
4
+ #
5
+ # release-please runs on a fixed daily window instead of on every push to main,
6
+ # so a single version carries every feat/fix merged since the previous tag
7
+ # rather than one version per merge. `workflow_dispatch` stays available as the
8
+ # emergency release and asset-backfill channel.
9
+ #
10
+ # release-please creates tags from merged-but-untagged release PRs, so the
11
+ # GitHub Release is cut on the first window after the release PR is merged
12
+ # instead of on the merge push itself.
13
+ on:
14
+ schedule:
15
+ # 03:29 UTC daily. Deliberately off the hour: GitHub queues scheduled
16
+ # runs and on-the-hour crons are the most delayed during peak load.
17
+ # CONTRACT: this window stays AFTER the dcc-mcp-core release window
18
+ # (01:13 UTC) so a release never packages a stale core.
19
+ - cron: "29 3 * * *"
20
+ workflow_dispatch:
21
+ inputs:
22
+ tag_name:
23
+ description: "Tag name for manual asset build, for example v0.1.0. Leave empty to only run release-please."
24
+ required: false
25
+ default: ""
26
+ publish_to_pypi:
27
+ description: "Publish rebuilt dist artifacts for tag_name to PyPI. Use to backfill an existing GitHub release."
28
+ required: false
29
+ type: boolean
30
+ default: false
31
+
32
+ concurrency:
33
+ group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}${{ inputs.tag_name && format('-{0}', inputs.tag_name) || '' }}
34
+ cancel-in-progress: false
35
+
36
+ permissions:
37
+ contents: read
38
+
39
+ jobs:
40
+ release-please:
41
+ name: Release Please
42
+ runs-on: ubuntu-latest
43
+ if: github.ref == 'refs/heads/main'
44
+ permissions:
45
+ contents: write
46
+ pull-requests: write
47
+ outputs:
48
+ release_created: ${{ steps.release.outputs.release_created }}
49
+ tag_name: ${{ steps.target.outputs.tag_name }}
50
+ tag_sha: ${{ steps.target.outputs.tag_sha }}
51
+ version: ${{ steps.target.outputs.version }}
52
+ steps:
53
+ # A PAT (user-owned) is required: PRs opened with the default
54
+ # GITHUB_TOKEN do not trigger pull_request workflows, so CI would never
55
+ # run on release PRs.
56
+ - uses: googleapis/release-please-action@v5
57
+ id: release
58
+ # On an explicit dispatch target the requested tag is authoritative.
59
+ # Running release-please here can create a new release whose tag then
60
+ # silently wins over the requested one in RELEASE_TAG below, so the
61
+ # operator asking to backfill v0.1.5 would get v0.1.6 published instead.
62
+ if: github.event_name != 'workflow_dispatch' || inputs.tag_name == ''
63
+ with:
64
+ token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
65
+ config-file: release-please-config.json
66
+ manifest-file: .release-please-manifest.json
67
+ - name: Resolve immutable release target
68
+ id: target
69
+ if: steps.release.outputs.release_created == 'true' || (github.event_name == 'workflow_dispatch' && inputs.tag_name != '')
70
+ env:
71
+ GH_TOKEN: ${{ github.token }}
72
+ RELEASE_TAG: ${{ steps.release.outputs.tag_name || inputs.tag_name }}
73
+ RELEASE_VERSION: ${{ steps.release.outputs.version }}
74
+ shell: bash
75
+ run: |
76
+ set -euo pipefail
77
+ if [[ ! "$RELEASE_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
78
+ echo "Release target must be a canonical vMAJOR.MINOR.PATCH tag." >&2
79
+ exit 1
80
+ fi
81
+ version="${RELEASE_TAG#v}"
82
+ if [[ -n "$RELEASE_VERSION" && "$RELEASE_VERSION" != "$version" ]]; then
83
+ echo "Release Please version does not match its tag." >&2
84
+ exit 1
85
+ fi
86
+ object_type="$(gh api "repos/${GITHUB_REPOSITORY}/git/ref/tags/${RELEASE_TAG}" --jq .object.type)"
87
+ object_sha="$(gh api "repos/${GITHUB_REPOSITORY}/git/ref/tags/${RELEASE_TAG}" --jq .object.sha)"
88
+ for _ in 1 2 3 4; do
89
+ if [[ "$object_type" == "commit" ]]; then
90
+ break
91
+ fi
92
+ if [[ "$object_type" != "tag" ]]; then
93
+ echo "Release tag does not resolve to a commit." >&2
94
+ exit 1
95
+ fi
96
+ tag_json="$(gh api "repos/${GITHUB_REPOSITORY}/git/tags/${object_sha}")"
97
+ object_type="$(jq -r .object.type <<<"$tag_json")"
98
+ object_sha="$(jq -r .object.sha <<<"$tag_json")"
99
+ done
100
+ if [[ "$object_type" != "commit" || ! "$object_sha" =~ ^[0-9a-f]{40}$ ]]; then
101
+ echo "Release tag commit identity is invalid." >&2
102
+ exit 1
103
+ fi
104
+ echo "tag_name=$RELEASE_TAG" >> "$GITHUB_OUTPUT"
105
+ echo "tag_sha=$object_sha" >> "$GITHUB_OUTPUT"
106
+ echo "version=$version" >> "$GITHUB_OUTPUT"
107
+
108
+ build:
109
+ name: Build wheel and sdist
110
+ runs-on: ubuntu-latest
111
+ needs: release-please
112
+ if: needs.release-please.outputs.release_created == 'true' || (github.event_name == 'workflow_dispatch' && inputs.tag_name != '')
113
+ steps:
114
+ - uses: actions/checkout@v6
115
+ with:
116
+ ref: ${{ needs.release-please.outputs.tag_name }}
117
+ fetch-depth: 0
118
+ - name: Verify immutable release target
119
+ env:
120
+ EXPECTED_TAG: ${{ needs.release-please.outputs.tag_name }}
121
+ EXPECTED_SHA: ${{ needs.release-please.outputs.tag_sha }}
122
+ EXPECTED_VERSION: ${{ needs.release-please.outputs.version }}
123
+ shell: bash
124
+ run: |
125
+ set -euo pipefail
126
+ actual_sha="$(git rev-parse HEAD)"
127
+ tag_sha="$(git rev-parse "${EXPECTED_TAG}^{commit}")"
128
+ if [[ "$actual_sha" != "$EXPECTED_SHA" || "$tag_sha" != "$EXPECTED_SHA" ]]; then
129
+ echo "Checked-out release identity does not match the resolved tag." >&2
130
+ exit 1
131
+ fi
132
+ python - <<'PY'
133
+ import json
134
+ import os
135
+ import re
136
+ from pathlib import Path
137
+
138
+ expected = os.environ["EXPECTED_VERSION"]
139
+ manifest = json.loads(Path(".release-please-manifest.json").read_text(encoding="utf-8"))["."]
140
+ source = Path("pyproject.toml").read_text(encoding="utf-8")
141
+ match = re.search(r'^version = "([0-9]+\.[0-9]+\.[0-9]+)"', source, re.MULTILINE)
142
+ if match is None or manifest != expected or match.group(1) != expected:
143
+ raise SystemExit("Checked-out package version does not match the release target.")
144
+ PY
145
+ - uses: actions/setup-python@v6
146
+ with:
147
+ python-version: "3.12"
148
+ - name: Install build tools
149
+ run: python -m pip install --upgrade pip build twine
150
+ - name: Build package
151
+ run: python -m build
152
+ - name: Check package
153
+ run: python -m twine check dist/*
154
+ - name: Upload dist artifacts
155
+ uses: actions/upload-artifact@v7
156
+ with:
157
+ name: dist
158
+ path: dist/
159
+
160
+ publish:
161
+ name: Publish to PyPI
162
+ runs-on: ubuntu-latest
163
+ needs: [release-please, build]
164
+ if: needs.release-please.outputs.release_created == 'true' || (github.event_name == 'workflow_dispatch' && inputs.tag_name != '' && inputs.publish_to_pypi == true)
165
+ environment:
166
+ name: pypi
167
+ url: https://pypi.org/p/dcc-mcp-openscreen
168
+ permissions:
169
+ contents: read
170
+ id-token: write
171
+ steps:
172
+ - uses: actions/checkout@v6
173
+ with:
174
+ ref: ${{ needs.release-please.outputs.tag_name }}
175
+ fetch-depth: 0
176
+ - name: Verify immutable release target
177
+ env:
178
+ EXPECTED_TAG: ${{ needs.release-please.outputs.tag_name }}
179
+ EXPECTED_SHA: ${{ needs.release-please.outputs.tag_sha }}
180
+ EXPECTED_VERSION: ${{ needs.release-please.outputs.version }}
181
+ shell: bash
182
+ run: |
183
+ set -euo pipefail
184
+ actual_sha="$(git rev-parse HEAD)"
185
+ tag_sha="$(git rev-parse "${EXPECTED_TAG}^{commit}")"
186
+ if [[ "$actual_sha" != "$EXPECTED_SHA" || "$tag_sha" != "$EXPECTED_SHA" ]]; then
187
+ echo "Checked-out release identity does not match the resolved tag." >&2
188
+ exit 1
189
+ fi
190
+ python - <<'PY'
191
+ import json
192
+ import os
193
+ import re
194
+ from pathlib import Path
195
+
196
+ expected = os.environ["EXPECTED_VERSION"]
197
+ manifest = json.loads(Path(".release-please-manifest.json").read_text(encoding="utf-8"))["."]
198
+ source = Path("pyproject.toml").read_text(encoding="utf-8")
199
+ match = re.search(r'^version = "([0-9]+\.[0-9]+\.[0-9]+)"', source, re.MULTILINE)
200
+ if match is None or manifest != expected or match.group(1) != expected:
201
+ raise SystemExit("Checked-out package version does not match the release target.")
202
+ PY
203
+ - name: Download dist artifacts
204
+ uses: actions/download-artifact@v8
205
+ with:
206
+ name: dist
207
+ path: dist/
208
+ - name: Publish to PyPI with trusted publishing
209
+ uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
210
+ with:
211
+ verbose: true
212
+ print-hash: true
@@ -0,0 +1,5 @@
1
+ __pycache__/
2
+ *.pyc
3
+ dist/
4
+ build/
5
+ *.egg-info/
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.1.1"
3
+ }
@@ -0,0 +1,17 @@
1
+ # Changelog
2
+
3
+ ## [0.1.1](https://github.com/dcc-mcp/dcc-mcp-openscreen/compare/v0.1.0...v0.1.1) (2026-09-27)
4
+
5
+
6
+ ### Features
7
+
8
+ * add OpenScreen MCP adapter ([f12c9eb](https://github.com/dcc-mcp/dcc-mcp-openscreen/commit/f12c9ebe72d7ab2371c5d8bba94c197308854f1a))
9
+ * expand OpenScreen tools and diagnostics ([dc49cde](https://github.com/dcc-mcp/dcc-mcp-openscreen/commit/dc49cdea522af0207fa9052c42effc36f9a3b350))
10
+
11
+
12
+ ### Bug Fixes
13
+
14
+ * clean up hung OpenScreen process trees ([354e603](https://github.com/dcc-mcp/dcc-mcp-openscreen/commit/354e6037c08b4ccca954cc41c473457672743f40))
15
+ * make OpenScreen server module executable ([3dc4815](https://github.com/dcc-mcp/dcc-mcp-openscreen/commit/3dc4815a76113696ecc3ae53c32cee0a7a5853cf))
16
+ * repair OpenScreen CLI runner syntax ([e27adc6](https://github.com/dcc-mcp/dcc-mcp-openscreen/commit/e27adc60f1cb0e2131c46fee10857afcd7e6ddd6))
17
+ * stop recordings cooperatively on Windows ([2adedfc](https://github.com/dcc-mcp/dcc-mcp-openscreen/commit/2adedfca815a4a08dfc02c5e1b62eb13e1ef2b34))
@@ -0,0 +1,28 @@
1
+ Metadata-Version: 2.4
2
+ Name: dcc-mcp-openscreen
3
+ Version: 0.1.1
4
+ Summary: OpenScreen recording and demo export adapter for DCC-MCP
5
+ Author-email: Long Hao <hal.long@outlook.com>
6
+ License: MIT
7
+ Requires-Python: >=3.7
8
+ Requires-Dist: dcc-mcp-core<1.0.0,>=0.20.14
9
+ Description-Content-Type: text/markdown
10
+
11
+ # dcc-mcp-openscreen
12
+
13
+ Standalone DCC-MCP adapter for [OpenScreen](https://github.com/getopenscreen/openscreen).
14
+
15
+ The adapter exposes only typed, bounded CLI operations. Set `DCC_MCP_OPENSCREEN_PATH` when the executable is not on `PATH`. It does not automate the desktop UI; DCC window selection and unsupported UI actions remain owned by `dcc-cua` / `ui-control`.
16
+
17
+ ## Run
18
+
19
+ ```powershell
20
+ python -m pip install -e .
21
+ dcc-mcp-openscreen
22
+ ```
23
+
24
+ Tools: `openscreen__sources`, `openscreen__record`, and `openscreen__export`.
25
+
26
+ ## License
27
+
28
+ MIT
@@ -0,0 +1,18 @@
1
+ # dcc-mcp-openscreen
2
+
3
+ Standalone DCC-MCP adapter for [OpenScreen](https://github.com/getopenscreen/openscreen).
4
+
5
+ The adapter exposes only typed, bounded CLI operations. Set `DCC_MCP_OPENSCREEN_PATH` when the executable is not on `PATH`. It does not automate the desktop UI; DCC window selection and unsupported UI actions remain owned by `dcc-cua` / `ui-control`.
6
+
7
+ ## Run
8
+
9
+ ```powershell
10
+ python -m pip install -e .
11
+ dcc-mcp-openscreen
12
+ ```
13
+
14
+ Tools: `openscreen__sources`, `openscreen__record`, and `openscreen__export`.
15
+
16
+ ## License
17
+
18
+ MIT
@@ -0,0 +1,21 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27,<1.30"]
3
+ build-backend = "hatchling.build"
4
+ [project]
5
+ name = "dcc-mcp-openscreen"
6
+ version = "0.1.1"
7
+ description = "OpenScreen recording and demo export adapter for DCC-MCP"
8
+ readme = "README.md"
9
+ license = {text = "MIT"}
10
+ requires-python = ">=3.7"
11
+ authors = [{name = "Long Hao", email = "hal.long@outlook.com"}]
12
+ dependencies = ["dcc-mcp-core>=0.20.14,<1.0.0"]
13
+ [project.scripts]
14
+ dcc-mcp-openscreen = "dcc_mcp_openscreen.server:main"
15
+ [project.entry-points."dcc_mcp.adapters"]
16
+ openscreen = "dcc_mcp_openscreen:OpenScreenMcpServer"
17
+ [tool.hatch.build.targets.wheel]
18
+ packages = ["src/dcc_mcp_openscreen"]
19
+ artifacts = ["src/dcc_mcp_openscreen/skills/**"]
20
+ [tool.pytest.ini_options]
21
+ pythonpath = ["src"]
@@ -0,0 +1,20 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
3
+ "packages": {
4
+ ".": {
5
+ "release-type": "python",
6
+ "package-name": "dcc-mcp-openscreen",
7
+ "include-component-in-tag": false,
8
+ "changelog-path": "CHANGELOG.md",
9
+ "bump-minor-pre-major": true,
10
+ "bump-patch-for-minor-pre-major": true,
11
+ "extra-files": [
12
+ {
13
+ "type": "toml",
14
+ "path": "pyproject.toml",
15
+ "jsonpath": "$.project.version"
16
+ }
17
+ ]
18
+ }
19
+ }
20
+ }
@@ -0,0 +1,2 @@
1
+ from .server import OpenScreenMcpServer
2
+ __all__ = ["OpenScreenMcpServer"]
@@ -0,0 +1,20 @@
1
+ """Standalone OpenScreen MCP adapter."""
2
+ from __future__ import annotations
3
+ import argparse, os, signal, threading
4
+ from pathlib import Path
5
+ from dcc_mcp_core import DccServerOptions
6
+ from dcc_mcp_core.server_base import DccServerBase
7
+ class OpenScreenMcpServer(DccServerBase):
8
+ def __init__(self, *, port: int | None = None):
9
+ options = DccServerOptions.from_env("openscreen", Path(__file__).parent / "skills", port=port, server_name="dcc-mcp-openscreen", instance_type="standalone", adapter_version="0.1.0", standalone_main_thread=False)
10
+ super().__init__(options=options)
11
+ def main(argv=None):
12
+ p=argparse.ArgumentParser(); p.add_argument("--mcp-port", type=int); a=p.parse_args(argv)
13
+ server=OpenScreenMcpServer(port=a.mcp_port); server.register_builtin_actions(); server.start()
14
+ stopped=threading.Event(); signal.signal(signal.SIGINT, lambda *_: stopped.set())
15
+ if hasattr(signal,"SIGTERM"): signal.signal(signal.SIGTERM, lambda *_: stopped.set())
16
+ try: stopped.wait()
17
+ finally: server.stop()
18
+
19
+ if __name__ == '__main__':
20
+ main()
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+ import json, os, subprocess, time
3
+ from pathlib import Path
4
+
5
+ def executable() -> str:
6
+ return os.getenv("DCC_MCP_OPENSCREEN_PATH", "openscreen")
7
+
8
+ def _events(stdout: str) -> list[dict]:
9
+ out=[]
10
+ for line in stdout.splitlines():
11
+ try: out.append(json.loads(line))
12
+ except json.JSONDecodeError: pass
13
+ return out
14
+
15
+ def run(args: list[str], *, timeout: float = 900.0) -> dict:
16
+ try:
17
+ proc=subprocess.run([executable(), *args, "--json"], capture_output=True, text=True, timeout=timeout, check=False)
18
+ except subprocess.TimeoutExpired as exc:
19
+ raise RuntimeError(f"OpenScreen CLI timed out after {timeout:.0f}s; verify permissions and headless build") from exc
20
+ return _finish(proc.returncode, proc.stdout, proc.stderr)
21
+
22
+ def record(args: list[str], *, duration: int, timeout: float) -> dict:
23
+ proc=subprocess.Popen([executable(), "record", *args, "--json"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
24
+ try:
25
+ time.sleep(duration)
26
+ if proc.poll() is None and proc.stdin:
27
+ proc.stdin.write("stop\n"); proc.stdin.flush()
28
+ stdout, stderr=proc.communicate(timeout=max(10.0, timeout-duration))
29
+ except subprocess.TimeoutExpired as exc:
30
+ if os.name == "nt":\n subprocess.run(["taskkill", "/PID", str(proc.pid), "/T", "/F"], capture_output=True, check=False)\n else:\n proc.kill()\n proc.communicate()
31
+ raise RuntimeError("OpenScreen recording did not stop after a cooperative stop request") from exc
32
+ return _finish(proc.returncode, stdout, stderr)
33
+
34
+ def _finish(code: int, stdout: str, stderr: str) -> dict:
35
+ events=_events(stdout)
36
+ done=next((e for e in reversed(events) if e.get("event")=="done"), None)
37
+ if code != 0 or not done or done.get("success") is False:
38
+ detail=(stderr or "OpenScreen command failed").strip()
39
+ if not events and not detail: detail="OpenScreen produced no JSON events; verify a headless-capable build"
40
+ raise RuntimeError(detail[-2000:])
41
+ return done
42
+
43
+ def path_arg(value: str) -> str:
44
+ path=Path(value).expanduser().resolve()
45
+ if len(str(path))>4096: raise ValueError("path too long")
46
+ return str(path)
47
+
@@ -0,0 +1,4 @@
1
+ from dcc_mcp_core.skill import skill_entry, skill_success
2
+ from ._client import run, path_arg
3
+ @skill_entry
4
+ def main(project, min_words=2, max_words=7, **_): return skill_success("OpenScreen captions generated.", **run(["captions", path_arg(project), "--min-words", str(min_words), "--max-words", str(max_words)], timeout=1800))
@@ -0,0 +1,7 @@
1
+ from dcc_mcp_core.skill import skill_entry, skill_success
2
+ from ._client import run, path_arg
3
+ @skill_entry
4
+ def main(project, output=None, quality="source", **_):
5
+ args=["export",path_arg(project),"--quality",quality]
6
+ if output: args += ["--out",path_arg(output)]
7
+ return skill_success("OpenScreen export completed.", **run(args, timeout=1800))
@@ -0,0 +1,4 @@
1
+ from dcc_mcp_core.skill import skill_entry, skill_success
2
+ from ._client import run, path_arg
3
+ @skill_entry
4
+ def main(project, **_): return skill_success("OpenScreen project inspected.", **run(["info", path_arg(project)]))
@@ -0,0 +1,4 @@
1
+ from dcc_mcp_core.skill import skill_entry, skill_success
2
+ from ._client import run, path_arg
3
+ @skill_entry
4
+ def main(project, output, **_): return skill_success("OpenScreen project packed.", **run(["pack", path_arg(project), "--out", path_arg(output)], timeout=600))
@@ -0,0 +1,11 @@
1
+ from dcc_mcp_core.skill import skill_entry, skill_success
2
+ from ._client import record, path_arg
3
+ @skill_entry
4
+ def main(duration=30, window=None, display=0, project=None, mic=False, system_audio=False, **_):
5
+ if not isinstance(duration,int) or duration<1 or duration>3600: raise ValueError("duration must be 1..3600 seconds")
6
+ args=["--display",str(display)]
7
+ if window: args += ["--window",str(window)]
8
+ if project: args += ["--project",path_arg(project)]
9
+ if mic: args += ["--mic"]
10
+ if system_audio: args += ["--system-audio"]
11
+ return skill_success("OpenScreen recording completed.", **record(args,duration=duration,timeout=duration+120))
@@ -0,0 +1,4 @@
1
+ from dcc_mcp_core.skill import skill_entry, skill_success
2
+ from ._client import run
3
+ @skill_entry
4
+ def main(**_): return skill_success("OpenScreen sources enumerated.", **run(["sources"]))
@@ -0,0 +1,67 @@
1
+ skill: openscreen
2
+ version: "0.1.0"
3
+ description: Record and export OpenScreen projects through its typed headless CLI.
4
+ tools:
5
+ - name: sources
6
+ description: List available displays, windows, and microphones.
7
+ source_file: scripts/sources.py
8
+ input_schema: {type: object, additionalProperties: false}
9
+ execution: sync
10
+ affinity: any
11
+ annotations: {read_only_hint: true, destructive_hint: false, idempotent_hint: true, open_world_hint: false}
12
+ - name: record
13
+ description: Record a bounded display or window capture to an optional OpenScreen project.
14
+ source_file: scripts/record.py
15
+ input_schema:
16
+ type: object
17
+ additionalProperties: false
18
+ properties:
19
+ duration: {type: integer, minimum: 1, maximum: 3600, default: 30}
20
+ window: {type: string, minLength: 1, maxLength: 256}
21
+ display: {type: integer, minimum: 0, maximum: 32, default: 0}
22
+ project: {type: string, maxLength: 4096}
23
+ mic: {type: boolean, default: false}
24
+ system_audio: {type: boolean, default: false}
25
+ execution: sync
26
+ affinity: any
27
+ timeout_hint_secs: 3720
28
+ annotations: {read_only_hint: false, destructive_hint: true, idempotent_hint: false, open_world_hint: false}
29
+ - name: export
30
+ description: Render an OpenScreen project to MP4 or GIF.
31
+ source_file: scripts/export.py
32
+ input_schema:
33
+ type: object
34
+ required: [project]
35
+ additionalProperties: false
36
+ properties:
37
+ project: {type: string, minLength: 1, maxLength: 4096}
38
+ output: {type: string, maxLength: 4096}
39
+ quality: {enum: [medium, good, source], default: source}
40
+ execution: sync
41
+ affinity: any
42
+ timeout_hint_secs: 1800
43
+ annotations: {read_only_hint: false, destructive_hint: true, idempotent_hint: true, open_world_hint: false}
44
+ - name: info
45
+ description: Inspect a project and verify referenced media.
46
+ source_file: scripts/info.py
47
+ input_schema: {type: object, required: [project], additionalProperties: false, properties: {project: {type: string, minLength: 1, maxLength: 4096}}}
48
+ execution: sync
49
+ affinity: any
50
+ timeout_hint_secs: 60
51
+ annotations: {read_only_hint: true, destructive_hint: false, idempotent_hint: true, open_world_hint: false}
52
+ - name: pack
53
+ description: Pack a project and referenced media into a portable folder.
54
+ source_file: scripts/pack.py
55
+ input_schema: {type: object, required: [project, output], additionalProperties: false, properties: {project: {type: string, minLength: 1, maxLength: 4096}, output: {type: string, minLength: 1, maxLength: 4096}}}
56
+ execution: sync
57
+ affinity: any
58
+ timeout_hint_secs: 600
59
+ annotations: {read_only_hint: false, destructive_hint: true, idempotent_hint: true, open_world_hint: false}
60
+ - name: captions
61
+ description: Generate local Whisper captions for a project with bounded word grouping.
62
+ source_file: scripts/captions.py
63
+ input_schema: {type: object, required: [project], additionalProperties: false, properties: {project: {type: string, minLength: 1, maxLength: 4096}, min_words: {type: integer, minimum: 1, maximum: 20, default: 2}, max_words: {type: integer, minimum: 1, maximum: 20, default: 7}}}
64
+ execution: sync
65
+ affinity: any
66
+ timeout_hint_secs: 1800
67
+ annotations: {read_only_hint: false, destructive_hint: true, idempotent_hint: true, open_world_hint: false}
@@ -0,0 +1,5 @@
1
+ from dcc_mcp_openscreen.server import OpenScreenMcpServer
2
+
3
+ def test_server_constructs():
4
+ server = OpenScreenMcpServer(port=0)
5
+ assert server is not None