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.
- dcc_mcp_openscreen-0.1.1/.github/workflows/release.yml +212 -0
- dcc_mcp_openscreen-0.1.1/.gitignore +5 -0
- dcc_mcp_openscreen-0.1.1/.release-please-manifest.json +3 -0
- dcc_mcp_openscreen-0.1.1/CHANGELOG.md +17 -0
- dcc_mcp_openscreen-0.1.1/PKG-INFO +28 -0
- dcc_mcp_openscreen-0.1.1/README.md +18 -0
- dcc_mcp_openscreen-0.1.1/pyproject.toml +21 -0
- dcc_mcp_openscreen-0.1.1/release-please-config.json +20 -0
- dcc_mcp_openscreen-0.1.1/src/dcc_mcp_openscreen/__init__.py +2 -0
- dcc_mcp_openscreen-0.1.1/src/dcc_mcp_openscreen/server.py +20 -0
- dcc_mcp_openscreen-0.1.1/src/dcc_mcp_openscreen/skills/openscreen/scripts/_client.py +47 -0
- dcc_mcp_openscreen-0.1.1/src/dcc_mcp_openscreen/skills/openscreen/scripts/captions.py +4 -0
- dcc_mcp_openscreen-0.1.1/src/dcc_mcp_openscreen/skills/openscreen/scripts/export.py +7 -0
- dcc_mcp_openscreen-0.1.1/src/dcc_mcp_openscreen/skills/openscreen/scripts/info.py +4 -0
- dcc_mcp_openscreen-0.1.1/src/dcc_mcp_openscreen/skills/openscreen/scripts/pack.py +4 -0
- dcc_mcp_openscreen-0.1.1/src/dcc_mcp_openscreen/skills/openscreen/scripts/record.py +11 -0
- dcc_mcp_openscreen-0.1.1/src/dcc_mcp_openscreen/skills/openscreen/scripts/sources.py +4 -0
- dcc_mcp_openscreen-0.1.1/src/dcc_mcp_openscreen/skills/openscreen/tools.yaml +67 -0
- dcc_mcp_openscreen-0.1.1/tests/test_server.py +5 -0
|
@@ -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,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,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,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,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}
|