# My TV Channel — MCP Server

Connect an AI agent to My TV Channel so it can create and run 24/7 linear TV channels for you.

**Endpoint:** `https://my-tv-channel.com/api/mytvchannel/mcp`
**Transport:** Streamable HTTP (stateless — no session id to track)
**Auth:** `Authorization: Bearer mytv_sk_...`
**Protocol revisions:** `2025-06-18`, `2025-03-26`, `2024-11-05`

## Get a key

In the app under **Profile → Agent Access**, or at <https://my-tv-channel.com/agent-keys/>.
An active subscription (any tier) is required. Keys inherit the owner's tier limits.
The key is shown once — store it. An agent cannot mint its own key, by design.

## Connect

### Claude Code

```bash
claude mcp add --transport http my-tv-channel \
  https://my-tv-channel.com/api/mytvchannel/mcp \
  --header "Authorization: Bearer mytv_sk_YOUR_KEY"
```

### Claude Desktop / any client using `mcpServers` JSON

```json
{
  "mcpServers": {
    "my-tv-channel": {
      "type": "http",
      "url": "https://my-tv-channel.com/api/mytvchannel/mcp",
      "headers": {
        "Authorization": "Bearer mytv_sk_YOUR_KEY"
      }
    }
  }
}
```

### Anything else

Plain JSON-RPC 2.0 over HTTP POST. No SDK required:

```bash
curl -s -X POST https://my-tv-channel.com/api/mytvchannel/mcp \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $KEY" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```

`initialize` and `tools/list` work without a key, so a client can discover the server before
credentials are configured. `tools/call` requires one.

## Try it — example prompts

Once connected, these work as-is:

1. **"What's my My TV Channel account status — what tier am I on and how much upload quota is
   left?"**
   → `get_account_status`. Good first call: it also lists every channel and exactly what each
   one still needs before it can go live.

2. **"Create a channel called Retro Arcade TV, handle `retro-arcade`, import these four videos,
   and tell me when it's broadcasting."**
   → `check_handle_available` → `create_channel` → `import_video_from_url` ×4 →
   `get_transcode_status` until ready → `get_channel` until `scheduleStatus` is `active`.
   The channel then streams at `https://retro-arcade.my-tv-channel.com`.

3. **"Play the long-plays twice as often as the reviews on my arcade channel, starting now."**
   → `list_assets` → `set_asset_weight` per video → `regenerate_schedule`. The last step is the
   one people forget; without it the new weights are stored but never reach the air.

4. **"Show me what's on my channel this evening."**
   → `get_schedule` returns the EPG with start/end times.

5. **"Put cartoons on 6–9am on weekdays and news every day at 6pm."** *(Pro tier and above)*
   → `validate_schedule_rules` first — it saves nothing and returns a simulated 24h grid — then
   `set_schedule_rules`.

## Tools (19)

| Tool | Does | Safe to autorun? |
|---|---|---|
| `get_account_status` | Tier, limits, quota used/remaining, all channels + what they need to go live | read-only |
| `list_channels` | Channels owned by this account | read-only |
| `get_channel` | One channel — includes `scheduleStatus`, poll this to confirm it went live | read-only |
| `check_handle_available` | Is a handle free | read-only |
| `list_assets` | Videos on a channel | read-only |
| `get_transcode_status` | Poll progress; tells you when launch requirements are met | read-only |
| `get_schedule` | Read the EPG | read-only |
| `get_schedule_rules` | Current ruleset | read-only |
| `validate_schedule_rules` | Dry-run a ruleset; **saves nothing**, returns a simulated 24h grid | read-only |
| `create_channel` | Create a channel | writes |
| `update_channel` | Rename / re-describe / re-tag / change visibility | writes |
| `import_video_from_url` | **Main way to add content.** Import from a direct media URL | writes, external |
| `import_youtube_video` | Import a YouTube video the owner proves they own | writes, external |
| `set_asset_weight` | How often a video plays (0–10) + title/tags | writes |
| `retry_transcode` | Re-queue a failed transcode | writes |
| `archive_channel` | Take off air — archives, does not destroy | **destructive** |
| `delete_asset` | Remove a video (soft delete, auto-regenerates) | **destructive** |
| `regenerate_schedule` | Rebuild the schedule now — replaces what was going to air | **destructive** |
| `set_schedule_rules` | **Replaces** the whole ruleset | **destructive** |

Every tool ships MCP `annotations` (`readOnlyHint`, `destructiveHint`, `idempotentHint`,
`openWorldHint`), so a client can decide for itself what to run unattended. "Destructive" here
means it changes or removes something the owner can see — none of it is unrecoverable: archiving
keeps the handle and the assets, and `delete_asset` is a soft delete.

## Two things that surprise people

**You cannot upload local files over MCP.** MCP tool arguments are JSON; there is no multipart
channel. `import_video_from_url` is therefore *the* ingestion path — the video must be reachable
at a direct, public URL. If the owner has files on their laptop, they upload through the app.

**Weights only air when the schedule is generated.** A channel builds its schedule the instant it
crosses the launch threshold (4+ ready videos, 15+ minutes). Weights set after that are stored but
do not change what airs until a regeneration. Either set each video's weight as you import it, or
call `regenerate_schedule` afterwards.

## How it works

A thin proxy. Every tool forwards to the REST endpoint that already implements it, over a
loopback-pinned HTTPS call, passing your `Authorization` header straight through. Auth,
ownership, quota and tier gating all live in the REST layer and are never re-implemented here —
so the MCP surface cannot drift from the API. See `backend/mcp/index.php`.

Served at a path rather than `mcp.my-tv-channel.com` because the channel vhost matches
`~^(?<channel>[^.]+)\.my-tv-channel\.com$` — an `mcp.` subdomain would be routed as a TV channel.
