yt
by ArasanDev
README.md
# yt
[](https://github.com/ArasanDev/yt/actions/workflows/ci.yml)
[](https://github.com/ArasanDev/yt/actions/workflows/release.yml)
[](LICENSE)
YouTube transcripts for agents and humans. One static Go binary, no API key,
no AI dependencies, no runtime requirements.
- Fetches a video's own caption tracks (manual or auto-generated) via YouTube's
internal **Innertube API**, using `ANDROID`/`IOS` client contexts — the
WEB-client caption URLs have been pot-token gated since mid-2025 and return
empty bodies.
- Cascades `innertube-android → innertube-ios → watch-page` so a single
throttled endpoint doesn't take the fetch down; every response reports which
method won (`source`).
- Channel monitoring: `yt channel @handle` (or `list_channel_videos`) resolves a
handle and lists the latest uploads with **exact** publish timestamps from the
channel RSS feed, so an agent can diff against a watermark and see what is new.
- Same binary is the MCP server: `yt mcp`, or bare `yt` when stdin is piped
(the shape MCP clients spawn). Bare `yt` at a terminal prints help.
- MCP responses are designed for agent context budgets: bounded payloads,
structural truncation with **exact** resume pointers, time/cue-addressed
ranges, server-side search, typed errors, and session-level caching so one
`get_video_info` + `get_transcript` + `search_transcript` share a single
player fetch.
Dependencies: `cobra` (CLI), `modelcontextprotocol/go-sdk` (official MCP SDK),
and `golang.org/x/term` (TTY detection). The YouTube fetch layer is stdlib only
— `net/http`, `encoding/json`, `encoding/xml`.
## Layout
The repository root is the Go module (`github.com/ArasanDev/yt`) and the
product root.
```
cmd/yt/ binary entry point
internal/cli/ cobra commands and flags
internal/mcp/ MCP tool schemas, handlers, safety warnings
internal/youtube/ Innertube client, captions, channels, cache, typed failures
internal/search/ pure transcript search
internal/render/ block/cue rendering, SRT, truncation metadata
internal/transcript/ shared Segment/Transcript model and timestamp helpers
internal/apperr/ typed error contract (code, retryable, suggestion)
internal/version/ build version, commit, and date
```
CI lives in `.github/workflows/ci.yml` and runs `gofmt`, `go vet`,
`go test -race`, a native build, and a cross-build matrix for the release
targets.
Dependencies point inward: `cli` and `mcp` depend on `youtube`, `search`,
`render`, and `transcript`; `youtube` depends on `apperr` and `transcript`;
`search` and `render` depend only on `transcript`. The acquisition layer does
not import the adapters.
## Install
### Homebrew (macOS/Linux)
```sh
brew trust arasandev/tap # Homebrew 7+ trusts third-party taps explicitly
brew install --cask arasandev/tap/yt
```
### Install script (macOS/Linux)
```sh
curl -fsSL https://raw.githubusercontent.com/ArasanDev/yt/main/install.sh | sh
```
The script downloads the latest release for your OS/architecture, verifies it
against `checksums.txt`, and installs to `/usr/local/bin` when writable,
otherwise `~/.local/bin`. Override the target with `YT_INSTALL_DIR` and pin a
version with `YT_VERSION`:
```sh
YT_VERSION=v0.1.0 YT_INSTALL_DIR="$HOME/bin" \
curl -fsSL https://raw.githubusercontent.com/ArasanDev/yt/main/install.sh | sh
```
### Go developers
```sh
go install github.com/ArasanDev/yt/cmd/yt@latest
```
`go install` records the module version, so `yt --version` reports the release
even though ldflags cannot be injected. Use `make install` for a local build
that also stamps commit and date.
### Docker
```sh
docker run --rm -i ghcr.io/arasandev/yt:latest mcp
# CLI use:
docker run --rm ghcr.io/arasandev/yt:latest transcript dQw4w9WgXcQ
```
The image is a distroless static build. With no TTY, bare `yt` already serves
MCP, so the default container command is `mcp`.
### Linux packages
Release assets include `.deb`, `.rpm`, and `.apk` packages for `amd64` and
`arm64`:
```sh
sudo dpkg -i yt_<version>_linux_amd64.deb
sudo rpm -i yt_<version>_linux_amd64.rpm
sudo apk add --allow-untrusted yt_<version>_linux_amd64.apk
```
### Prebuilt binaries
Every release publishes archives, Linux packages, and a `checksums.txt`:
<https://github.com/ArasanDev/yt/releases>
Targets: `darwin`/`linux`/`windows` on `amd64` and `arm64`; Linux also ships
`deb`/`rpm`/`apk` packages.
### From source
```sh
git clone https://github.com/ArasanDev/yt.git
cd yt
make build # ./bin/yt
make install # into GOBIN with version/commit/date stamped
```
Release builds stamp a version, commit, and date (the default is `dev`):
```sh
go build -trimpath -ldflags "\
-X github.com/ArasanDev/yt/internal/version.Version=v0.1.0 \
-X github.com/ArasanDev/yt/internal/version.Commit=$(git rev-parse --short HEAD) \
-X github.com/ArasanDev/yt/internal/version.Date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
-o bin/yt ./cmd/yt
yt --version
```
## CLI
```sh
yt transcript <url-or-id> # transcript to stdout
yt transcript <url> -l en,es -t # language fallback + [MM:SS] stamps
yt transcript <url> --start 1274 --end 1290 # time range
yt transcript <url> -f srt -o subs.srt # SRT file
yt transcript <url> -f json --max-chars 2000 # cue JSON, bounded
yt transcript <url> --bare # body only, no metadata header
yt transcript <url> --start-cue 120 --max-cues 40 # resumable cue window
yt search <url> "harness engineering" # timestamped hits, no full read
yt search <url> "M C P" --match-mode any # spelled acronyms collapse to mcp
yt search <url> "state of the art" --match-mode phrase
yt search <url> "harness" --context-cues 2 # human output shows context
yt tracks <url> # list caption languages
yt info <url> # title, channel, duration, views, chapters
yt channel @aiDotEngineer --limit 10 # latest uploads (exact timestamps)
yt channel @aiDotEngineer --since 2026-10-01 --json
yt doctor # probe which acquisition clients work
yt --version # build version (or module version under go install)
```
Accepts `watch?v=`, `youtu.be/`, `/shorts/`, `/live/`, `/embed/` URLs or a bare
11-char video ID. By default, `-f text` includes a metadata header and, when
needed, a truncation trailer. Use `--bare` for body-only stdout; truncation is
then reported on stderr. Fetch/renderer notes always go to stderr.
CLI usage errors and upstream failures are typed. `cmd/yt` prints `code`,
`retryable`, `retry_after_seconds`, and `suggestion` to stderr while keeping
stdout clean. Invalid formats, negative limits, and inverted time ranges fail
before any network request.
## MCP
`yt mcp` serves stdio JSON-RPC; so does bare `yt` when stdin is not a terminal
(how MCP clients spawn a server), while bare `yt` at a prompt prints help. Call
`get_video_info` first: it reports caption availability, live status, and an
estimated transcript size, so you can decide between reading the whole
transcript, searching it, or reading a range.
| Tool | Purpose |
|---|---|
| `list_channel_videos` | Latest uploads for a channel (@handle/URL/UC…), from RSS: exact publish times, titles, links, description excerpts. |
| `get_video_info` | Metadata + caption summary + transcript size estimate + chapters. No transcript download. |
| `get_transcript` | Transcript text or cue JSON, bounded and addressable. |
| `search_transcript` | Find a topic with timestamps and context; `match_mode` all/any/phrase; zero results explain themselves. |
| `list_tracks` | Caption tracks. **Deprecated** — use `get_video_info.captions`. |
For monitoring, call `list_channel_videos` and pass the previous response's
`newest_published` as `since`. Filtering is strictly `published > since`, so no
video is reported twice. The feed exposes at most 15 entries and has no duration
or view counts; those need `get_video_info` on a specific video.
Key `get_transcript` parameters: `languages` (CSV, default `en`; `original` for
the uploader's own language), `allow_translation`, `format` (`text`|`json`),
`granularity` (`block` default |`cue`), `timestamp_mode` (`block` default
|`cue`|`none`), `start_seconds`/`end_seconds`, `start_cue`/`max_cues`,
`max_chars` (default 50000; bounds the transcript body only).
Truncation is never silent: the header carries `TRUNCATED next_start_seconds=…
next_cue_index=…`, structured metadata carries the same fields, and `next_*`
values are exact (not rounded), so resuming at them never drops the boundary
cue. `max_cues` clipping is reported even when `end_seconds` is also set.
Errors come back as `isError` results with a machine-readable `error.code`
(`bot_check`, `rate_limited`, `no_captions`, `bad_request`, …) plus
`retryable` and a `suggestion`.
The transcript body is returned in both `content` and `structuredContent` for
client compatibility, so a response is roughly twice the body size;
`returned_chars` measures one copy.
Auto-dubbed videos list one auto-generated track per language. A track whose
language differs from the uploader's known original is reported as
`translated=true`, whether it is manual or ASR, so machine-translated speech is
never presented as the speaker's words. `get_video_info.captions.original_language`
names that track. If the player response carries no audio-track metadata and
multiple caption languages exist, the original is unknown: `languages=original`
returns an actionable `no_matching_language` error instead of guessing, and
`original_language` may be empty. Request a specific language in that case.
Wire into any agent:
```sh
devin mcp add yt -- yt mcp # Devin CLI
claude mcp add yt -- yt mcp # Claude Code
```
or raw `mcpServers` JSON (Devin, Claude Desktop, Cursor, Windsurf, …):
```json
{"mcpServers": {"yt": {"command": "yt", "args": ["mcp"]}}}
```
## Releasing and distribution
A `v*` tag triggers `.github/workflows/release.yml`, which:
1. runs the test suite and GoReleaser (archives, Linux packages, checksums,
GitHub Release);
2. builds and pushes a multi-arch `ghcr.io/arasandev/yt` image;
3. stamps the version into `server.json` and publishes to the MCP registry.
```sh
git tag v0.1.0
git push origin v0.1.0
```
Homebrew cask updates are skipped unless the repository has a
`HOMEBREW_TAP_GITHUB_TOKEN` secret: a fine-grained PAT with `contents: write`
on `ArasanDev/homebrew-tap`. Without it, the release still ships binaries,
Docker, and MCP metadata; add the cask manually or add the secret and re-run.
The MCP registry job is best-effort (`continue-on-error`). GHCR packages are
private by default: set the `yt` package to public once in the package settings,
then re-run the job. The registry checks the image annotation
`io.modelcontextprotocol.server.name` against `server.json`'s `name`, so those
two strings must stay in sync.
Useful local checks before tagging:
```sh
goreleaser check
goreleaser release --snapshot --clean # artifacts in dist/, never published
docker build --build-arg VERSION=dev -t yt:dev .
```
## Development
```sh
make build # ./bin/yt
make test # go test -race ./...
make check # gofmt + go vet + go test -race
make snapshot # local GoReleaser snapshot
make clean
```
See `AGENTS.md` for the architecture, contracts, and conventions agents must
preserve. In short: stdout is data, stderr is diagnostics; errors are typed;
truncation is never silent and resume pointers are exact; provenance and
`content_trust` labels are not optional; tests never hit the network.
## Caveats
- **IP blocking**: datacenter IPs (AWS/GCP/VPS) get blocked or rate-limited by
YouTube, surfacing as `bot_check` or `rate_limited`. From a residential IP —
your Mac — it just works. For a server, cross-compile
(`GOOS=linux GOARCH=arm64 go build`) and expect to need a residential proxy
eventually.
- Auto-generated (`asr`) captions lack punctuation, so `granularity=sentence`
degrades to `block` with a note. Manual tracks are preferred; YouTube
auto-translation is used when the requested language has no track, and the
result is marked `translated=true`.
- **Multi-dub videos** expose one ASR track per language. `languages=original`
resolves the actual original via YouTube's audio-track→caption mapping; if
that metadata is absent, the original is unknown and the tool fails with an
actionable error rather than guessing.
- Age-restricted / private videos need auth — not supported.
- `search_transcript` matches multi-word queries within one cue or across up to
3 adjacent cues. `match_mode=any` relaxes the AND; `match_mode=phrase` requires
adjacency. Contiguous runs of three or more single-letter tokens (`M C P`)
collapse to `mcp`, and terms of 3 or fewer characters match whole words only.
A zero result includes per-term hit counts and a note naming missing terms or
a start-cue boundary, so it is not a silent dead end. Searches cover the
whole transcript; there is no time-range filter yet.
- `chapters` comes from the description's `Chapters:` block and is parsed from
the full description, before the excerpt is truncated. Emoji/markdown
headings and an inline first entry are accepted; invalid, duplicate,
out-of-order, or past-duration entries are dropped. At least two valid,
strictly increasing, in-duration timestamps are required.
- `timestamp_window_seconds` follows the timestamp mode: `30` when timestamps
are block-level, `0` for cue-level or disabled timestamps. Cite a block
timestamp as the start of that window, not as a single line.
- `warnings` is advisory. It flags instruction-shaped text in transcripts,
search matches/context, video metadata, and channel listings so a caller can
quote rather than obey. User-generated content remains
`content_trust: "untrusted"` regardless.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues