{
  "markdown": "<!-- mcp-name: io.github.santiriera626/tubekit-mcp -->\n<!-- ^ Proof of ownership for the MCP Registry, which reads it from the PyPI\n     description (this file). Do not remove, reword or glue it to other text:\n     the token must be followed by a boundary. Guarded by\n     tests/unit/test_registry_metadata.py. -->\n\n# tubekit-mcp\n\nCompliance-aware [Model Context Protocol](https://modelcontextprotocol.io)\nserver for YouTube publishing, analytics, and creator insights — every action\n**gated, metered, and audited** before it touches a channel.\n\n[![CI](https://github.com/santiriera626/tubekit-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/santiriera626/tubekit-mcp/actions/workflows/ci.yml)\n[![License: FSL-1.1-MIT](https://img.shields.io/badge/License-FSL--1.1--MIT-blue.svg)](LICENSE.md)\n[![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](pyproject.toml)\n[![Type-checked: mypy strict](https://img.shields.io/badge/mypy-strict-blue.svg)](https://mypy-lang.org/)\n[![Lint: ruff](https://img.shields.io/badge/lint-ruff-orange.svg)](https://github.com/astral-sh/ruff)\n\n`tubekit-mcp` is an MCP server: you connect it to an MCP client (Claude Desktop,\nClaude Code, …) and the assistant can then publish and manage videos on a\nYouTube channel **on your behalf**, with compliance checks, quota accounting,\nidempotent uploads and a tamper-evident audit log on every state-changing call.\nLicensed under [FSL-1.1-MIT](LICENSE.md) — run and self-host it freely,\ncommercially or not; each release becomes plain MIT two years after publication.\n\n## Architecture\n\nA single tool call (e.g. `upload_video`) flows through a compliance gate, a\nquota / idempotency ledger, an OAuth refresh, the YouTube API, and a\ntamper-evident audit row — with the **pure core kept free of protocol concerns**,\na boundary enforced in CI by an AST fitness test\n(`tests/unit/services/test_no_fastmcp_imports.py`).\n\n```mermaid\nflowchart LR\n    client[\"MCP client<br/>(Claude Desktop / Code)\"]\n    subgraph edge[\"Protocol edge\"]\n      transport[\"Transport<br/>stdio · HTTP + Bearer\"]\n      tool[\"Tool wrapper\"]\n    end\n    subgraph core[\"Pure core — no protocol imports (CI-enforced)\"]\n      compliance{\"Compliance<br/>gate\"}\n      ledger[\"Quota +<br/>idempotency<br/>ledger\"]\n      oauth[\"OAuth refresh<br/>(Fernet at rest)\"]\n    end\n    upstream[\"YouTube<br/>Data / Analytics API\"]\n    audit[(\"Tamper-evident<br/>hash-chained<br/>audit log\")]\n\n    client --> transport --> tool --> compliance\n    compliance -->|pass| ledger --> oauth --> upstream\n    compliance -->|fail| suggestion[\"Suggestion →<br/>agent retries\"]\n    tool -. audited .-> audit\n    ledger -. audited .-> audit\n    oauth -. audited .-> audit\n```\n\n> **Why this exists.** Agents are starting to operate real YouTube channels —\n> and a channel is an asset you can lose. One bad call can publish a video\n> that violates policy, burn the day's API quota (an upload costs 1600 of the\n> default 10,000 daily units), or double-publish on a retry. Generic YouTube\n> wrappers hand the agent the raw API; `tubekit` wraps every call in the\n> guardrails an unattended operator needs: a compliance gate *before* anything\n> goes live, quota accounted before it is spent, idempotent retries, OAuth\n> tokens encrypted at rest (fingerprint-only logging), and a tamper-evident\n> audit chain of everything the agent did. Full design rationale in\n> [ADR 0001](docs/adr/0001-design.md).\n\n## Usage\n\n### Prerequisites\n\nBefore an assistant can act on a channel you need, **once per channel**:\n\n1. A **Google Cloud project** with the *YouTube Data API v3* (and *YouTube\n   Analytics API* if you use `get_analytics`) enabled.\n2. An **OAuth client** of type *Desktop app*, downloaded as `client_secret.json`.\n3. The **channel** whose Google account will grant the consent.\n\nThe OAuth grant is interactive and only happens here — `tubekit` stores the\nresulting refresh token Fernet-encrypted and never prints it. Never used\nGoogle Cloud Console? [`docs/runbooks/gcp-setup.md`](docs/runbooks/gcp-setup.md)\nwalks the whole thing from zero (project, APIs, consent screen, client type,\nand the two Google policies — token expiry in testing, private-locked uploads\nfor unaudited projects — that surprise people later). The consent flows\nthemselves are in\n[`docs/runbooks/oauth-bootstrap.md`](docs/runbooks/oauth-bootstrap.md).\n\n### Install\n\nFrom PyPI:\n\n```bash\nuv tool install tubekit-mcp   # installs the tubekit / tubekit-mcp-* commands\n```\n\nUse `uv tool install`, not `uvx`, for a real setup: the one-time steps below need\nthe `tubekit` CLI to still be there afterwards. `uvx tubekit-mcp` runs the stdio\nserver from a throwaway environment, which is what an MCP client's config points\nat once the setup is done.\n\n**The server will not start yet, and that is deliberate** — it refuses to run\nwithout a master key rather than inventing one, since that key is what encrypts\nyour OAuth tokens at rest. [One-time setup](#one-time-setup) below is the two\ncommands that fix it.\n\nOr from source, for development:\n\n```bash\ngit clone https://github.com/santiriera626/tubekit-mcp.git tubekit-mcp && cd tubekit-mcp\nuv sync --all-extras        # installs the tubekit / tubekit-mcp-* entry points\nmake db-upgrade             # builds the state schema (wraps `tubekit db upgrade`;\n                            #   `tubekit db current` prints the applied revision)\n```\n\n(`uv` itself installs with `curl -LsSf https://astral.sh/uv/install.sh | sh`.)\n\n### Try it in 60 seconds — no Google account needed\n\nThe compliance engine is pure-local, so you can watch the gate and its\nself-correction `Suggestion`s work before touching Google Cloud:\n\n```bash\ncat > video.json <<'EOF'\n{\n  \"title\": \"Test Upload — Compliance Gate Demo\",\n  \"description\": \"Demo description for the compliance gate.\",\n  \"tags\": [\"demo\", \"test\"],\n  \"ai_disclosure\": false,\n  \"made_for_kids\": null,\n  \"category_id\": \"10\"\n}\nEOF\nuv run tubekit compliance test video.json\n```\n\n```text\nCompliance: PASSED  (8 rules evaluated)\n  WARNING AI_DISCLOSURE_MISSING: AI disclosure flag is false; confirm this is the\n      channel owner's intended policy for AI-generated content.\n      suggestion: action=set field=metadata.ai_disclosure value=True\n  WARNING KIDS_FLAG_NOT_SET: made_for_kids is unset; set it explicitly to true or\n      false (this rule does not classify by content).\n      suggestion: action=set field=metadata.made_for_kids value=False\n```\n\nAn error-severity violation — say a 126-character title — fails the gate with\nexit 1 and a machine-applicable fix (`suggestion: action=truncate\nfield=metadata.title value=100`). That is the same `Suggestion` an agent\nreceives from a rejected `upload_video` and applies before retrying (ADR D8).\n`tubekit compliance list` prints the rule catalog; `tubekit compliance\nmanifest` exports it for external auditors.\n\n### Install with an AI agent\n\nRather not type the commands above yourself? Paste the block below into Claude\nCode (or any coding agent with shell access) and it will clone, install and\nregister the server for you. It stops at the boundary above: no Google\naccount, `client_secret.json` or OAuth consent is involved.\n\n```text\nInstall tubekit-mcp and register it as an MCP server. Stop and report if any step fails.\n\n1. Clone the repo and enter it (install uv first if missing:\n   curl -LsSf https://astral.sh/uv/install.sh | sh):\n   git clone https://github.com/santiriera626/tubekit-mcp.git tubekit-mcp && cd tubekit-mcp\n\n2. Install dependencies, apply the database migrations and create the Fernet\n   master key (the server refuses to start without one):\n   uv sync --all-extras\n   make db-upgrade\n   uv run tubekit auth init-master-key\n   (Idempotent: an existing key is reported and left untouched.)\n\n3. Run the zero-credential compliance smoke check to confirm the install works:\n   cat > video.json <<'EOF'\n   {\n     \"title\": \"Test Upload — Compliance Gate Demo\",\n     \"description\": \"Demo description for the compliance gate.\",\n     \"tags\": [\"demo\", \"test\"],\n     \"ai_disclosure\": false,\n     \"made_for_kids\": null,\n     \"category_id\": \"10\"\n   }\n   EOF\n   uv run tubekit compliance test video.json\n   Expect \"Compliance: PASSED (8 rules evaluated)\" and exit code 0.\n\n4. Register the server with Claude Code over stdio, using absolute paths:\n   claude mcp add tubekit \\\n     -e TUBEKIT_STATE_DB_PATH=\"$(pwd)/state.db\" \\\n     -e TUBEKIT_MASTER_KEY_PATH=\"$HOME/.config/tubekit/master.key\" \\\n     -- \"$(pwd)/.venv/bin/tubekit-mcp-stdio\"\n\nDo not run `tubekit auth setup-channel` or open any Google consent screen —\nOAuth channel bootstrap is a separate, human-in-the-loop step documented in\ndocs/runbooks/oauth-bootstrap.md. Stop after step 4 and tell me to run that\nmyself.\n```\n\nRestart your MCP client after registration; it can now call `tubekit.health()`\nand list the full tool catalog. Every tool that takes a `channel` argument —\n`validate_compliance` included — needs a configured channel, which means\ncompleting [One-time setup](#one-time-setup) — including the OAuth consent in\n[`docs/runbooks/oauth-bootstrap.md`](docs/runbooks/oauth-bootstrap.md) —\nyourself. The compliance gate itself is testable without any channel via\nstep 3's `tubekit compliance test`.\n\n### One-time setup\n\n```bash\n# 1. Create the Fernet master key used to encrypt stored tokens (mode 0600).\n#    Skip if the agent install above already created it.\ntubekit auth init-master-key\n\n# 2. Bootstrap a channel — opens a browser for the OAuth consent.\n#    The alias is positional; --channel-id, --gcp-project, --client-secrets\n#    and --scopes are all required (see docs/runbooks/oauth-bootstrap.md).\ntubekit auth setup-channel mychannel \\\n  --channel-id UCxxxxxxxxxxxxxxxxxxxxxx \\\n  --gcp-project my-gcp-project \\\n  --client-secrets ./client_secret.json \\\n  --scopes youtube.upload,youtube.readonly\n\n#    …or, on a headless box (no browser), append --device-code:\ntubekit auth setup-channel mychannel \\\n  --channel-id UCxxxxxxxxxxxxxxxxxxxxxx \\\n  --gcp-project my-gcp-project \\\n  --client-secrets ./client_secret.json \\\n  --scopes youtube.upload,youtube.readonly \\\n  --device-code\n\n# 3. Define the channel registry — setup-channel persists only the encrypted\n#    token; the registry is a TOML you write yourself. The table name is the\n#    alias. Full field reference: docs/runbooks/vps-deploy.md §5.\nmkdir -p ~/.config/tubekit/channels\ncat > ~/.config/tubekit/channels/mychannel.toml <<'EOF'\n[mychannel]\nchannel_id     = \"UCxxxxxxxxxxxxxxxxxxxxxx\"\ngcp_project_id = \"my-gcp-project\"\noauth_scopes   = [\n  \"https://www.googleapis.com/auth/youtube.upload\",\n  \"https://www.googleapis.com/auth/youtube.readonly\",\n]\nEOF\n\n# 4. Verify — `show` prints one channel's status (token fingerprint only),\n#    `list` prints every configured alias\ntubekit channels show mychannel --channels-dir ~/.config/tubekit/channels\ntubekit channels list --channels-dir ~/.config/tubekit/channels\n```\n\n### Connect to an MCP client (stdio)\n\nMost clients launch the server over **stdio**. Add this to the client's MCP\nconfig (for Claude Desktop: `claude_desktop_config.json`):\n\n```json\n{\n  \"mcpServers\": {\n    \"tubekit\": {\n      \"command\": \"tubekit-mcp-stdio\",\n      \"env\": {\n        \"TUBEKIT_STATE_DB_PATH\": \"/absolute/path/to/state.db\",\n        \"TUBEKIT_MASTER_KEY_PATH\": \"/absolute/path/to/master.key\"\n      }\n    }\n  }\n}\n```\n\nRestart the client. The assistant now sees the tools below plus a\n`tubekit.health()` probe. Ask it, e.g., *\"upload ./intro.mp4 to mychannel\"* and\nit will call `upload_video`, which gates on compliance, charges quota, refreshes\nOAuth and writes an audit row — all transparently.\n\n### Available tools\n\n| Tool | What it does | OAuth scope | Quota |\n|------|--------------|-------------|-------|\n| `validate_compliance(channel, metadata, thumbnail_present=False)` | Run the rule registry against metadata; returns a `ComplianceReport`. Pure-local, no YouTube call. | — | 0 |\n| `upload_video(channel, video_path, metadata, idempotency_key, privacy_status=\"private\", notify_subscribers=False)` | Upload a video after an internal compliance gate. Idempotent via `idempotency_key`. | `youtube.upload` | 1600 |\n| `set_thumbnail(channel, video_id, thumbnail_path)` | Set a thumbnail on an existing video. | `youtube` | 50 |\n| `update_metadata(channel, video_id, patch)` | Partially update title/description/tags/privacy. Compliance-gated on the post-patch metadata. | `youtube` | 50 |\n| `get_video_status(channel, video_id)` | Read processing/privacy status of a video. | `youtube.readonly` | 1 |\n| `get_analytics(channel, metrics, start_date, end_date, dimensions=None)` | Channel-level metrics over a window. Uses the **separate** Analytics quota bucket. | `yt-analytics.readonly` | 1 (Analytics) |\n| `get_portfolio_report(channels=None, period=\"28d\", start_date=None, end_date=None)` | One row per channel: period totals + vs-prior deltas (views, revenue), YPP progress for non-monetized channels. Per-channel failures are isolated. | `yt-analytics.readonly` + `youtube.readonly` (+`yt-analytics-monetary.readonly` for revenue) | 3/channel (Analytics) |\n| `get_channel_report(channel, period=\"28d\", start_date=None, end_date=None, granularity=\"auto\")` | Single-channel health report: totals, trend series, top videos, traffic sources, Shorts/long-form split, revenue or YPP progress, lifetime snapshot. | `yt-analytics.readonly` + `youtube.readonly` (+monetary for revenue) | 7 (Analytics) |\n| `get_video_report(channel, video_id, period=\"lifetime\", start_date=None, end_date=None, granularity=\"auto\")` | Per-video deep dive: retention proxy, traffic sources, revenue when monetized; window starts at publication by default. | `yt-analytics.readonly` + `youtube.readonly` (+monetary for revenue) | 5 (Analytics) |\n| `list_my_videos(channel, page_size=50, page_token=None)` | Paginated list of channel uploads. | `youtube.readonly` | 1 |\n| `get_comments_digest(channels=None, force_sync=False)` | Portfolio-wide comment counts (unread/unanswered/held/spam, per video). | `youtube.force-ssl` | 4/channel |\n| `list_comments(channel, video_id=None, only=\"unread\", search=None, order=\"time\", page_size=20, page=1)` | Compact comment listing from the local mirror. | `youtube.force-ssl` | 1 |\n| `get_comment_thread(channel, comment_id)` | One thread in full: untruncated text + all replies. | `youtube.force-ssl` | 2 |\n| `mark_comments_reviewed(channel, ids=None, video_id=None, all_unread=False, state=\"read\")` | Local triage state; never mutates YouTube. | — | 0 |\n| `check_oauth(channel)` | Zero-quota OAuth liveness pre-flight: refresh the stored grant against Google's token endpoint and report `oauth_ok` + expiry. Run before a quota-spending tool. | — | 0 |\n| `request_ingest_upload(channel, filename, size_bytes, sha256)` | Issue a one-time brokered upload URL so a remote client can `PUT` a multi-GB video the server then ingests — no ssh. See [media ingest](#media-ingest-remote-upload_video). | — | 0 |\n| `list_playlists(channel, page_size=50, page_token=None)` | The channel's own playlists: id, title, privacy, item count. | `youtube.readonly` | 1 |\n| `list_playlist_items(channel, playlist_id, page_size=50, page_token=None)` | One playlist's videos in order. Returns each `playlist_item_id` — the handle remove/reorder need. | `youtube.readonly` | 1 |\n| `create_playlist(channel, title, description=\"\", privacy_status=\"private\")` | Create a playlist. **Defaults to private**; going public is an explicit act. Not idempotent. | `youtube` | 50 |\n| `update_playlist(channel, playlist_id, title=None, description=None, privacy_status=None)` | Patch title/description/privacy. Omitted fields are read and merged, never blanked. | `youtube` | 50 |\n| `add_to_playlist(channel, playlist_id, video_id, position=None)` | Add a video (append, or insert at `position`). Not idempotent — YouTube allows duplicates. | `youtube` | 50 |\n| `reorder_playlist_item(channel, playlist_id, playlist_item_id, video_id, position)` | Move an entry to `position` (0 = first). | `youtube` | 50 |\n| `remove_from_playlist(channel, playlist_item_id)` | Remove one entry. The video itself is untouched. | `youtube` | 50 |\n\n> Playlist writes take a `playlist_item_id`, never a bare video id: the same\n> video may appear in a list more than once, so YouTube cannot disambiguate from\n> the video alone. Get the ids from one `list_playlist_items` call (1 unit) and\n> reuse them — the services deliberately do **not** resolve them for you, because\n> that would be a quota charge the caller never asked for.\n>\n> Playlists are **not** run through the compliance registry: it scores video\n> metadata (thumbnail, category, tags, synthetic-media declaration), none of\n> which a playlist has, and its `TITLE_TOO_LONG` would apply the *video* ceiling\n> (100) to a resource whose real ceiling is 150. What is enforced instead is what\n> Google documents for the playlist resource: a non-empty title, a valid\n> `privacyStatus`, **≤150 characters of title, ≤5000 of description, and no\n> `<`, `>` or U+2028** in either. Those limits live in the\n> [YouTube Help page](https://support.google.com/youtube/answer/10232933), not in\n> the Data API reference — which documents none, and is why they were missed at\n> first.\n\n> Comments tools need the channel grant to carry `youtube.force-ssl`\n> (moderation-status filtering is owner-only). Verify with `check_oauth`; if\n> missing, re-consent once:\n> `tubekit auth setup-channel --scopes …,https://www.googleapis.com/auth/youtube.force-ssl`.\n\nRevenue figures across the report tools are **estimated ad + YouTube Premium\nrevenue only** (memberships, Super Chat and Shopping are not exposed by the\nchannel-level APIs), lag ~2 days (`period.revenue_complete_until`), and are\nfinalized around the 10th of the following month. Thumbnail impressions/CTR are\nnot available in the targeted Analytics API. `get_analytics` remains the raw\nescape hatch for any ad-hoc query.\n\nEvery tool returns a `ToolResult` envelope (`ok` + `data`, or a structured\n`ToolError` whose `code` is one of the documented error codes). On a compliance\nfailure the error carries a `Suggestion` the assistant can apply and retry — the\nself-correction loop described in ADR D8.\n\n### HTTP transport (multi-user)\n\nFor remote / multi-tenant use, run the HTTP transport instead. It requires a\nper-client **Bearer API token** scoped to specific channels and tools, with an\noptional per-token rate limit:\n\n```bash\n# Issue a token (the plain value is printed exactly once — store it now)\ntubekit auth issue --name claude-prod --channels mychannel --tools \"*\" \\\n  --rate-limit-per-min 30\n\n# Serve over HTTP\ntubekit-mcp-http --host 0.0.0.0 --port 8080\n```\n\nClients send `Authorization: Bearer <token>`. Requests that are\nunauthenticated, out-of-scope, or rate-limited get JSON-RPC errors\n(`-32001`..`-32006`). `GET /healthz` returns the liveness payload; the\nper-channel detail (aliases, OAuth expiry) is added only for a valid Bearer and\nnarrowed to that token's channels. Manage tokens with\n`tubekit auth list | show | revoke | rotate`.\n\nTo run this centrally on a VPS (one server, many projects connecting over\nHTTPS), follow [`docs/runbooks/vps-deploy.md`](docs/runbooks/vps-deploy.md) —\na self-contained, step-by-step deployment guide (systemd + Traefik + per-project\ntokens) written for an infra team.\n\n#### Media ingest (remote `upload_video`)\n\n`upload_video(video_path)` reads the file from the **server's** filesystem — a\nclient-local path can never resolve on a remote deployment. There are two ways\nto get the media onto the server.\n\n**Brokered upload (recommended, no ssh).** The agent orchestrates the whole\ntransfer over HTTP via the MCP. Call `request_ingest_upload`, then `PUT` the\nbytes to the one-time URL it returns (same Bearer token), then `upload_video`\nwith the returned server-side path:\n\n```bash\n# 1. request_ingest_upload(channel, filename=\"video.mp4\", size_bytes, sha256)\n#    → { upload_url, curl, video_path: \"/ingest/video.mp4\" }\n# 2. transfer the bytes (the returned curl recipe):\ncurl -fSs -T video.mp4 -H \"Authorization: Bearer $TUBEKIT_TOKEN\" \"<upload_url>\"\n# 3. upload_video(channel=\"...\", video_path=\"/ingest/video.mp4\", ...)\n```\n\nThe server streams the body to a staging `.part`, verifies `size_bytes` + sha256,\nand atomically publishes before `upload_video` reads it. The URL is a single-use,\nchannel-bound capability with a TTL; an interrupted transfer resumes via `HEAD` +\n`Content-Range`. Design rationale: [ADR 0002](docs/adr/0002-ingest-brokered-upload.md).\nRequires `TUBEKIT_INGEST_UPLOAD_URL_BASE` to be set (otherwise the tool returns\n`precondition_failed`).\n\n**rsync convention (fallback, requires ssh).** Transfer out-of-band, then call\nthe tool with the server-side path:\n\n```bash\nrsync -av --chmod=F644 video.mp4 <server>:/opt/tubekit-ingest/\n# upload_video(channel=\"...\", video_path=\"/ingest/video.mp4\", ...)\n```\n\nSet `TUBEKIT_INGEST_DIR` to the in-server ingest path (e.g. `/ingest`): when a\n`video_path` is unreadable the tool returns `validation_error` with a\n`suggestion` pointing the calling agent at this convention. See the\n[media-ingest section of the VPS runbook](docs/runbooks/vps-deploy.md#10-media-ingest-for-upload_video)\nfor the volume mount and retention policy.\n\n### Environment variables\n\nAll settings use the `TUBEKIT_` prefix (see `.env.example`):\n\n| Variable | Default | Purpose |\n|----------|---------|---------|\n| `TUBEKIT_STATE_DB_PATH` | `state.db` | SQLite file holding audit + state tables |\n| `TUBEKIT_MASTER_KEY_PATH` | `~/.config/tubekit/master.key` | Fernet key that encrypts stored OAuth tokens |\n| `TUBEKIT_OAUTH_CLIENT_SECRETS_PATH` | _unset_ | Path to `client_secret.json` (Google Cloud confidential client). **Required at runtime** — google-auth cannot refresh access tokens without it, so every channel tool fails if unset |\n| `TUBEKIT_TOKEN_BACKEND` | `sqlite_fernet` | Token store backend (`sqlite_fernet` \\| `in_memory`) |\n| `TUBEKIT_AUDIT_RETENTION_DAYS` | `365` | Audit retention window |\n| `TUBEKIT_OTLP_ENDPOINT` | _unset_ | OTLP collector endpoint; unset → no-op telemetry |\n| `TUBEKIT_INGEST_DIR` | _unset_ | Server-side media ingest dir. Advertised in `upload_video` path errors, and — for HTTP callers only — the **boundary** their `video_path`/`thumbnail_path` must resolve inside. Unset means no such boundary: an HTTP token is then equivalent to read access to the server's filesystem |\n| `TUBEKIT_INGEST_UPLOAD_URL_BASE` | _unset_ | Public origin for brokered uploads; unset disables `request_ingest_upload` |\n| `TUBEKIT_INGEST_UPLOAD_TTL_SECONDS` | `3600` | One-time brokered-upload token lifetime |\n| `TUBEKIT_INGEST_MAX_BYTES` | `21474836480` | Max size of a single brokered upload (20 GiB) |\n| `TUBEKIT_INGEST_MAX_PENDING` | `32` | Cap on concurrent pending upload tokens (disk-exhaustion guard) |\n| `TUBEKIT_INGEST_PURGE_AFTER_UPLOAD` | `true` | Delete the source file from the ingest dir once the upload/thumbnail is confirmed |\n| `TUBEKIT_HTTP_KEEPALIVE_TIMEOUT` | `5` | uvicorn keep-alive (s); raise for long SSE sessions, align with the proxy idle timeout |\n| `TUBEKIT_HTTP_LIMIT_CONCURRENCY` | _unset_ | uvicorn max concurrent connections |\n| `TUBEKIT_HTTP_GRACEFUL_SHUTDOWN_TIMEOUT` | _unset_ | uvicorn graceful-shutdown timeout (s) |\n| `TUBEKIT_HTTP_MAX_JSONRPC_BODY` | `4194304` | Max bytes the auth gate buffers for one JSON-RPC POST before authenticating (pre-auth DoS guard); oversized → 413 |\n| `TUBEKIT_HTTP_FORWARDED_ALLOW_IPS` | _unset_ | Reverse-proxy IPs/CIDRs whose `X-Forwarded-*` uvicorn may trust. Unset → uvicorn trusts only `127.0.0.1`, so behind a proxy every audit row is stamped with the *proxy's* IP and the request scheme stays `http`. Use the proxy's **own pinned address**, not the bridge subnet — every container on a shared network resolves inside that CIDR and could forge the header. **Never `*`** (rejected at load): it would let any caller choose the audited IP |\n| `TUBEKIT_HTTP_SSE_RESUMABLE` | `false` | Enable MCP streamable-HTTP resumability (in-memory event store) so a dropped SSE session resumes via `Last-Event-ID` |\n| `TUBEKIT_HTTP_SSE_RETRY_INTERVAL_MS` | `2000` | SSE reconnect hint (ms) advertised to clients; only used when resumable |\n| `TUBEKIT_SQLITE_BUSY_TIMEOUT_MS` | `5000` | SQLite writer lock wait before `SQLITE_BUSY` |\n| `TUBEKIT_LOG_LEVEL` / `TUBEKIT_LOG_FORMAT` | `INFO` / `json` | Logging |\n\n> A lost `state.db` / `master.key` pair means re-bootstrapping every channel.\n> Back them up together — see [`docs/runbooks/state-db-backup.md`](docs/runbooks/state-db-backup.md).\n\n## Hosted version\n\n[Gatecast](https://gatecast.sriera.dev/?ref=github) is the hosted tier of tubekit — same\nengine, run and kept alive for you. It is pre-launch; the waiting list is the\nwhole thing so far. Self-hosting with this repo is and stays fully supported.\n\n## Development\n\n```bash\nuv sync --all-extras\nmake lint    # ruff\nmake type    # mypy --strict\nmake test    # pytest unit suite\nmake cov     # coverage report\n```\n\n`make demo-stack-up` brings up the Jaeger/Prometheus/Grafana observability stack\nwith five provisioned dashboards (`docker-compose.demo.yml`, `docs/grafana/`).\n\n## Repository layout\n\n```\ndocs/adr/        Architecture Decision Records (source of truth for design)\ndocs/grafana/    Provisioned Grafana dashboards + demo observability config\ndocs/runbooks/   Operational procedures (master key rotation, audit verify, …)\ndocs/ROADMAP.md  Shipped, planned and under-exploration work\nsrc/tubekit/     Library code (Alembic migrations ship in src/tubekit/migrations/)\ntests/           Unit + integration suites\n```\n\n## Contributing & security\n\n- Contributions are welcome — see [`CONTRIBUTING.md`](CONTRIBUTING.md) for the\n  workflow and quality bar.\n- Report security vulnerabilities privately per [`SECURITY.md`](SECURITY.md).\n- Release history is in [`CHANGELOG.md`](CHANGELOG.md); the roadmap lives in\n  [`docs/ROADMAP.md`](docs/ROADMAP.md).\n\n## License\n\n[FSL-1.1-MIT](LICENSE.md) © 2026 sriera\n\nThe [Functional Source License](https://fsl.software) in plain words: read it,\nrun it, modify it, self-host it for your own channels — commercially or not.\nThe one thing it forbids is offering tubekit itself to others as a competing\nproduct or service. Each release additionally becomes plain **MIT two years\nafter its publication**, automatically and irrevocably.\n",
  "bytes": 26309,
  "sha": "d3522fa6ccb82db3163a39a42f0c428fa4a27c961989335e34f1bde5df1b519e",
  "repo_slug": "santiriera626/tubekit-mcp",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_santiriera626_tubekit_mcp_71f7e0d2/readme"
}