mcp-video-analyzer 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +245 -0
  3. package/dist/adapters/adapter.interface.d.ts +15 -0
  4. package/dist/adapters/adapter.interface.js +17 -0
  5. package/dist/adapters/adapter.interface.js.map +1 -0
  6. package/dist/adapters/direct.adapter.d.ts +13 -0
  7. package/dist/adapters/direct.adapter.js +67 -0
  8. package/dist/adapters/direct.adapter.js.map +1 -0
  9. package/dist/adapters/loom.adapter.d.ts +13 -0
  10. package/dist/adapters/loom.adapter.js +183 -0
  11. package/dist/adapters/loom.adapter.js.map +1 -0
  12. package/dist/config/detail-levels.d.ts +18 -0
  13. package/dist/config/detail-levels.js +30 -0
  14. package/dist/config/detail-levels.js.map +1 -0
  15. package/dist/index.d.ts +2 -0
  16. package/dist/index.js +5 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/processors/annotated-timeline.d.ts +25 -0
  19. package/dist/processors/annotated-timeline.js +83 -0
  20. package/dist/processors/annotated-timeline.js.map +1 -0
  21. package/dist/processors/audio-transcriber.d.ts +16 -0
  22. package/dist/processors/audio-transcriber.js +191 -0
  23. package/dist/processors/audio-transcriber.js.map +1 -0
  24. package/dist/processors/browser-frame-extractor.d.ts +27 -0
  25. package/dist/processors/browser-frame-extractor.js +132 -0
  26. package/dist/processors/browser-frame-extractor.js.map +1 -0
  27. package/dist/processors/frame-dedup.d.ts +23 -0
  28. package/dist/processors/frame-dedup.js +76 -0
  29. package/dist/processors/frame-dedup.js.map +1 -0
  30. package/dist/processors/frame-extractor.d.ts +19 -0
  31. package/dist/processors/frame-extractor.js +201 -0
  32. package/dist/processors/frame-extractor.js.map +1 -0
  33. package/dist/processors/frame-ocr.d.ts +13 -0
  34. package/dist/processors/frame-ocr.js +45 -0
  35. package/dist/processors/frame-ocr.js.map +1 -0
  36. package/dist/processors/image-optimizer.d.ts +7 -0
  37. package/dist/processors/image-optimizer.js +21 -0
  38. package/dist/processors/image-optimizer.js.map +1 -0
  39. package/dist/server.d.ts +2 -0
  40. package/dist/server.js +67 -0
  41. package/dist/server.js.map +1 -0
  42. package/dist/tools/analyze-moment.d.ts +2 -0
  43. package/dist/tools/analyze-moment.js +145 -0
  44. package/dist/tools/analyze-moment.js.map +1 -0
  45. package/dist/tools/analyze-video.d.ts +2 -0
  46. package/dist/tools/analyze-video.js +320 -0
  47. package/dist/tools/analyze-video.js.map +1 -0
  48. package/dist/tools/get-frame-at.d.ts +2 -0
  49. package/dist/tools/get-frame-at.js +88 -0
  50. package/dist/tools/get-frame-at.js.map +1 -0
  51. package/dist/tools/get-frame-burst.d.ts +2 -0
  52. package/dist/tools/get-frame-burst.js +106 -0
  53. package/dist/tools/get-frame-burst.js.map +1 -0
  54. package/dist/tools/get-frames.d.ts +2 -0
  55. package/dist/tools/get-frames.js +143 -0
  56. package/dist/tools/get-frames.js.map +1 -0
  57. package/dist/tools/get-metadata.d.ts +2 -0
  58. package/dist/tools/get-metadata.js +65 -0
  59. package/dist/tools/get-metadata.js.map +1 -0
  60. package/dist/tools/get-transcript.d.ts +2 -0
  61. package/dist/tools/get-transcript.js +82 -0
  62. package/dist/tools/get-transcript.js.map +1 -0
  63. package/dist/types.d.ts +62 -0
  64. package/dist/types.js +2 -0
  65. package/dist/types.js.map +1 -0
  66. package/dist/utils/cache.d.ts +31 -0
  67. package/dist/utils/cache.js +87 -0
  68. package/dist/utils/cache.js.map +1 -0
  69. package/dist/utils/field-filter.d.ts +10 -0
  70. package/dist/utils/field-filter.js +32 -0
  71. package/dist/utils/field-filter.js.map +1 -0
  72. package/dist/utils/temp-files.d.ts +3 -0
  73. package/dist/utils/temp-files.js +28 -0
  74. package/dist/utils/temp-files.js.map +1 -0
  75. package/dist/utils/url-detector.d.ts +4 -0
  76. package/dist/utils/url-detector.js +33 -0
  77. package/dist/utils/url-detector.js.map +1 -0
  78. package/dist/utils/vtt-parser.d.ts +2 -0
  79. package/dist/utils/vtt-parser.js +85 -0
  80. package/dist/utils/vtt-parser.js.map +1 -0
  81. package/package.json +78 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Guilherme
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 ADDED
@@ -0,0 +1,245 @@
1
+ # mcp-video-analyzer
2
+
3
+ MCP server for video analysis — extracts transcripts, key frames, and metadata from video URLs. Supports Loom, direct video files (.mp4, .webm), and more.
4
+
5
+ No existing video MCP combines **transcripts + visual frames + metadata** in one tool. This one does.
6
+
7
+ ## Quick Start
8
+
9
+ ```bash
10
+ # One-command install for Claude Code
11
+ claude mcp add video-analyzer npx mcp-video-analyzer@latest
12
+ ```
13
+
14
+ Or manually add to your MCP config (Claude Desktop, Cursor, VS Code):
15
+
16
+ ```json
17
+ {
18
+ "mcpServers": {
19
+ "video-analyzer": {
20
+ "command": "npx",
21
+ "args": ["mcp-video-analyzer@latest"]
22
+ }
23
+ }
24
+ }
25
+ ```
26
+
27
+ ## Tools
28
+
29
+ ### `analyze_video` — Full video analysis
30
+
31
+ Extracts everything from a video URL in one call:
32
+
33
+ ```
34
+ > Analyze this video: https://www.loom.com/share/abc123...
35
+ ```
36
+
37
+ Returns:
38
+ - **Transcript** with timestamps and speakers
39
+ - **Key frames** extracted via scene-change detection (automatically deduplicated)
40
+ - **OCR text** extracted from frames (code, error messages, UI text visible on screen)
41
+ - **Annotated timeline** merging transcript + frames + OCR into a unified "what happened when" view
42
+ - **Metadata** (title, duration, platform)
43
+ - **Comments** from viewers
44
+ - **Chapters** and **AI summary** (when available)
45
+
46
+ The AI will **automatically** call this tool when it sees a video URL — no need to ask.
47
+
48
+ Options:
49
+ - `detail` — analysis depth: `"brief"` (metadata + truncated transcript, no frames), `"standard"` (default), `"detailed"` (dense sampling, more frames)
50
+ - `fields` — array of specific fields to return, e.g. `["metadata", "transcript"]`. Available: `metadata`, `transcript`, `frames`, `comments`, `chapters`, `ocrResults`, `timeline`, `aiSummary`
51
+ - `maxFrames` (1-60, default depends on detail level) — cap on extracted frames
52
+ - `threshold` (0.0-1.0, default 0.1) — scene-change sensitivity
53
+ - `forceRefresh` — bypass cache and re-analyze
54
+ - `skipFrames` — skip frame extraction for transcript-only analysis
55
+
56
+ ### `get_transcript` — Transcript only
57
+
58
+ ```
59
+ > Get the transcript from this video
60
+ ```
61
+
62
+ Quick transcript extraction. Falls back to Whisper transcription when no native transcript is available.
63
+
64
+ ### `get_metadata` — Metadata only
65
+
66
+ ```
67
+ > What's this video about?
68
+ ```
69
+
70
+ Returns metadata, comments, chapters, and AI summary without downloading the video.
71
+
72
+ ### `get_frames` — Frames only
73
+
74
+ ```
75
+ > Extract frames from this video with dense sampling
76
+ ```
77
+
78
+ Two modes:
79
+ - **Scene-change detection** (default) — captures visual transitions
80
+ - **Dense sampling** (`dense: true`) — 1 frame/sec for full coverage
81
+
82
+ ### `analyze_moment` — Deep-dive on a time range
83
+
84
+ ```
85
+ > Analyze what happens between 1:30 and 2:00 in this video
86
+ ```
87
+
88
+ Combines burst frame extraction + filtered transcript + OCR + annotated timeline for a focused segment. Use when you need to understand exactly what happens at a specific moment.
89
+
90
+ ### `get_frame_at` — Single frame at a timestamp
91
+
92
+ ```
93
+ > Show me the frame at 1:23 in this video
94
+ ```
95
+
96
+ The AI reads the transcript, spots a critical moment, and requests the exact frame to see what's on screen.
97
+
98
+ ### `get_frame_burst` — N frames in a time range
99
+
100
+ ```
101
+ > Show me 10 frames between 0:15 and 0:17 of this video
102
+ ```
103
+
104
+ For motion, vibration, animations, or fast scrolling — burst mode captures N frames in a narrow window so the AI can see frame-by-frame changes.
105
+
106
+ ## Detail Levels
107
+
108
+ | Level | Frames | Transcript | OCR | Timeline | Use case |
109
+ |-------|--------|-----------|-----|----------|----------|
110
+ | `brief` | None | First 10 entries | No | No | Quick check — what's this video about? |
111
+ | `standard` | Up to 20 (scene-change) | Full | Yes | Yes | Default — full analysis |
112
+ | `detailed` | Up to 60 (1fps dense) | Full | Yes | Yes | Deep analysis — every second captured |
113
+
114
+ ## Caching
115
+
116
+ Results are cached in memory for 10 minutes. Subsequent calls with the same URL and options return instantly. Use `forceRefresh: true` to bypass the cache.
117
+
118
+ ## Supported Platforms
119
+
120
+ | Platform | Transcript | Metadata | Comments | Frames | Auth |
121
+ |----------|:----------:|:--------:|:--------:|:------:|:----:|
122
+ | **Loom** | Yes | Yes | Yes | Yes | None |
123
+ | **Direct URL** (.mp4, .webm) | No | Duration only | No | Yes | None |
124
+
125
+ ### Frame Extraction Strategies
126
+
127
+ Frame extraction uses a two-strategy fallback chain — no single dependency is required:
128
+
129
+ | Strategy | How it works | Speed | Requirements |
130
+ |----------|-------------|-------|-------------|
131
+ | **yt-dlp + ffmpeg** (primary) | Downloads video, extracts frames via scene detection | Fast, precise | [yt-dlp](https://github.com/yt-dlp/yt-dlp) (`pip install yt-dlp`) |
132
+ | **Browser** (fallback) | Opens video in headless Chrome, seeks to timestamps, takes screenshots | Slower, no download needed | Chrome or Chromium installed |
133
+
134
+ The fallback is automatic — if yt-dlp is not available, the server tries browser-based extraction via `puppeteer-core`. If neither is available, analysis still returns transcript + metadata + comments, just no frames.
135
+
136
+ ### Post-Processing Pipeline
137
+
138
+ After frame extraction, the pipeline automatically applies:
139
+
140
+ | Step | What it does | Why |
141
+ |------|-------------|-----|
142
+ | **Frame deduplication** | Removes near-identical consecutive frames using perceptual hashing (dHash + Hamming distance) | Screencasts often have long static moments — dedup removes redundant frames, saving tokens |
143
+ | **OCR** | Extracts text visible on screen from each frame (via tesseract.js) | Captures code, error messages, terminal output, UI text that the transcript doesn't cover |
144
+ | **Annotated timeline** | Merges transcript timestamps + frame timestamps + OCR text into a single chronological view | Gives the AI a unified "what was said, what changed visually, and what text appeared" at each moment |
145
+
146
+ The OCR step requires `tesseract.js` (included as a dependency). If it fails to load, analysis continues without OCR — no frames or transcript are lost.
147
+
148
+ ## Complementary Tools
149
+
150
+ ### Chrome DevTools MCP
151
+
152
+ For **live web debugging** alongside video analysis, pair this server with the [Chrome DevTools MCP](https://github.com/anthropics/anthropic-quickstarts/tree/main/mcp-devtools):
153
+
154
+ ```bash
155
+ claude mcp add chrome-devtools npx @anthropic-ai/mcp-devtools@latest
156
+ ```
157
+
158
+ **When to use each:**
159
+
160
+ | Scenario | Tool |
161
+ |----------|------|
162
+ | Bug report recorded as a Loom video | `mcp-video-analyzer` — extract transcript, frames, and error text from the recording |
163
+ | Live debugging a web page | Chrome DevTools MCP — inspect DOM, console, network, take screenshots |
164
+ | Video shows UI issue, need to reproduce it | Use both: analyze the video first, then open the page in Chrome DevTools to reproduce |
165
+
166
+ The two MCPs complement each other: video analyzer understands **recorded** content, DevTools interacts with **live** pages.
167
+
168
+ ## Example Output
169
+
170
+ The [`examples/loom-demo/`](examples/loom-demo/) folder contains **real outputs** from analyzing a public Loom video ([Boost In-App Demo Video](https://www.loom.com/share/bdebdfe44b294225ac718bad241a94fe), 2:55).
171
+
172
+ | File | What it shows |
173
+ |------|--------------|
174
+ | [`metadata.json`](examples/loom-demo/metadata.json) | Title, duration, platform |
175
+ | [`transcript.json`](examples/loom-demo/transcript.json) | 42 timestamped entries with speaker IDs |
176
+ | [`timeline.json`](examples/loom-demo/timeline.json) | Unified chronological view (transcript + frames merged) |
177
+ | [`moment-transcript-0m30s-0m45s.json`](examples/loom-demo/moment-transcript-0m30s-0m45s.json) | Filtered transcript for `analyze_moment` (0:30–0:45) |
178
+ | [`full-analysis.json`](examples/loom-demo/full-analysis.json) | Complete `analyze_video` output |
179
+
180
+ **Frame images** (19 total in [`examples/loom-demo/frames/`](examples/loom-demo/frames/)):
181
+ - `scene_*.jpg` — scene-change detection (key visual transitions)
182
+ - `dense_*.jpg` — 1fps dense sampling (every 10th frame saved as sample)
183
+ - `burst_*.jpg` — burst extraction for moment analysis (0:30–0:45)
184
+
185
+ > **Regenerate after changes:** `npx tsx examples/generate.ts` — requires yt-dlp + network access.
186
+
187
+ ## Development
188
+
189
+ ```bash
190
+ # Install dependencies
191
+ npm install
192
+
193
+ # Run all checks (format, lint, typecheck, knip, tests)
194
+ npm run check
195
+
196
+ # Build
197
+ npm run build
198
+
199
+ # Run E2E tests (requires network)
200
+ npm run test:e2e
201
+
202
+ # Open MCP Inspector for manual testing
203
+ npm run inspect
204
+ ```
205
+
206
+ ## Architecture
207
+
208
+ ```
209
+ src/
210
+ ├── index.ts # Entry point (shebang + stdio)
211
+ ├── server.ts # FastMCP server + tool registration
212
+ ├── tools/ # MCP tool definitions (7 tools)
213
+ │ ├── analyze-video.ts # Full analysis with detail levels + caching
214
+ │ ├── analyze-moment.ts # Deep-dive on a time range
215
+ │ ├── get-transcript.ts # Transcript-only with Whisper fallback
216
+ │ ├── get-metadata.ts # Metadata + comments + chapters
217
+ │ ├── get-frames.ts # Frames-only (scene-change or dense)
218
+ │ ├── get-frame-at.ts # Single frame at timestamp
219
+ │ └── get-frame-burst.ts # N frames in a time range
220
+ ├── adapters/ # Platform-specific logic
221
+ │ ├── adapter.interface.ts # IVideoAdapter interface + registry
222
+ │ ├── loom.adapter.ts # Loom: authless GraphQL
223
+ │ └── direct.adapter.ts # Direct URL: any mp4/webm link
224
+ ├── processors/ # Shared processing
225
+ │ ├── frame-extractor.ts # ffmpeg scene detection + dense + burst extraction
226
+ │ ├── browser-frame-extractor.ts # Headless Chrome fallback for frames
227
+ │ ├── audio-transcriber.ts # Whisper fallback (HF transformers → CLI → OpenAI)
228
+ │ ├── image-optimizer.ts # sharp resize/compress
229
+ │ ├── frame-dedup.ts # Perceptual dedup (dHash + Hamming distance)
230
+ │ ├── frame-ocr.ts # OCR text extraction (tesseract.js)
231
+ │ └── annotated-timeline.ts # Unified timeline (transcript + frames + OCR)
232
+ ├── config/
233
+ │ └── detail-levels.ts # brief / standard / detailed config
234
+ ├── utils/
235
+ │ ├── cache.ts # In-memory TTL cache with LRU eviction
236
+ │ ├── field-filter.ts # Selective field filtering for responses
237
+ │ ├── url-detector.ts # Platform detection from URL
238
+ │ ├── vtt-parser.ts # WebVTT → transcript entries
239
+ │ └── temp-files.ts # Temp directory management
240
+ └── types.ts # Shared TypeScript interfaces
241
+ ```
242
+
243
+ ## License
244
+
245
+ MIT
@@ -0,0 +1,15 @@
1
+ import type { ITranscriptEntry, IVideoMetadata, IVideoComment, IChapter, IAdapterCapabilities } from '../types.js';
2
+ export interface IVideoAdapter {
3
+ readonly name: string;
4
+ readonly capabilities: IAdapterCapabilities;
5
+ canHandle(url: string): boolean;
6
+ getMetadata(url: string): Promise<IVideoMetadata>;
7
+ getTranscript(url: string): Promise<ITranscriptEntry[]>;
8
+ getComments(url: string): Promise<IVideoComment[]>;
9
+ getChapters(url: string): Promise<IChapter[]>;
10
+ getAiSummary(url: string): Promise<string | null>;
11
+ downloadVideo(url: string, destDir: string): Promise<string | null>;
12
+ }
13
+ export declare function registerAdapter(adapter: IVideoAdapter): void;
14
+ export declare function getAdapter(url: string): IVideoAdapter;
15
+ export declare function clearAdapters(): void;
@@ -0,0 +1,17 @@
1
+ import { UserError } from 'fastmcp';
2
+ const adapters = [];
3
+ export function registerAdapter(adapter) {
4
+ adapters.push(adapter);
5
+ }
6
+ export function getAdapter(url) {
7
+ for (const adapter of adapters) {
8
+ if (adapter.canHandle(url)) {
9
+ return adapter;
10
+ }
11
+ }
12
+ throw new UserError(`Unsupported video URL: "${url}". Supported platforms: Loom (loom.com/share/...), direct video files (.mp4, .webm, .mov).`);
13
+ }
14
+ export function clearAdapters() {
15
+ adapters.length = 0;
16
+ }
17
+ //# sourceMappingURL=adapter.interface.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.interface.js","sourceRoot":"","sources":["../../src/adapters/adapter.interface.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAepC,MAAM,QAAQ,GAAoB,EAAE,CAAC;AAErC,MAAM,UAAU,eAAe,CAAC,OAAsB;IACpD,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;AACzB,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,GAAW;IACpC,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,IAAI,OAAO,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC;YAC3B,OAAO,OAAO,CAAC;QACjB,CAAC;IACH,CAAC;IACD,MAAM,IAAI,SAAS,CACjB,2BAA2B,GAAG,4FAA4F,CAC3H,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,aAAa;IAC3B,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;AACtB,CAAC"}
@@ -0,0 +1,13 @@
1
+ import type { ITranscriptEntry, IVideoMetadata, IVideoComment, IChapter, IAdapterCapabilities } from '../types.js';
2
+ import type { IVideoAdapter } from './adapter.interface.js';
3
+ export declare class DirectAdapter implements IVideoAdapter {
4
+ readonly name = "direct";
5
+ readonly capabilities: IAdapterCapabilities;
6
+ canHandle(url: string): boolean;
7
+ getMetadata(url: string): Promise<IVideoMetadata>;
8
+ getTranscript(_url: string): Promise<ITranscriptEntry[]>;
9
+ getComments(_url: string): Promise<IVideoComment[]>;
10
+ getChapters(_url: string): Promise<IChapter[]>;
11
+ getAiSummary(_url: string): Promise<string | null>;
12
+ downloadVideo(url: string, destDir: string): Promise<string | null>;
13
+ }
@@ -0,0 +1,67 @@
1
+ import { createWriteStream } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { pipeline } from 'node:stream/promises';
4
+ import { Readable } from 'node:stream';
5
+ import { detectPlatform } from '../utils/url-detector.js';
6
+ function getFilenameFromUrl(url) {
7
+ try {
8
+ const parsed = new URL(url);
9
+ const segments = parsed.pathname.split('/');
10
+ const lastSegment = segments[segments.length - 1];
11
+ if (lastSegment && lastSegment.includes('.')) {
12
+ return lastSegment;
13
+ }
14
+ }
15
+ catch {
16
+ // ignore parse errors
17
+ }
18
+ return 'video.mp4';
19
+ }
20
+ export class DirectAdapter {
21
+ name = 'direct';
22
+ capabilities = {
23
+ transcript: false,
24
+ metadata: false,
25
+ comments: false,
26
+ chapters: false,
27
+ aiSummary: false,
28
+ videoDownload: true,
29
+ };
30
+ canHandle(url) {
31
+ return detectPlatform(url) === 'direct';
32
+ }
33
+ async getMetadata(url) {
34
+ const filename = getFilenameFromUrl(url);
35
+ return {
36
+ platform: 'direct',
37
+ title: filename,
38
+ duration: 0,
39
+ durationFormatted: '0:00',
40
+ url,
41
+ };
42
+ }
43
+ async getTranscript(_url) {
44
+ return [];
45
+ }
46
+ async getComments(_url) {
47
+ return [];
48
+ }
49
+ async getChapters(_url) {
50
+ return [];
51
+ }
52
+ async getAiSummary(_url) {
53
+ return null;
54
+ }
55
+ async downloadVideo(url, destDir) {
56
+ const filename = getFilenameFromUrl(url);
57
+ const destPath = join(destDir, filename);
58
+ const response = await fetch(url);
59
+ if (!response.ok || !response.body) {
60
+ return null;
61
+ }
62
+ const nodeStream = Readable.fromWeb(response.body);
63
+ await pipeline(nodeStream, createWriteStream(destPath));
64
+ return destPath;
65
+ }
66
+ }
67
+ //# sourceMappingURL=direct.adapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"direct.adapter.js","sourceRoot":"","sources":["../../src/adapters/direct.adapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAChD,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AASvC,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAE1D,SAAS,kBAAkB,CAAC,GAAW;IACrC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;QAC5B,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC5C,MAAM,WAAW,GAAG,QAAQ,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QAClD,IAAI,WAAW,IAAI,WAAW,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;YAC7C,OAAO,WAAW,CAAC;QACrB,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,sBAAsB;IACxB,CAAC;IACD,OAAO,WAAW,CAAC;AACrB,CAAC;AAED,MAAM,OAAO,aAAa;IACf,IAAI,GAAG,QAAQ,CAAC;IAChB,YAAY,GAAyB;QAC5C,UAAU,EAAE,KAAK;QACjB,QAAQ,EAAE,KAAK;QACf,QAAQ,EAAE,KAAK;QACf,QAAQ,EAAE,KAAK;QACf,SAAS,EAAE,KAAK;QAChB,aAAa,EAAE,IAAI;KACpB,CAAC;IAEF,SAAS,CAAC,GAAW;QACnB,OAAO,cAAc,CAAC,GAAG,CAAC,KAAK,QAAQ,CAAC;IAC1C,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,GAAW;QAC3B,MAAM,QAAQ,GAAG,kBAAkB,CAAC,GAAG,CAAC,CAAC;QACzC,OAAO;YACL,QAAQ,EAAE,QAAQ;YAClB,KAAK,EAAE,QAAQ;YACf,QAAQ,EAAE,CAAC;YACX,iBAAiB,EAAE,MAAM;YACzB,GAAG;SACJ,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,aAAa,CAAC,IAAY;QAC9B,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,IAAY;QAC5B,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,IAAY;QAC5B,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,IAAY;QAC7B,OAAO,IAAI,CAAC;IACd,CAAC;IAED,KAAK,CAAC,aAAa,CAAC,GAAW,EAAE,OAAe;QAC9C,MAAM,QAAQ,GAAG,kBAAkB,CAAC,GAAG,CAAC,CAAC;QACzC,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;QAEzC,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,CAAC;QAClC,IAAI,CAAC,QAAQ,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;YACnC,OAAO,IAAI,CAAC;QACd,CAAC;QAED,MAAM,UAAU,GAAG,QAAQ,CAAC,OAAO,CAAC,QAAQ,CAAC,IAA8C,CAAC,CAAC;QAC7F,MAAM,QAAQ,CAAC,UAAU,EAAE,iBAAiB,CAAC,QAAQ,CAAC,CAAC,CAAC;QAExD,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF"}
@@ -0,0 +1,13 @@
1
+ import type { ITranscriptEntry, IVideoMetadata, IVideoComment, IChapter, IAdapterCapabilities } from '../types.js';
2
+ import type { IVideoAdapter } from './adapter.interface.js';
3
+ export declare class LoomAdapter implements IVideoAdapter {
4
+ readonly name = "loom";
5
+ readonly capabilities: IAdapterCapabilities;
6
+ canHandle(url: string): boolean;
7
+ getMetadata(url: string): Promise<IVideoMetadata>;
8
+ getTranscript(url: string): Promise<ITranscriptEntry[]>;
9
+ getComments(url: string): Promise<IVideoComment[]>;
10
+ getChapters(_url: string): Promise<IChapter[]>;
11
+ getAiSummary(_url: string): Promise<string | null>;
12
+ downloadVideo(url: string, destDir: string): Promise<string | null>;
13
+ }
@@ -0,0 +1,183 @@
1
+ import { execFile as execFileCb } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ import { join } from 'node:path';
4
+ import { existsSync } from 'node:fs';
5
+ import { detectPlatform, extractLoomId } from '../utils/url-detector.js';
6
+ import { parseVtt } from '../utils/vtt-parser.js';
7
+ const execFile = promisify(execFileCb);
8
+ const LOOM_GRAPHQL_URL = 'https://www.loom.com/graphql';
9
+ const GRAPHQL_HEADERS = {
10
+ 'Content-Type': 'application/json',
11
+ Accept: 'application/json',
12
+ 'User-Agent': 'mcp-video-analyzer/0.1.0',
13
+ };
14
+ async function loomGraphQL(query, variables) {
15
+ const response = await fetch(LOOM_GRAPHQL_URL, {
16
+ method: 'POST',
17
+ headers: GRAPHQL_HEADERS,
18
+ body: JSON.stringify({ query, variables }),
19
+ });
20
+ if (!response.ok) {
21
+ return null;
22
+ }
23
+ const json = (await response.json());
24
+ if (json.errors?.length) {
25
+ return null;
26
+ }
27
+ return json.data ?? null;
28
+ }
29
+ function formatDuration(seconds) {
30
+ const hrs = Math.floor(seconds / 3600);
31
+ const mins = Math.floor((seconds % 3600) / 60);
32
+ const secs = Math.floor(seconds % 60);
33
+ if (hrs > 0) {
34
+ return `${hrs}:${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
35
+ }
36
+ return `${mins}:${String(secs).padStart(2, '0')}`;
37
+ }
38
+ function timestampFromMs(ms) {
39
+ return formatDuration(ms / 1000);
40
+ }
41
+ export class LoomAdapter {
42
+ name = 'loom';
43
+ capabilities = {
44
+ transcript: true,
45
+ metadata: true,
46
+ comments: true,
47
+ chapters: false,
48
+ aiSummary: false,
49
+ videoDownload: true,
50
+ };
51
+ canHandle(url) {
52
+ return detectPlatform(url) === 'loom';
53
+ }
54
+ async getMetadata(url) {
55
+ const videoId = extractLoomId(url);
56
+ const data = await loomGraphQL(`query GetVideo($videoId: ID!, $password: String) {
57
+ getVideo(id: $videoId, password: $password) {
58
+ ... on RegularUserVideo {
59
+ __typename id name description playable_duration
60
+ owner { display_name }
61
+ createdAt
62
+ }
63
+ ... on PrivateVideo {
64
+ __typename id
65
+ }
66
+ }
67
+ }`, { videoId, password: null });
68
+ const video = data?.getVideo;
69
+ return {
70
+ platform: 'loom',
71
+ title: video?.name ?? 'Untitled Loom Video',
72
+ description: video?.description ?? undefined,
73
+ duration: video?.playable_duration ?? 0,
74
+ durationFormatted: formatDuration(video?.playable_duration ?? 0),
75
+ url,
76
+ };
77
+ }
78
+ async getTranscript(url) {
79
+ const videoId = extractLoomId(url);
80
+ const data = await loomGraphQL(`query FetchVideoTranscript($videoId: ID!, $password: String) {
81
+ fetchVideoTranscript(videoId: $videoId, password: $password) {
82
+ ... on VideoTranscriptDetails {
83
+ captions_source_url
84
+ source_url
85
+ transcription_status
86
+ language
87
+ }
88
+ }
89
+ }`, { videoId, password: null });
90
+ const captionsUrl = data?.fetchVideoTranscript?.captions_source_url;
91
+ if (!captionsUrl) {
92
+ return [];
93
+ }
94
+ const vttResponse = await fetch(captionsUrl);
95
+ if (!vttResponse.ok) {
96
+ return [];
97
+ }
98
+ const vttContent = await vttResponse.text();
99
+ return parseVtt(vttContent);
100
+ }
101
+ async getComments(url) {
102
+ const videoId = extractLoomId(url);
103
+ const data = await loomGraphQL(`query FetchVideoComments($videoId: ID!, $password: String) {
104
+ getVideo(id: $videoId, password: $password) {
105
+ ... on RegularUserVideo {
106
+ comments {
107
+ id plain_content time_stamp user_name createdAt
108
+ children_comments {
109
+ id plain_content time_stamp user_name createdAt
110
+ }
111
+ }
112
+ }
113
+ }
114
+ }`, { videoId, password: null });
115
+ const comments = data?.getVideo?.comments ?? [];
116
+ return flattenComments(comments);
117
+ }
118
+ async getChapters(_url) {
119
+ return [];
120
+ }
121
+ async getAiSummary(_url) {
122
+ return null;
123
+ }
124
+ async downloadVideo(url, destDir) {
125
+ // Try yt-dlp for Loom video download (works without auth for public videos)
126
+ const ytDlp = await findYtDlp();
127
+ if (!ytDlp)
128
+ return null;
129
+ const videoId = extractLoomId(url);
130
+ const outputPath = join(destDir, `${videoId ?? 'loom_video'}.mp4`);
131
+ try {
132
+ await execFile(ytDlp.bin, [...ytDlp.prefix, '-o', outputPath, '--no-warnings', '-q', url], {
133
+ timeout: 120000,
134
+ });
135
+ return existsSync(outputPath) ? outputPath : null;
136
+ }
137
+ catch {
138
+ return null;
139
+ }
140
+ }
141
+ }
142
+ function flattenComments(comments) {
143
+ const result = [];
144
+ for (const comment of comments) {
145
+ result.push({
146
+ author: comment.user_name ?? 'Unknown',
147
+ text: comment.plain_content,
148
+ time: comment.time_stamp != null ? timestampFromMs(comment.time_stamp) : undefined,
149
+ createdAt: comment.createdAt,
150
+ });
151
+ if (comment.children_comments?.length) {
152
+ for (const child of comment.children_comments) {
153
+ result.push({
154
+ author: child.user_name ?? 'Unknown',
155
+ text: child.plain_content,
156
+ time: child.time_stamp != null ? timestampFromMs(child.time_stamp) : undefined,
157
+ createdAt: child.createdAt,
158
+ });
159
+ }
160
+ }
161
+ }
162
+ return result;
163
+ }
164
+ async function findYtDlp() {
165
+ for (const bin of ['yt-dlp', 'yt-dlp.exe']) {
166
+ try {
167
+ await execFile(bin, ['--version'], { timeout: 5000 });
168
+ return { bin, prefix: [] };
169
+ }
170
+ catch {
171
+ // not found, try next
172
+ }
173
+ }
174
+ // Try python module
175
+ try {
176
+ await execFile('python', ['-m', 'yt_dlp', '--version'], { timeout: 5000 });
177
+ return { bin: 'python', prefix: ['-m', 'yt_dlp'] };
178
+ }
179
+ catch {
180
+ return null;
181
+ }
182
+ }
183
+ //# sourceMappingURL=loom.adapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loom.adapter.js","sourceRoot":"","sources":["../../src/adapters/loom.adapter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ export type DetailLevel = 'brief' | 'standard' | 'detailed';
2
+ interface DetailConfig {
3
+ /** Maximum frames to extract (0 = skip frames entirely) */
4
+ maxFrames: number;
5
+ /** Maximum transcript entries to return (null = unlimited) */
6
+ transcriptMaxEntries: number | null;
7
+ /** Whether to run OCR on extracted frames */
8
+ includeOcr: boolean;
9
+ /** Whether to build the annotated timeline */
10
+ includeTimeline: boolean;
11
+ /** Whether to extract frames at all */
12
+ includeFrames: boolean;
13
+ /** Whether to use dense sampling (1 fps) instead of scene detection */
14
+ denseSampling: boolean;
15
+ }
16
+ export declare const DETAIL_CONFIGS: Record<DetailLevel, DetailConfig>;
17
+ export declare function getDetailConfig(level: DetailLevel): DetailConfig;
18
+ export {};
@@ -0,0 +1,30 @@
1
+ export const DETAIL_CONFIGS = {
2
+ brief: {
3
+ maxFrames: 0,
4
+ transcriptMaxEntries: 10,
5
+ includeOcr: false,
6
+ includeTimeline: false,
7
+ includeFrames: false,
8
+ denseSampling: false,
9
+ },
10
+ standard: {
11
+ maxFrames: 20,
12
+ transcriptMaxEntries: null,
13
+ includeOcr: true,
14
+ includeTimeline: true,
15
+ includeFrames: true,
16
+ denseSampling: false,
17
+ },
18
+ detailed: {
19
+ maxFrames: 60,
20
+ transcriptMaxEntries: null,
21
+ includeOcr: true,
22
+ includeTimeline: true,
23
+ includeFrames: true,
24
+ denseSampling: true,
25
+ },
26
+ };
27
+ export function getDetailConfig(level) {
28
+ return DETAIL_CONFIGS[level];
29
+ }
30
+ //# sourceMappingURL=detail-levels.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detail-levels.js","sourceRoot":"","sources":["../../src/config/detail-levels.ts"],"names":[],"mappings":"AAiBA,MAAM,CAAC,MAAM,cAAc,GAAsC;IAC/D,KAAK,EAAE;QACL,SAAS,EAAE,CAAC;QACZ,oBAAoB,EAAE,EAAE;QACxB,UAAU,EAAE,KAAK;QACjB,eAAe,EAAE,KAAK;QACtB,aAAa,EAAE,KAAK;QACpB,aAAa,EAAE,KAAK;KACrB;IACD,QAAQ,EAAE;QACR,SAAS,EAAE,EAAE;QACb,oBAAoB,EAAE,IAAI;QAC1B,UAAU,EAAE,IAAI;QAChB,eAAe,EAAE,IAAI;QACrB,aAAa,EAAE,IAAI;QACnB,aAAa,EAAE,KAAK;KACrB;IACD,QAAQ,EAAE;QACR,SAAS,EAAE,EAAE;QACb,oBAAoB,EAAE,IAAI;QAC1B,UAAU,EAAE,IAAI;QAChB,eAAe,EAAE,IAAI;QACrB,aAAa,EAAE,IAAI;QACnB,aAAa,EAAE,IAAI;KACpB;CACF,CAAC;AAEF,MAAM,UAAU,eAAe,CAAC,KAAkB;IAChD,OAAO,cAAc,CAAC,KAAK,CAAC,CAAC;AAC/B,CAAC"}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import { createServer } from './server.js';
3
+ const server = createServer();
4
+ server.start({ transportType: 'stdio' });
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,MAAM,MAAM,GAAG,YAAY,EAAE,CAAC;AAC9B,MAAM,CAAC,KAAK,CAAC,EAAE,aAAa,EAAE,OAAO,EAAE,CAAC,CAAC"}