@sylphx/video-reader-mcp 0.1.0 → 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/README.md +222 -41
- package/bin/native/video-reader-mcp-server +0 -0
- package/bin/video-reader-mcp +48 -0
- package/dist/doctor-cli.js +167 -0
- package/package.json +23 -10
- package/src/doctor-cli.ts +18 -0
- package/src/doctor.ts +132 -0
- package/src/engine/rust-asr.ts +98 -0
- package/src/engine/rust-frames.ts +95 -0
- package/src/engine/rust-timeline.ts +164 -0
- package/src/engine/rust-video-evidence.ts +132 -0
- package/src/handlers/readVideo.ts +41 -0
- package/src/handlers/videoEvidence.ts +161 -0
- package/src/mcp.ts +302 -0
- package/src/schemas/readVideo.ts +67 -0
- package/src/schemas/videoEvidence.ts +53 -0
- package/src/sdk.ts +44 -0
- package/src/types/timeline.ts +106 -0
- package/src/utils/asr.ts +59 -0
- package/src/utils/errors.ts +16 -0
- package/src/utils/exec.ts +76 -0
- package/src/utils/ffmpeg.ts +81 -0
- package/src/utils/ffprobe.ts +137 -0
- package/src/utils/frameOcr.ts +99 -0
- package/src/utils/frames.ts +93 -0
- package/src/utils/pathUtils.ts +36 -0
- package/src/utils/scenes.ts +63 -0
- package/src/utils/subtitles.ts +101 -0
- package/src/video/readCoordinator.ts +165 -0
- package/dist/handlers/readVideo.js +0 -24
- package/dist/index.js +0 -59
- package/dist/mcp.js +0 -181
- package/dist/schemas/readVideo.js +0 -33
- package/dist/types/timeline.js +0 -1
- package/dist/utils/asr.js +0 -26
- package/dist/utils/errors.js +0 -14
- package/dist/utils/exec.js +0 -56
- package/dist/utils/ffmpeg.js +0 -66
- package/dist/utils/ffprobe.js +0 -79
- package/dist/utils/pathUtils.js +0 -31
- package/dist/utils/scenes.js +0 -49
- package/dist/utils/subtitles.js +0 -85
- package/dist/video/readCoordinator.js +0 -81
package/README.md
CHANGED
|
@@ -1,86 +1,267 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# Cue
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
### Timeline proof for agents
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
**Cue** (transitional package `@sylphx/video-reader-mcp`) — local video timeline evidence, not frame-by-frame VLM.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
<p align="center">
|
|
10
|
+
<img src="https://mark.sylphx.com/api/v1/banner?type=mesh&theme=tokyonight&text=video+reader+mcp&desc=Evidence-first+video+reading+for+AI+agents+%E2%80%94+ffprobe%2C+subtitles%2C+scenes%2C+transcr&height=200&animation=rise&credit=0" alt="video-reader-mcp — Sylphx Mark banner" width="100%" />
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
### Your agent watched the video. **Did it read the timeline?**
|
|
14
|
+
|
|
15
|
+
Evidence-first video reading for AI agents. One call turns any local video into a
|
|
16
|
+
**timeline document** — ffprobe streams, chapters, embedded subtitles, scene
|
|
17
|
+
boundaries, and warnings you can cite without frame-by-frame vision LLM calls.
|
|
18
|
+
|
|
19
|
+
[](https://www.npmjs.com/package/@sylphx/video-reader-mcp)
|
|
20
|
+
[](https://opensource.org/licenses/MIT)
|
|
21
|
+
[](https://github.com/SylphxAI/video-reader-mcp/actions/workflows/ci.yml)
|
|
22
|
+
[](https://www.typescriptlang.org/)
|
|
23
|
+
|
|
24
|
+
**Local-first** · **One smart `read_video` call** · **Timeline evidence + provenance** · **20 tests**
|
|
25
|
+
|
|
26
|
+
SOTA family roadmap: [docs/roadmap/sota-family-roadmap.md](docs/roadmap/sota-family-roadmap.md).
|
|
27
|
+
|
|
28
|
+
[⭐ Star this repo](https://github.com/SylphxAI/video-reader-mcp) if agents should read video timelines with proof, not sampled frame captions.
|
|
29
|
+
· [Quick start](#quick-start) · [See it work](#see-it-work) · [Why not frame-by-frame vision?](#why-not-frame-by-frame-vision)
|
|
30
|
+
|
|
31
|
+
This repository is product SSOT. Sibling agent tools live in separate repos.
|
|
32
|
+
Each instrument is an independent repository (marketplace + stars).
|
|
33
|
+
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
## Product docs
|
|
40
|
+
|
|
41
|
+
| Doc | Purpose |
|
|
10
42
|
| --- | --- |
|
|
11
|
-
| [
|
|
12
|
-
| [
|
|
13
|
-
|
|
|
14
|
-
| [
|
|
43
|
+
| [docs/POSITIONING.md](docs/POSITIONING.md) | Strategic positioning |
|
|
44
|
+
| [docs/COMPETITIVE.md](docs/COMPETITIVE.md) | Peer anchors and wedge |
|
|
45
|
+
| [docs/EVIDENCE_CONTRACT.md](docs/EVIDENCE_CONTRACT.md) | Evidence = result contract |
|
|
46
|
+
| [docs/TOOL_SURFACE.md](docs/TOOL_SURFACE.md) | Few clear tools policy |
|
|
47
|
+
| [docs/PRODUCT_INDEPENDENCE.md](docs/PRODUCT_INDEPENDENCE.md) | This repo is SSOT |
|
|
48
|
+
| [docs/IPPB.md](docs/IPPB.md) | Independent public product bar |
|
|
49
|
+
| [docs/PUBLISH.md](docs/PUBLISH.md) | npm/git publish status |
|
|
15
50
|
|
|
16
|
-
##
|
|
51
|
+
## The problem
|
|
17
52
|
|
|
18
|
-
|
|
53
|
+
Videos are not a pile of frames. They are codecs, chapters, embedded subtitles,
|
|
54
|
+
scene cuts, variable frame rates, and timelines measured in milliseconds.
|
|
19
55
|
|
|
20
|
-
|
|
56
|
+
Most agent stacks sample frames and ask a **vision LLM** what it sees. Subtitles
|
|
57
|
+
get skipped. Scene boundaries vanish. Duration and stream metadata never reach
|
|
58
|
+
context. Citations become "around minute two, I think." Then the agent
|
|
59
|
+
hallucinates — confidently.
|
|
21
60
|
|
|
22
|
-
|
|
61
|
+
**Video Reader MCP is built for the moment your agent needs a citeable timeline,
|
|
62
|
+
not a slideshow summary.**
|
|
23
63
|
|
|
24
|
-
|
|
64
|
+
## Why not frame-by-frame vision?
|
|
25
65
|
|
|
26
|
-
|
|
66
|
+
| Typical vision path | Video Reader MCP |
|
|
67
|
+
| --- | --- |
|
|
68
|
+
| Sample N frames into a vision model | ffprobe format + stream metadata in one call |
|
|
69
|
+
| Paraphrased "what happens" | Embedded subtitle cues with `start_ms`, `end_ms`, and provenance |
|
|
70
|
+
| Scene changes guessed from captions | Optional ffmpeg scene filter with timestamp evidence |
|
|
71
|
+
| Missing audio / VFR silently ignored | Warnings for missing ffmpeg/ffprobe, VFR, missing audio, skipped ASR |
|
|
72
|
+
| Cloud API by default | **Local-first** — ffprobe + ffmpeg on your machine |
|
|
73
|
+
| Ship and pray | **20** tests on parsers, fixture corpus, doctor, release gate, and integration |
|
|
27
74
|
|
|
28
|
-
|
|
29
|
-
- chapter markers
|
|
30
|
-
- embedded subtitle cues (`time_ms`, text, provenance)
|
|
31
|
-
- optional scene boundaries (ffmpeg `scene` filter)
|
|
32
|
-
- warnings (missing ffmpeg/ffprobe, VFR, missing audio, skipped ASR)
|
|
33
|
-
- optional local ASR hook (skipped in v0.1 unless adapter is wired)
|
|
75
|
+
## See it work
|
|
34
76
|
|
|
35
|
-
|
|
77
|
+
### Install (30 seconds)
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
npm install -g @sylphx/video-reader-mcp
|
|
81
|
+
cue doctor
|
|
82
|
+
claude mcp add cue -- npx @sylphx/video-reader-mcp
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Install once. Call once.**
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
claude mcp add video-reader -- npx @sylphx/video-reader-mcp
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"sources": [{ "path": "/absolute/path/to/demo.mp4" }],
|
|
94
|
+
"include_subtitles": true,
|
|
95
|
+
"include_scenes": true
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`read_video` builds a timeline document per source — no per-frame vision LLM
|
|
100
|
+
calls:
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
{
|
|
104
|
+
"source": "/absolute/path/to/demo.mp4",
|
|
105
|
+
"success": true,
|
|
106
|
+
"data": {
|
|
107
|
+
"provenance": {
|
|
108
|
+
"source": "/absolute/path/to/demo.mp4",
|
|
109
|
+
"tool": "read_video",
|
|
110
|
+
"version": "0.1.0",
|
|
111
|
+
"extracted_at": "2026-07-09T12:00:00.000Z"
|
|
112
|
+
},
|
|
113
|
+
"format": {
|
|
114
|
+
"format_name": "mov,mp4,m4a,3gp,3g2,mj2",
|
|
115
|
+
"duration_ms": 125500
|
|
116
|
+
},
|
|
117
|
+
"streams": [
|
|
118
|
+
{ "index": 0, "codec_type": "video", "width": 1920, "height": 1080 },
|
|
119
|
+
{ "index": 1, "codec_type": "audio", "channels": 2, "sample_rate": 48000 }
|
|
120
|
+
],
|
|
121
|
+
"chapters": [
|
|
122
|
+
{ "id": 0, "start_ms": 0, "end_ms": 60250, "title": "Intro" }
|
|
123
|
+
],
|
|
124
|
+
"subtitles": [
|
|
125
|
+
{
|
|
126
|
+
"index": 0,
|
|
127
|
+
"start_ms": 1200,
|
|
128
|
+
"end_ms": 3400,
|
|
129
|
+
"text": "Welcome to the demo.",
|
|
130
|
+
"provenance": { "method": "ffmpeg_extract", "format": "srt" }
|
|
131
|
+
}
|
|
132
|
+
],
|
|
133
|
+
"scenes": [
|
|
134
|
+
{
|
|
135
|
+
"index": 0,
|
|
136
|
+
"time_ms": 45200,
|
|
137
|
+
"provenance": { "method": "ffmpeg_scene_filter", "threshold": 0.4 }
|
|
138
|
+
}
|
|
139
|
+
],
|
|
140
|
+
"warnings": []
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Abbreviated shape — optional local ASR transcript hooks skip gracefully when no
|
|
146
|
+
adapter is wired.
|
|
36
147
|
|
|
37
148
|
## Prerequisites
|
|
38
149
|
|
|
39
|
-
- Node.js
|
|
150
|
+
- Node.js `>=22.13`
|
|
40
151
|
- **ffprobe** (required) and **ffmpeg** (recommended for subtitles + scenes) on `PATH`
|
|
41
152
|
|
|
42
|
-
##
|
|
153
|
+
## MCP Tool Surface
|
|
43
154
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
155
|
+
| Tool | Use it when the agent needs to... |
|
|
156
|
+
| --- | --- |
|
|
157
|
+
| `read_video` | Read one or more local videos and return ffprobe metadata, chapters, subtitles, scenes, and timeline warnings. |
|
|
47
158
|
|
|
48
|
-
|
|
159
|
+
Supported formats: MP4, M4V, MKV, MOV, WebM, and other formats ffprobe can inspect.
|
|
160
|
+
|
|
161
|
+
## Quick Start
|
|
162
|
+
|
|
163
|
+
### Claude Code
|
|
49
164
|
|
|
50
165
|
```bash
|
|
51
|
-
|
|
52
|
-
bun run build
|
|
53
|
-
bun run start
|
|
166
|
+
claude mcp add video-reader -- npx @sylphx/video-reader-mcp
|
|
54
167
|
```
|
|
55
168
|
|
|
56
|
-
###
|
|
169
|
+
### Claude Desktop
|
|
170
|
+
|
|
171
|
+
Add this to `claude_desktop_config.json`:
|
|
57
172
|
|
|
58
173
|
```json
|
|
59
174
|
{
|
|
60
|
-
"
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
175
|
+
"mcpServers": {
|
|
176
|
+
"video-reader": {
|
|
177
|
+
"command": "npx",
|
|
178
|
+
"args": ["@sylphx/video-reader-mcp"]
|
|
179
|
+
}
|
|
180
|
+
}
|
|
64
181
|
}
|
|
65
182
|
```
|
|
66
183
|
|
|
184
|
+
### Any MCP Client
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
npx @sylphx/video-reader-mcp
|
|
188
|
+
```
|
|
189
|
+
|
|
67
190
|
### HTTP transport (optional)
|
|
68
191
|
|
|
69
192
|
```bash
|
|
70
|
-
MCP_TRANSPORT=http MCP_HTTP_PORT=8080
|
|
193
|
+
MCP_TRANSPORT=http MCP_HTTP_PORT=8080 npx @sylphx/video-reader-mcp
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Security model
|
|
197
|
+
|
|
198
|
+
- **Local-first** — `read_video` inspects local files; remote URLs are not fetched by default.
|
|
199
|
+
- **ffprobe/ffmpeg boundary** — probe and frame tools shell out to configured binaries on PATH; missing tools return explicit errors.
|
|
200
|
+
- **Fixture corpus** — CI validates parser and safety fixtures; corrupted inputs fail closed with structured diagnostics.
|
|
201
|
+
- **Evidence envelope** — timestamps, frame indices, and extraction routes are preserved so agents can verify claims.
|
|
202
|
+
|
|
203
|
+
## Release proof
|
|
204
|
+
|
|
205
|
+
Claims are backed by CI `benchmark:release-gate`, fixture corpus checks, and the shipped-path matrix (Rust-default primary tools).
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
bun run benchmark:release-gate
|
|
71
209
|
```
|
|
72
210
|
|
|
211
|
+
Artifact: `benchmark-artifacts/video_reader_release_gate.json` — must report `status: passed` before release.
|
|
212
|
+
|
|
73
213
|
## Development
|
|
74
214
|
|
|
75
215
|
```bash
|
|
216
|
+
git clone https://github.com/SylphxAI/video-reader-mcp.git
|
|
217
|
+
cd video-reader-mcp
|
|
76
218
|
bun install
|
|
77
|
-
bun run typecheck
|
|
78
|
-
bun test
|
|
79
219
|
bun run build
|
|
220
|
+
bun test
|
|
221
|
+
bun run doctor
|
|
222
|
+
bun run benchmark:release-gate
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Useful checks:
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
bun run check
|
|
229
|
+
bun run typecheck
|
|
230
|
+
bun run benchmark:release-gate
|
|
80
231
|
```
|
|
81
232
|
|
|
82
|
-
|
|
233
|
+
Example `read_video` requests live in [`examples/`](examples/). CI runs parser,
|
|
234
|
+
fixture corpus, doctor, and release-gate checks; integration tests exercise ffmpeg
|
|
235
|
+
when available on the runner.
|
|
236
|
+
|
|
237
|
+
## Support
|
|
238
|
+
|
|
239
|
+
- [Issues](https://github.com/SylphxAI/video-reader-mcp/issues)
|
|
240
|
+
- [npm package](https://www.npmjs.com/package/@sylphx/video-reader-mcp)
|
|
241
|
+
- Portfolio orchestration: [smart-reader-mcp](https://github.com/SylphxAI/smart-reader-mcp)
|
|
242
|
+
|
|
243
|
+
## Help this reach more builders
|
|
244
|
+
|
|
245
|
+
If frame-by-frame vision guesses have wasted your context, your citations, or
|
|
246
|
+
your trust in agent output, you are exactly who this project is for.
|
|
247
|
+
|
|
248
|
+
**[⭐ Star the repo](https://github.com/SylphxAI/video-reader-mcp)** — it is the
|
|
249
|
+
fastest way to help more agent builders find evidence-first video reading. Share
|
|
250
|
+
it in your MCP client setup, team wiki, or agent stack README.
|
|
251
|
+
|
|
252
|
+
### Discovery (in progress)
|
|
253
|
+
|
|
254
|
+
| Channel | Status |
|
|
255
|
+
| --- | --- |
|
|
256
|
+
| [Glama MCP directory](https://glama.ai/mcp/servers/SylphxAI/video-reader-mcp) | Listed — [claim server](https://glama.ai/mcp/servers/SylphxAI/video-reader-mcp/admin) for full discoverability |
|
|
257
|
+
| [Official MCP Registry](https://registry.modelcontextprotocol.io/v0.1/servers?search=io.github.SylphxAI/video-reader-mcp) | Listed — `io.github.SylphxAI/video-reader-mcp` @ v0.1.0 |
|
|
258
|
+
| [TensorBlock MCP Index PR #1113](https://github.com/TensorBlock/awesome-mcp-servers/pull/1113) | Open — multimedia/document processing listing |
|
|
259
|
+
| [MCP servers community issue #4500](https://github.com/modelcontextprotocol/servers/issues/4500) | Open — community server highlight |
|
|
260
|
+
| [mcp.so listing issue #3068](https://github.com/chatmcp/mcpso/issues/3068) | Open — directory submission request |
|
|
261
|
+
| [mcpservers.org submit](https://mcpservers.org/submit) | Not listed yet — free web-form submission |
|
|
262
|
+
|
|
263
|
+
Know another MCP directory? [Open an issue](https://github.com/SylphxAI/video-reader-mcp/issues/new) with the link.
|
|
83
264
|
|
|
84
265
|
## License
|
|
85
266
|
|
|
86
|
-
MIT © [SylphxAI](https://github.com/SylphxAI)
|
|
267
|
+
MIT © [SylphxAI](https://github.com/SylphxAI)
|
|
Binary file
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Video Reader MCP launcher — Rust rmcp only (fail-closed, no TS stdio fallback).
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
6
|
+
|
|
7
|
+
resolve_rust_bin() {
|
|
8
|
+
if [[ -n "${VIDEO_READER_MCP_RUST_BIN:-}" && -x "${VIDEO_READER_MCP_RUST_BIN}" ]]; then
|
|
9
|
+
printf '%s\n' "${VIDEO_READER_MCP_RUST_BIN}"
|
|
10
|
+
return 0
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
for candidate in \
|
|
14
|
+
"$ROOT/bin/native/video-reader-mcp-server" \
|
|
15
|
+
"$ROOT/target/release/video-reader-mcp-server" \
|
|
16
|
+
"$ROOT/target/debug/video-reader-mcp-server"; do
|
|
17
|
+
if [[ -x "$candidate" ]]; then
|
|
18
|
+
printf '%s\n' "$candidate"
|
|
19
|
+
return 0
|
|
20
|
+
fi
|
|
21
|
+
done
|
|
22
|
+
|
|
23
|
+
return 1
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
resolve_transport() {
|
|
27
|
+
if [[ -n "${VIDEO_READER_MCP_TRANSPORT:-}" ]]; then
|
|
28
|
+
printf '%s\n' "${VIDEO_READER_MCP_TRANSPORT}"
|
|
29
|
+
return 0
|
|
30
|
+
fi
|
|
31
|
+
if [[ -n "${MCP_TRANSPORT:-}" ]]; then
|
|
32
|
+
printf '%s\n' "${MCP_TRANSPORT}"
|
|
33
|
+
return 0
|
|
34
|
+
fi
|
|
35
|
+
printf '%s\n' "stdio"
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if bin="$(resolve_rust_bin)"; then
|
|
39
|
+
transport="$(resolve_transport)"
|
|
40
|
+
if [[ "$transport" == "http" ]]; then
|
|
41
|
+
export MCP_TRANSPORT=http
|
|
42
|
+
export VIDEO_READER_MCP_TRANSPORT=http
|
|
43
|
+
fi
|
|
44
|
+
exec "$bin" "$@"
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
echo "[video-reader-mcp] Rust MCP server (rmcp) is not built. Run: bun run build:rust" >&2
|
|
48
|
+
exit 1
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/doctor-cli.ts
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
|
|
6
|
+
// src/doctor.ts
|
|
7
|
+
import { spawnSync } from "node:child_process";
|
|
8
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
9
|
+
import path2 from "node:path";
|
|
10
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
11
|
+
|
|
12
|
+
// src/engine/rust-timeline.ts
|
|
13
|
+
import { existsSync } from "node:fs";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
var here = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
function resolveRustCliBinary() {
|
|
18
|
+
const env = process.env.VIDEO_READER_CLI;
|
|
19
|
+
if (env && existsSync(env)) {
|
|
20
|
+
return env;
|
|
21
|
+
}
|
|
22
|
+
const release = path.join(here, "../../target/release/video-reader-cli");
|
|
23
|
+
if (existsSync(release)) {
|
|
24
|
+
return release;
|
|
25
|
+
}
|
|
26
|
+
const debug = path.join(here, "../../target/debug/video-reader-cli");
|
|
27
|
+
if (existsSync(debug)) {
|
|
28
|
+
return debug;
|
|
29
|
+
}
|
|
30
|
+
return "video-reader-cli";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// src/utils/exec.ts
|
|
34
|
+
import { execFile } from "node:child_process";
|
|
35
|
+
import { promisify } from "node:util";
|
|
36
|
+
var execFileAsync = promisify(execFile);
|
|
37
|
+
var DEFAULT_MAX_BUFFER = 16 * 1024 * 1024;
|
|
38
|
+
var binaryCache = new Map;
|
|
39
|
+
var isBinaryAvailable = async (binary) => {
|
|
40
|
+
const cached = binaryCache.get(binary);
|
|
41
|
+
if (cached !== undefined)
|
|
42
|
+
return cached;
|
|
43
|
+
try {
|
|
44
|
+
await execFileAsync(binary, ["-version"], { timeout: 5000, encoding: "utf8" });
|
|
45
|
+
binaryCache.set(binary, true);
|
|
46
|
+
return true;
|
|
47
|
+
} catch {
|
|
48
|
+
try {
|
|
49
|
+
await execFileAsync(binary, ["--version"], { timeout: 5000, encoding: "utf8" });
|
|
50
|
+
binaryCache.set(binary, true);
|
|
51
|
+
return true;
|
|
52
|
+
} catch {
|
|
53
|
+
binaryCache.set(binary, false);
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// src/doctor.ts
|
|
60
|
+
var here2 = path2.dirname(fileURLToPath2(import.meta.url));
|
|
61
|
+
var probeBinary = async (id, binary, required) => {
|
|
62
|
+
const available = await isBinaryAvailable(binary);
|
|
63
|
+
if (available) {
|
|
64
|
+
return {
|
|
65
|
+
id,
|
|
66
|
+
status: "ok",
|
|
67
|
+
message: `${binary} is installed and responds to -version.`
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
id,
|
|
72
|
+
status: required ? "fail" : "warn",
|
|
73
|
+
message: required ? `${binary} is required for read_video but was not found on PATH.` : `${binary} is optional for some extraction paths.`
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
var probeRustTimelineCli = () => {
|
|
77
|
+
const binary = resolveRustCliBinary();
|
|
78
|
+
if (binary !== "video-reader-cli" && existsSync2(binary)) {
|
|
79
|
+
const probe = spawnSync(binary, [], {
|
|
80
|
+
input: JSON.stringify({
|
|
81
|
+
tool: "build_cache_key",
|
|
82
|
+
input: {
|
|
83
|
+
source_hash: "abc123",
|
|
84
|
+
options: { include_streams: true, include_chapters: true }
|
|
85
|
+
}
|
|
86
|
+
}),
|
|
87
|
+
encoding: "utf8",
|
|
88
|
+
timeout: 5000
|
|
89
|
+
});
|
|
90
|
+
if (probe.status === 0) {
|
|
91
|
+
return {
|
|
92
|
+
id: "rust_timeline_cli",
|
|
93
|
+
status: "ok",
|
|
94
|
+
message: `Rust timeline CLI is available at ${binary}.`
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const release = path2.join(here2, "../target/release/video-reader-cli");
|
|
99
|
+
const debug = path2.join(here2, "../target/debug/video-reader-cli");
|
|
100
|
+
if (existsSync2(release) || existsSync2(debug)) {
|
|
101
|
+
return {
|
|
102
|
+
id: "rust_timeline_cli",
|
|
103
|
+
status: "ok",
|
|
104
|
+
message: "Rust timeline CLI is built locally."
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
id: "rust_timeline_cli",
|
|
109
|
+
status: "warn",
|
|
110
|
+
message: "Rust timeline CLI is not built. Run `cargo build --release` to enable VIDEO_READER_USE_RUST_TIMELINE=1."
|
|
111
|
+
};
|
|
112
|
+
};
|
|
113
|
+
var probeNode = () => {
|
|
114
|
+
const version = process.versions.node;
|
|
115
|
+
const major = Number.parseInt(version.split(".")[0] ?? "0", 10);
|
|
116
|
+
if (major >= 22) {
|
|
117
|
+
return {
|
|
118
|
+
id: "node",
|
|
119
|
+
status: "ok",
|
|
120
|
+
message: `Node.js ${version} meets the >=22.13 requirement.`
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
id: "node",
|
|
125
|
+
status: "warn",
|
|
126
|
+
message: `Node.js ${version} is below the recommended >=22.13 runtime.`
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
var aggregateStatus = (checks) => {
|
|
130
|
+
if (checks.some((check) => check.status === "fail")) {
|
|
131
|
+
return "unavailable";
|
|
132
|
+
}
|
|
133
|
+
if (checks.some((check) => check.status === "warn")) {
|
|
134
|
+
return "degraded";
|
|
135
|
+
}
|
|
136
|
+
return "ready";
|
|
137
|
+
};
|
|
138
|
+
async function runDoctor(version) {
|
|
139
|
+
const checks = [
|
|
140
|
+
probeNode(),
|
|
141
|
+
probeRustTimelineCli(),
|
|
142
|
+
await probeBinary("ffprobe", "ffprobe", true),
|
|
143
|
+
await probeBinary("ffmpeg", "ffmpeg", false)
|
|
144
|
+
];
|
|
145
|
+
return {
|
|
146
|
+
profile: "video_reader_doctor",
|
|
147
|
+
version,
|
|
148
|
+
status: aggregateStatus(checks),
|
|
149
|
+
checks
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function formatDoctorReport(report) {
|
|
153
|
+
return JSON.stringify(report, null, 2);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// src/doctor-cli.ts
|
|
157
|
+
var require2 = createRequire(import.meta.url);
|
|
158
|
+
var packageJson = require2("../package.json");
|
|
159
|
+
async function main() {
|
|
160
|
+
const report = await runDoctor(packageJson.version);
|
|
161
|
+
console.log(formatDoctorReport(report));
|
|
162
|
+
process.exit(report.status === "unavailable" ? 1 : 0);
|
|
163
|
+
}
|
|
164
|
+
main().catch((error) => {
|
|
165
|
+
console.error("[Video Reader MCP] Doctor error:", error);
|
|
166
|
+
process.exit(1);
|
|
167
|
+
});
|
package/package.json
CHANGED
|
@@ -1,23 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sylphx/video-reader-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"mcpName": "io.github.SylphxAI/video-reader-mcp",
|
|
5
|
-
"description": "Evidence-first video reading for AI agents
|
|
5
|
+
"description": "Cue \u2014 Evidence-first video reading for AI agents \u2014 ffprobe, subtitles, scenes, transcripts, and timelines without frame-by-frame LLM vision.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"publishConfig": {
|
|
8
8
|
"access": "public"
|
|
9
9
|
},
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"bin": {
|
|
12
|
-
"video-reader-mcp": "./
|
|
12
|
+
"video-reader-mcp": "./bin/video-reader-mcp",
|
|
13
|
+
"cue": "./bin/video-reader-mcp"
|
|
13
14
|
},
|
|
14
15
|
"files": [
|
|
16
|
+
"bin/",
|
|
15
17
|
"dist/",
|
|
16
18
|
"README.md",
|
|
17
|
-
"LICENSE"
|
|
19
|
+
"LICENSE",
|
|
20
|
+
"src/"
|
|
18
21
|
],
|
|
19
22
|
"exports": {
|
|
20
|
-
".": "./dist/
|
|
23
|
+
".": "./dist/doctor-cli.js",
|
|
24
|
+
"./sdk": "./src/sdk.ts",
|
|
25
|
+
"./cue": "./src/sdk.ts"
|
|
21
26
|
},
|
|
22
27
|
"repository": {
|
|
23
28
|
"type": "git",
|
|
@@ -39,17 +44,24 @@
|
|
|
39
44
|
"node": ">=22.13.0"
|
|
40
45
|
},
|
|
41
46
|
"scripts": {
|
|
42
|
-
"build": "
|
|
47
|
+
"build:rust": "cargo build --release -p video-reader-core -p video-reader-cli -p video-reader-mcp-server && bun scripts/stage-rust-mcp.ts",
|
|
48
|
+
"test:rust": "cargo test",
|
|
49
|
+
"build": "bun build src/doctor-cli.ts --outdir dist --target node && chmod +x dist/doctor-cli.js",
|
|
43
50
|
"typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.test.json",
|
|
44
51
|
"test": "bun test",
|
|
52
|
+
"test:rust-timeline": "cargo test && bun test test/rustTimeline.boundary.test.ts",
|
|
45
53
|
"check": "biome check .",
|
|
46
54
|
"check:fix": "biome check --write .",
|
|
47
55
|
"sync:server-json": "bun scripts/sync-server-json.ts",
|
|
48
56
|
"version-packages": "changeset version && bun run sync:server-json",
|
|
49
|
-
"start": "
|
|
57
|
+
"start": "./bin/video-reader-mcp",
|
|
50
58
|
"clean": "rm -rf dist coverage",
|
|
51
|
-
"prepublishOnly": "bun run clean && bun run build",
|
|
52
|
-
"release": "bun run typecheck && bun run check && bun run build && bun test && changeset publish"
|
|
59
|
+
"prepublishOnly": "bun run clean && bun run build && bun run build:rust",
|
|
60
|
+
"release": "bun run typecheck && bun run check && bun run build && bun test && changeset publish",
|
|
61
|
+
"doctor": "bun run src/doctor-cli.ts",
|
|
62
|
+
"benchmark:release-gate": "bun scripts/release-gate.ts",
|
|
63
|
+
"benchmark:public-proof": "bun scripts/public-proof.ts",
|
|
64
|
+
"brand:pack-plan": "bun scripts/brand-pack-plan.ts"
|
|
53
65
|
},
|
|
54
66
|
"dependencies": {
|
|
55
67
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
@@ -63,5 +75,6 @@
|
|
|
63
75
|
"@types/node": "^25.9.1",
|
|
64
76
|
"typescript": "^7.0.2"
|
|
65
77
|
},
|
|
66
|
-
"packageManager": "bun@1.3.12"
|
|
78
|
+
"packageManager": "bun@1.3.12",
|
|
79
|
+
"private": false
|
|
67
80
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { formatDoctorReport, runDoctor } from './doctor.js';
|
|
5
|
+
|
|
6
|
+
const require = createRequire(import.meta.url);
|
|
7
|
+
const packageJson = require('../package.json') as { version: string };
|
|
8
|
+
|
|
9
|
+
async function main(): Promise<void> {
|
|
10
|
+
const report = await runDoctor(packageJson.version);
|
|
11
|
+
console.log(formatDoctorReport(report));
|
|
12
|
+
process.exit(report.status === 'unavailable' ? 1 : 0);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
main().catch((error: unknown) => {
|
|
16
|
+
console.error('[Video Reader MCP] Doctor error:', error);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
});
|