# 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:** `2026-07-28` (current), plus `2025-11-25`, `2025-06-18`, `2025-03-26`,
`2024-11-05` for clients that still open with `initialize`

## 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" \
  -H "MCP-Protocol-Version: 2026-07-28" \
  -H "Mcp-Method: tools/list" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{"_meta":{
        "io.modelcontextprotocol/protocolVersion":"2026-07-28",
        "io.modelcontextprotocol/clientCapabilities":{}}}}'
```

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

## Protocol

The server speaks both shapes of MCP on the same endpoint, chosen per request from the version
the client declares. There is nothing to configure either way.

**2026-07-28 (current).** No handshake. Every request carries
`params._meta["io.modelcontextprotocol/protocolVersion"]` and
`params._meta["io.modelcontextprotocol/clientCapabilities"]`, mirrored into the
`MCP-Protocol-Version` and `Mcp-Method` headers (plus `Mcp-Name` on `tools/call`) so
intermediaries can route without reading the body. Header and body **must agree** — a
disagreement is `400` with JSON-RPC `-32020`, because a load balancer routing on one while the
server executes the other is a confused deputy. Ask what the server is with `server/discover`:

```bash
curl -s -X POST https://my-tv-channel.com/api/mytvchannel/mcp \
  -H "Content-Type: application/json" \
  -H "MCP-Protocol-Version: 2026-07-28" -H "Mcp-Method: server/discover" \
  -d '{"jsonrpc":"2.0","id":1,"method":"server/discover","params":{"_meta":{
        "io.modelcontextprotocol/protocolVersion":"2026-07-28",
        "io.modelcontextprotocol/clientCapabilities":{}}}}'
```

It returns `supportedVersions`, `capabilities` and `instructions`. An unsupported version is
refused with `-32022` and the list of versions we do speak, so a client can retry rather than
guess. `tools/list` results carry `ttlMs`/`cacheScope` and are in a stable order, so they are
safe to cache.

**2025-11-25 and earlier.** The `initialize` handshake still works exactly as it did, and
legacy responses carry none of the newer fields. Nothing that works today stops working.

Not implemented, deliberately: no resources, prompts, sampling, roots or logging (this is a
verb surface); no `subscriptions/listen` — the tool list only changes when we deploy, so there
is nothing to push. `ping` is answered even though 2026-07-28 drops it, because clients use it
as a keepalive. Sessions never existed here, so any `Mcp-Session-Id` is ignored.

## 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.
