ytdl-rmcp 0.7.1 → 0.7.2
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 +361 -32
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,53 +1,382 @@
|
|
|
1
1
|
# ytdl-rmcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/ytdl-rmcp)
|
|
4
|
+
[](https://github.com/jmagar/ytdl-rmcp/actions/workflows/release.yml)
|
|
5
|
+
[](https://github.com/jmagar/ytdl-rmcp/actions/workflows/ci.yml)
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
A cross-platform, single-binary **MCP server** that downloads media from any
|
|
8
|
+
[yt-dlp](https://github.com/yt-dlp/yt-dlp)-supported site (YouTube, Vimeo, …),
|
|
9
|
+
embeds metadata and cover art, organizes files by artist, and transfers the
|
|
10
|
+
result to a directory on an SSH remote — over `rsync` (with an `scp` fallback for
|
|
11
|
+
hosts that lack it, e.g. Windows).
|
|
12
|
+
|
|
13
|
+
Written in Rust on the [`rmcp`](https://crates.io/crates/rmcp) crate. **yt-dlp
|
|
14
|
+
and ffmpeg are auto-downloaded** into a per-user cache on first run, so the host
|
|
15
|
+
needs neither pre-installed — the one binary is the whole install.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- **Audio, video, or both** — audio-first by default, with separate remote
|
|
22
|
+
destinations for audio and video.
|
|
23
|
+
- **Proper tagging** — embeds title / artist / album / date and cover art, and
|
|
24
|
+
organizes output as `Artist/Title [id].ext` so media servers (Plex, etc.)
|
|
25
|
+
index it cleanly. A non-greedy `Artist - Title` parse recovers the artist from
|
|
26
|
+
free-form video titles. Source `.info.json`, thumbnail, and description
|
|
27
|
+
sidecars are preserved next to the media for future retagging/indexing.
|
|
28
|
+
Common YouTube title noise like `(Official Video)`, `[Official Audio]`, and
|
|
29
|
+
trailing channel handles is stripped from embedded title metadata by default.
|
|
30
|
+
- **Self-contained paths** — the binary downloads/caches yt-dlp + ffmpeg when
|
|
31
|
+
run directly; the container image bakes in ffmpeg, fpcalc, SSH, and rsync for
|
|
32
|
+
media-host batch jobs.
|
|
33
|
+
- **Self-installing** — `ytdl-rmcp setup` registers the server into Claude Code,
|
|
34
|
+
Codex, and/or Gemini CLI via each tool's own `mcp add`.
|
|
35
|
+
- **Robust transfers** — `rsync --protect-args` when present, `scp` otherwise;
|
|
36
|
+
non-interactive SSH (`BatchMode=yes`, `StrictHostKeyChecking=accept-new`) so a
|
|
37
|
+
TTY-less server never hangs on a prompt. On transfer failure the local staging
|
|
38
|
+
copy is kept for retry.
|
|
39
|
+
- **Repeat-safe** — `use_archive` records downloaded IDs (per mode) and skips
|
|
40
|
+
them on later runs; YouTube mix/radio URLs are auto-cleaned to the seed video.
|
|
41
|
+
- **Stats-ready ledger** — every completed download call appends a JSONL entry
|
|
42
|
+
with timestamp, destinations, files, bytes, uploader, and transfer status.
|
|
43
|
+
- **Plex playlist sync** — when Plex credentials are configured, downloaded
|
|
44
|
+
audio is added to `yt-dlp Downloads` by default.
|
|
45
|
+
|
|
46
|
+
## Tools
|
|
47
|
+
|
|
48
|
+
| Tool | Purpose |
|
|
49
|
+
| --- | --- |
|
|
50
|
+
| `youtube_search` | Search YouTube with yt-dlp and return result URLs without downloading. |
|
|
51
|
+
| `youtube_search_ui` | Open an interactive YouTube search UI in MCP App-capable hosts. |
|
|
52
|
+
| `youtube_download` | Download one or more URLs (audio/video/both) and rsync/scp them to a remote dir. |
|
|
53
|
+
| `youtube_probe` | Read-only: resolve title/duration/uploader/format counts without downloading. |
|
|
54
|
+
| `youtube_identify` | Fingerprint local audio with `fpcalc`, return AcoustID/MusicBrainz candidates, preview canonical tags, and optionally write high-confidence tags. |
|
|
55
|
+
| `youtube_stats` | Summarize the download ledger: totals, file kinds, uploaders, and recent entries. |
|
|
56
|
+
|
|
57
|
+
### `youtube_download` parameters
|
|
58
|
+
|
|
59
|
+
| Param | Default | Meaning |
|
|
60
|
+
| --- | --- | --- |
|
|
61
|
+
| `urls` | — (required) | One URL string or an array of URLs. |
|
|
62
|
+
| `mode` | `audio` | `audio`, `video`, or `both`. |
|
|
63
|
+
| `audio_format` | env `YTDLP_AUDIO_FORMAT` → `mp3` | `mp3`/`m4a`/`opus`/`flac`/`wav`/`best`. |
|
|
64
|
+
| `audio_quality` | `0` | yt-dlp quality for lossy codecs: `0`–`9` or a bitrate like `192K`. |
|
|
65
|
+
| `max_height` | best | Cap video resolution (e.g. `1080`). |
|
|
66
|
+
| `container` | `mp4` | `mp4` or `mkv` for video. |
|
|
67
|
+
| `remote` | env `YTDLP_REMOTE` | SSH alias or `user@host`. |
|
|
68
|
+
| `dest_path` | env `YTDLP_REMOTE_PATH` | Absolute remote dir for audio. |
|
|
69
|
+
| `video_dest_path` | env `YTDLP_VIDEO_REMOTE_PATH` → `dest_path` | Absolute remote dir for video. |
|
|
70
|
+
| `keep_local` | `false` | Keep the local staging copy after transfer. |
|
|
71
|
+
| `use_archive` | `false` | Record + skip already-downloaded IDs (per mode). |
|
|
72
|
+
| `plex_playlist` | env `YTDLP_PLEX_PLAYLIST` → `yt-dlp Downloads` when Plex is configured | Plex playlist title or ID to add downloaded audio tracks to. Requires `YTDLP_PLEX_URL` and `YTDLP_PLEX_TOKEN`. |
|
|
73
|
+
| `response_format` | `markdown` | `markdown` or `json`. |
|
|
74
|
+
|
|
75
|
+
When Plex credentials are configured, successful downloads that produced audio
|
|
76
|
+
files search Plex for each downloaded track, create the target playlist if
|
|
77
|
+
needed, and add missing tracks while skipping entries already present. The
|
|
78
|
+
default playlist is `yt-dlp Downloads`; set `YTDLP_PLEX_PLAYLIST` or pass
|
|
79
|
+
`plex_playlist` to override it. Plex errors are reported as
|
|
80
|
+
`plex_playlist_error` and do not make the completed download fail. JSON
|
|
81
|
+
responses include a `plex_playlist` summary with `matched`, `added`,
|
|
82
|
+
`already_present`, and `missing` counts.
|
|
83
|
+
|
|
84
|
+
Canonical metadata matching through MusicBrainz/AcoustID is documented in
|
|
85
|
+
`docs/musicbrainz-acoustid.md`. `youtube_download` automatically runs
|
|
86
|
+
high-confidence MusicBrainz retagging for downloaded audio when
|
|
87
|
+
`YTDLP_ACOUSTID_CLIENT_KEY` is configured; `youtube_identify` remains available
|
|
88
|
+
for previewing or repairing existing library files, with manual tag writes
|
|
89
|
+
enabled by `write_tags=true`.
|
|
90
|
+
|
|
91
|
+
#### `youtube_download` JSON response
|
|
92
|
+
|
|
93
|
+
With `response_format=json`, the call returns a single object describing the
|
|
94
|
+
batch:
|
|
95
|
+
|
|
96
|
+
| Field | Meaning |
|
|
97
|
+
| --- | --- |
|
|
98
|
+
| `transferred` | `true` if every produced subtree reached the remote. |
|
|
99
|
+
| `transfer_error` | `null` on success, else the failure/timeout message (string). |
|
|
100
|
+
| `remote` / `dest_path` / `destination` / `destinations` | The remote and the per-kind destination(s) actually used. |
|
|
101
|
+
| `staging_kept_at` | Local staging path retained for retry (set when the transfer failed or `keep_local` was requested). |
|
|
102
|
+
| `total_files` / `total_bytes` / `total_size` | Aggregate counts across all items. |
|
|
103
|
+
| `partial_items` | Count of items that errored **but** still produced files. |
|
|
104
|
+
| `failed_items` | Count of items that errored **and** produced no files. |
|
|
105
|
+
| `items[]` | Per-URL results, each with a `status`, `title`, `video_id`, `error`, and a `files[]` list. |
|
|
106
|
+
|
|
107
|
+
Each `items[].status` is one of:
|
|
108
|
+
|
|
109
|
+
- `ok` — succeeded with files.
|
|
110
|
+
- `partial` — an error occurred but some files were still produced.
|
|
111
|
+
- `failed` — errored with no files.
|
|
112
|
+
- `skipped` — nothing new (already in the archive).
|
|
8
113
|
|
|
9
|
-
|
|
114
|
+
Optional keys are attached only when the relevant stage ran:
|
|
115
|
+
|
|
116
|
+
- `metadata_retag` — MusicBrainz/AcoustID auto-retag summary (`attempted`,
|
|
117
|
+
`matched`, `written`, `skipped`, `errors`, or an `error` string); present when
|
|
118
|
+
`YTDLP_ACOUSTID_CLIENT_KEY` is configured.
|
|
119
|
+
- `plex_playlist` — Plex playlist summary (`playlist`, `matched`, `added`,
|
|
120
|
+
`already_present`, `missing`); `plex_playlist_error` is set instead if the
|
|
121
|
+
Plex update failed (a Plex failure does not fail the download).
|
|
122
|
+
- `history_error` — set when the download succeeded but the JSONL ledger append
|
|
123
|
+
failed.
|
|
124
|
+
|
|
125
|
+
`youtube_probe` takes `urls` and `response_format`.
|
|
126
|
+
|
|
127
|
+
### `youtube_identify` parameters
|
|
128
|
+
|
|
129
|
+
| Param | Default | Meaning |
|
|
130
|
+
| --- | --- | --- |
|
|
131
|
+
| `paths` | — (required) | One local audio file path string or an array of paths. |
|
|
132
|
+
| `write_tags` | `false` | Write high-confidence MusicBrainz tag previews back to the audio files. |
|
|
133
|
+
| `response_format` | `markdown` | `markdown` or `json`. |
|
|
134
|
+
|
|
135
|
+
`youtube_identify` runs Chromaprint `fpcalc`, sends the fingerprint to AcoustID,
|
|
136
|
+
and returns MusicBrainz recording candidates. When the best candidate is
|
|
137
|
+
high-confidence, it also fetches the MusicBrainz recording/release data and
|
|
138
|
+
includes a `retag_preview` showing the canonical artist, title, release, release
|
|
139
|
+
date, release type, track number, and MusicBrainz IDs. By default it is
|
|
140
|
+
preview-only. With `write_tags=true`, it writes the preview to the file with
|
|
141
|
+
Lofty, including common title/artist/album/date/track fields plus MusicBrainz
|
|
142
|
+
recording, release, release-group, and release-type tags. It requires
|
|
143
|
+
`YTDLP_ACOUSTID_CLIENT_KEY`; set `FPCALC_PATH` if `fpcalc` is not on `PATH`.
|
|
144
|
+
|
|
145
|
+
### `youtube_search` parameters
|
|
146
|
+
|
|
147
|
+
| Param | Default | Meaning |
|
|
148
|
+
| --- | --- | --- |
|
|
149
|
+
| `query` | - (required) | YouTube search text. The server passes this to yt-dlp as `ytsearchN:<query>`. |
|
|
150
|
+
| `limit` | `10` | Number of results, clamped to `1..=25`. |
|
|
151
|
+
| `response_format` | `markdown` | `markdown` or `json`. |
|
|
152
|
+
|
|
153
|
+
`youtube_search_ui` accepts the same input and returns the same search payload,
|
|
154
|
+
plus MCP App metadata for hosts that can render the embedded UI.
|
|
155
|
+
|
|
156
|
+
### `youtube_stats` parameters
|
|
157
|
+
|
|
158
|
+
| Param | Default | Meaning |
|
|
159
|
+
| --- | --- | --- |
|
|
160
|
+
| `limit` | `10` | Number of recent ledger entries to include, clamped to `0..=100`. |
|
|
161
|
+
| `response_format` | `markdown` | `markdown` or `json`. |
|
|
162
|
+
|
|
163
|
+
JSON stats include `total_downloads`, `total_files`, `total_bytes`,
|
|
164
|
+
`skipped_entries`, `by_kind`, `by_uploader`, and `recent`. Bucket fields include
|
|
165
|
+
`downloads` (compatibility alias for call count), `calls`, `items`, `files`,
|
|
166
|
+
`bytes`, and human-readable `size`. Malformed ledger lines are skipped and
|
|
167
|
+
counted instead of failing the whole stats call. If a download succeeds but the
|
|
168
|
+
ledger append fails, the download response still succeeds and includes
|
|
169
|
+
`history_error` in JSON output.
|
|
170
|
+
|
|
171
|
+
## Install
|
|
172
|
+
|
|
173
|
+
Run the guided installer through npm:
|
|
10
174
|
|
|
11
175
|
```bash
|
|
12
176
|
npx -y ytdl-rmcp setup
|
|
13
177
|
```
|
|
14
178
|
|
|
15
|
-
|
|
179
|
+
Or install the command globally:
|
|
16
180
|
|
|
17
181
|
```bash
|
|
18
182
|
npm i -g ytdl-rmcp
|
|
19
|
-
ytdl-rmcp --version
|
|
20
183
|
ytdl-rmcp setup
|
|
21
184
|
```
|
|
22
185
|
|
|
23
|
-
The package downloads the matching GitHub Release binary during
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
186
|
+
The npm package downloads the matching GitHub Release binary during
|
|
187
|
+
`postinstall`; the installed command is the Rust binary served through a tiny
|
|
188
|
+
Node launcher. You can also use the one-line installer:
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
curl -fsSL https://raw.githubusercontent.com/jmagar/ytdl-rmcp/main/scripts/install.sh | bash
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Or download the binary tarball for your platform from
|
|
195
|
+
[Releases](https://github.com/jmagar/ytdl-rmcp/releases), or build it (see below).
|
|
196
|
+
The guided setup fetches yt-dlp + ffmpeg, prompts for your SSH remote and
|
|
197
|
+
audio/video destinations, detects which agent CLIs are present, and registers
|
|
198
|
+
the server into the ones you pick.
|
|
199
|
+
|
|
200
|
+
### Manual registration
|
|
201
|
+
|
|
202
|
+
Run without subcommands, `npx -y ytdl-rmcp` serves MCP over stdio. Register it
|
|
203
|
+
yourself:
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
# Claude Code
|
|
207
|
+
claude mcp add -s user ytdl-rmcp -e YTDLP_REMOTE=tootie -e YTDLP_REMOTE_PATH=/media/music -- npx -y ytdl-rmcp
|
|
208
|
+
# Codex
|
|
209
|
+
codex mcp add --env YTDLP_REMOTE=tootie --env YTDLP_REMOTE_PATH=/media/music ytdl-rmcp -- npx -y ytdl-rmcp
|
|
210
|
+
# Gemini CLI (command is positional, env last)
|
|
211
|
+
gemini mcp add -s user ytdl-rmcp npx -y ytdl-rmcp -e YTDLP_REMOTE=tootie -e YTDLP_REMOTE_PATH=/media/music
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
If you already installed a standalone binary with `npm i -g ytdl-rmcp`,
|
|
215
|
+
`scripts/install.sh`, or a release tarball, you can use that binary path in
|
|
216
|
+
place of `npx -y ytdl-rmcp`.
|
|
217
|
+
|
|
218
|
+
### Distributed forms
|
|
219
|
+
|
|
220
|
+
- **npm launcher** — `npx -y ytdl-rmcp` downloads and runs the matching
|
|
221
|
+
GitHub Release binary. Run without subcommands, it serves MCP over stdio;
|
|
222
|
+
`npx -y ytdl-rmcp setup` runs the guided installer. Stable releases publish
|
|
223
|
+
the package from GitHub Actions with npm provenance.
|
|
224
|
+
- **Claude Code plugin** — `.claude-plugin/plugin.json` prompts for config via
|
|
225
|
+
`userConfig`; `.mcp.json` launches `npx -y ytdl-rmcp`, which downloads the
|
|
226
|
+
matching GitHub Release binary through npm.
|
|
227
|
+
- **Gemini CLI extension** — `gemini-extension.json`; install with
|
|
228
|
+
`gemini extensions install https://github.com/jmagar/ytdl-rmcp`. MCP clients
|
|
229
|
+
should prefer the npm launcher command, `npx -y ytdl-rmcp`.
|
|
230
|
+
- **Container image** — `ghcr.io/jmagar/ytdl-rmcp:main` is published on every
|
|
231
|
+
push to `main`, or build locally with `docker build -t ytdl-rmcp:local .`. It
|
|
232
|
+
includes `ffmpeg`, `fpcalc`, `openssh-client`, and `rsync`. See
|
|
233
|
+
[`docs/container.md`](docs/container.md) for MCP and mounted-library examples.
|
|
234
|
+
- **MCP bundle (`.mcpb` / `.dxt`)** — `mcpb/manifest.json` defines a
|
|
235
|
+
`binary`-type bundle for one-click install in MCPB-capable desktop hosts.
|
|
236
|
+
Every main release publishes `ytdl-rmcp.mcpb` plus a legacy `ytdl-rmcp.dxt`
|
|
237
|
+
alias; both contain the same linux + windows binaries. The bundle defaults
|
|
238
|
+
optional config values to empty strings so Claude Desktop can install it
|
|
239
|
+
before you fill in the SSH destination settings. Configure at least the
|
|
240
|
+
remote and audio destination in the extension settings before downloading.
|
|
241
|
+
Build one locally from prebuilt binaries with `scripts/build-mcpb.sh` (needs
|
|
242
|
+
Node for the `@anthropic-ai/mcpb` CLI).
|
|
243
|
+
|
|
244
|
+
## Configuration (environment variables)
|
|
245
|
+
|
|
246
|
+
| Var | Default | Meaning |
|
|
247
|
+
| --- | --- | --- |
|
|
248
|
+
| `YTDLP_REMOTE` | — | SSH remote (alias or `user@host`) for transfers. |
|
|
249
|
+
| `YTDLP_REMOTE_PATH` | — | Absolute remote dir for **audio**. |
|
|
250
|
+
| `YTDLP_VIDEO_REMOTE_PATH` | falls back to audio | Absolute remote dir for **video**. |
|
|
251
|
+
| `YTDLP_AUDIO_FORMAT` | `mp3` | Default audio codec. |
|
|
252
|
+
| `YTDLP_STAGING_DIR` | system temp | Local staging base dir. |
|
|
253
|
+
| `YTDLP_SSH_OPTS` | — | Extra ssh options parsed with shell-word syntax; appended after the forced `BatchMode`/`StrictHostKeyChecking` flags. Example: `-i "~/.ssh/ytdl key" -o ProxyJump=media-bastion`. Malformed quoting is rejected. |
|
|
254
|
+
| `YTDLP_ARCHIVE_DIR` | per-user state dir | Where `use_archive` history lives. |
|
|
255
|
+
| `YTDLP_HISTORY_PATH` | per-user state dir `downloads.jsonl` | JSONL download ledger used by `youtube_stats`. |
|
|
256
|
+
| `YTDLP_PLEX_URL` | — | Plex server URL, e.g. `http://plex.local:32400`, used when adding audio downloads to a Plex playlist. |
|
|
257
|
+
| `YTDLP_PLEX_TOKEN` | — | Plex auth token for playlist/search API calls. |
|
|
258
|
+
| `YTDLP_PLEX_PLAYLIST` | `yt-dlp Downloads` when Plex URL/token are set | Default Plex playlist title or ID for `youtube_download`; can be overridden with the `plex_playlist` parameter. |
|
|
259
|
+
| `YTDLP_CLEAN_METADATA` | `1` | Strip common YouTube title noise before embedding metadata. Set to `0` to preserve source titles exactly. |
|
|
260
|
+
| `YTDLP_ACOUSTID_CLIENT_KEY` | — | AcoustID application API key required by `youtube_identify`; when set, `youtube_download` automatically writes high-confidence MusicBrainz tags to downloaded audio before transfer. |
|
|
261
|
+
| `FPCALC_PATH` | `fpcalc` on `PATH` | Optional explicit path to the Chromaprint `fpcalc` executable. |
|
|
262
|
+
| `YTDLP_MUSICBRAINZ_CONTACT` | GitHub repo URL | Contact URL/email included in lookup User-Agent strings. |
|
|
263
|
+
| `YTDLP_AUTO_UPDATE` | `1` | Re-download yt-dlp when stale. |
|
|
264
|
+
| `YTDLP_MAX_AGE_DAYS` | `14` | Staleness threshold (days). |
|
|
265
|
+
| `YTDLP_UPDATE_PRE` | `0` | Track yt-dlp's nightly channel. |
|
|
266
|
+
| `YTDLP_EXTRACTOR_ARGS` | — | Passed to yt-dlp `--extractor-args`, e.g. `youtube:player_client=android` for videos the default clients can't reach. |
|
|
267
|
+
| `YTDLP_TIMEOUT_SECS` | `1800` | Timeout for each yt-dlp probe/download command. |
|
|
268
|
+
| `YTDLP_TRANSFER_TIMEOUT_SECS` | `600` | Timeout for each transfer phase. |
|
|
269
|
+
| `YTDLP_SHA256` / `FFMPEG_SHA256` | — | Optional SHA-256 digest required for the resolved yt-dlp / ffmpeg executable. |
|
|
270
|
+
| `YTDLP_PATH` / `FFMPEG_PATH` | — | Use a specific yt-dlp / ffmpeg instead of auto-download. |
|
|
271
|
+
| `YTDLP_LOG` | `info` | `tracing` filter (stderr only). |
|
|
272
|
+
|
|
273
|
+
> **Maintainers:** this table is maintained **by hand**. `scripts/check-packaging.sh`
|
|
274
|
+
> only cross-checks the four machine-readable config surfaces (the Claude plugin
|
|
275
|
+
> `.mcp.json` / `userConfig`, `gemini-extension.json`, and `mcpb/manifest.json`);
|
|
276
|
+
> it does **not** read this README. When you add, rename, or remove a `YTDLP_*`
|
|
277
|
+
> env var, update this table manually — nothing in CI will catch the drift.
|
|
278
|
+
|
|
279
|
+
### Bootstrap trust model
|
|
280
|
+
|
|
281
|
+
By default, first run resolves tools in this order: explicit env override,
|
|
282
|
+
`PATH`, cache, then HTTPS download from the upstream release source. Set
|
|
283
|
+
`YTDLP_SHA256` and/or `FFMPEG_SHA256` to require an exact executable digest
|
|
284
|
+
after resolution or download. These pins verify bytes on disk, but they do not
|
|
285
|
+
fetch upstream signatures or automatically discover trusted digests; operators
|
|
286
|
+
who need a fully pinned supply chain should provide known-good binaries through
|
|
287
|
+
`YTDLP_PATH` / `FFMPEG_PATH` plus matching SHA-256 pins, or disable yt-dlp
|
|
288
|
+
auto-update.
|
|
289
|
+
|
|
290
|
+
For stricter bootstrap control, combine `YTDLP_PATH` / `FFMPEG_PATH` with
|
|
291
|
+
matching `YTDLP_SHA256` / `FFMPEG_SHA256` pins. Hash pins verify the resolved
|
|
292
|
+
executable bytes; they are not upstream signature verification.
|
|
293
|
+
|
|
294
|
+
### Security posture
|
|
295
|
+
|
|
296
|
+
This server is designed to run with **trusted callers and operator-supplied
|
|
297
|
+
config** — it is not a hardened multi-tenant boundary.
|
|
298
|
+
|
|
299
|
+
- **Tool-call URLs reach yt-dlp.** Whatever `urls` an MCP caller passes are
|
|
300
|
+
handed to yt-dlp, a powerful extraction tool. Only point callers at it that
|
|
301
|
+
you trust. Tool-call URLs are validated as `http`/`https` before they reach
|
|
302
|
+
yt-dlp, and every positional is passed after a `--` end-of-options separator so
|
|
303
|
+
a `-`-prefixed value can't be parsed as a flag; the trust assumption above
|
|
304
|
+
still holds regardless.
|
|
305
|
+
- **SSH is key-only and non-interactive.** Transfers force `BatchMode=yes` and
|
|
306
|
+
`StrictHostKeyChecking=accept-new`, so a TTY-less server fails fast instead of
|
|
307
|
+
prompting; there is no password auth. Auth comes from your SSH key/agent and
|
|
308
|
+
any options you add via `YTDLP_SSH_OPTS`.
|
|
309
|
+
- **Remote specs and paths are validated.** The `RemoteSpec` and `RemotePath`
|
|
310
|
+
newtypes reject empty values, option-like (`-`-leading) remotes, and
|
|
311
|
+
whitespace/control characters before any value reaches `ssh`/`rsync`/`scp`,
|
|
312
|
+
and remote paths are single-quote-escaped for the remote shell.
|
|
313
|
+
|
|
314
|
+
## Requirements
|
|
315
|
+
|
|
316
|
+
- **ssh** (and optionally **rsync** — falls back to **scp**, e.g. on Windows).
|
|
317
|
+
- Passwordless key-based SSH auth to the remote.
|
|
318
|
+
- yt-dlp and ffmpeg are fetched automatically (override with `YTDLP_PATH` /
|
|
319
|
+
`FFMPEG_PATH`, or just have them on `PATH`).
|
|
320
|
+
- `youtube_identify` additionally needs `fpcalc`; the container image includes
|
|
321
|
+
it via `libchromaprint-tools`.
|
|
322
|
+
|
|
323
|
+
## Build from source
|
|
324
|
+
|
|
325
|
+
```bash
|
|
326
|
+
cargo build --release # Linux/macOS
|
|
327
|
+
cargo test && cargo clippy --all-targets -- -D warnings # checks
|
|
328
|
+
|
|
329
|
+
# Cross-compile to Windows from Linux (needs nasm + the LLVM toolchain):
|
|
330
|
+
sudo apt-get install -y nasm llvm clang lld
|
|
331
|
+
cargo install cargo-xwin
|
|
332
|
+
cargo xwin build --release --target x86_64-pc-windows-msvc
|
|
42
333
|
```
|
|
43
334
|
|
|
44
|
-
|
|
335
|
+
On dookie/local shells, `~/.local/bin/cargo` is a wrapper that can break
|
|
336
|
+
`cargo xwin`; use the real rustup cargo for local Windows rehearsals:
|
|
45
337
|
|
|
46
338
|
```bash
|
|
47
|
-
|
|
48
|
-
YTDL_RMCP_RELEASE_BASE_URL=https://github.com/jmagar/ytdl-rmcp/releases/download npm i -g ytdl-rmcp
|
|
49
|
-
YTDL_RMCP_SKIP_DOWNLOAD=1 npm i -g ytdl-rmcp
|
|
339
|
+
~/.cargo/bin/cargo xwin build --release --target x86_64-pc-windows-msvc
|
|
50
340
|
```
|
|
51
341
|
|
|
52
|
-
|
|
53
|
-
GitHub Release
|
|
342
|
+
CI (`.github/workflows/`) runs fmt + clippy + tests and a Windows cross-build on
|
|
343
|
+
every push/PR, and publishes both binaries to a GitHub Release on `v*` tags.
|
|
344
|
+
|
|
345
|
+
This crate intentionally remains on Rust edition 2021 for the distributable
|
|
346
|
+
single-binary/plugin build. Move to edition 2024 only after proving Linux,
|
|
347
|
+
Windows MSVC cross-build, and plugin startup compatibility together.
|
|
348
|
+
|
|
349
|
+
## How it works
|
|
350
|
+
|
|
351
|
+
Bare invocation serves MCP over stdio; `setup` runs the installer. A
|
|
352
|
+
`youtube_download` call:
|
|
353
|
+
|
|
354
|
+
1. Resolves yt-dlp + ffmpeg (env override → PATH → cache → download) and
|
|
355
|
+
verifies SHA-256 pins when configured.
|
|
356
|
+
2. Cleans mix/radio URLs, then runs yt-dlp per mode into a staging tree
|
|
357
|
+
(`staging/audio`, `staging/video`) with metadata/thumbnail/archive flags,
|
|
358
|
+
source metadata sidecars, and the `Artist/Title [id]` output template.
|
|
359
|
+
3. *(optional)* When `YTDLP_ACOUSTID_CLIENT_KEY` is set, fingerprints the
|
|
360
|
+
downloaded audio and writes high-confidence MusicBrainz/AcoustID tags
|
|
361
|
+
in-place — before transfer, so the remote copy carries the canonical tags.
|
|
362
|
+
4. Transfers each kind's subtree to its own remote dir (rsync, else scp).
|
|
363
|
+
5. *(optional)* When Plex credentials are configured and the transfer
|
|
364
|
+
succeeded, adds the downloaded audio tracks to the target Plex playlist.
|
|
365
|
+
6. Appends the completed call to the JSONL download ledger.
|
|
366
|
+
7. Returns a markdown or JSON summary listing files, sizes, and the actual
|
|
367
|
+
destination(s).
|
|
368
|
+
|
|
369
|
+
See `CLAUDE.md` for architecture, conventions, and gotchas.
|
|
370
|
+
|
|
371
|
+
## License
|
|
372
|
+
|
|
373
|
+
MIT — see `LICENSE`.
|
|
374
|
+
|
|
375
|
+
## Rust MCP naming pattern
|
|
376
|
+
|
|
377
|
+
This repo follows the Rust MCP server naming convention:
|
|
378
|
+
|
|
379
|
+
- Repo: `ytdl-rmcp`
|
|
380
|
+
- CLI alias: `rytdl`
|
|
381
|
+
- npm package: `ytdl-rmcp`
|
|
382
|
+
|