tvmaze-mcp-server
by cyanheads
README.md
<div align="center">
<h1>@cyanheads/tvmaze-mcp-server</h1>
<p><b>Search TVmaze shows and people, next episodes in your timezone, episode guides, daily schedules, and credits via MCP. STDIO or Streamable HTTP.</b>
<div>8 Tools</div>
</p>
</div>
<div align="center">
[](./CHANGELOG.md) [](./LICENSE) [](https://github.com/users/cyanheads/packages/container/package/tvmaze-mcp-server) [](https://modelcontextprotocol.io/) [](https://www.npmjs.com/package/@cyanheads/tvmaze-mcp-server) [](https://www.typescriptlang.org/) [](https://bun.sh/)
</div>
<div align="center">
[](https://github.com/cyanheads/tvmaze-mcp-server/releases/latest/download/tvmaze-mcp-server.mcpb) [](https://cursor.com/en/install-mcp?name=tvmaze-mcp-server&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBjeWFuaGVhZHMvdHZtYXplLW1jcC1zZXJ2ZXIiXX0=) [](https://vscode.dev/redirect?url=vscode:mcp/install?%7B%22name%22%3A%22tvmaze-mcp-server%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40cyanheads%2Ftvmaze-mcp-server%22%5D%7D)
[](https://www.npmjs.com/package/@cyanheads/mcp-ts-core)
</div>
<div align="center">
**Public Hosted Server:** [https://tvmaze.caseyjhand.com/mcp](https://tvmaze.caseyjhand.com/mcp)
</div>
---
## Overview
TV data from TVmaze: shows, episode guides, cast, and air times. Find a show by title or IMDb/TheTVDB id, see when the next episode airs in your timezone, or check what aired and streamed on any date. No API key required.
### Tools
| Tool | Description |
|:---|:---|
| `tvmaze_search_shows` | Fuzzy title search returning up to 10 shows with channel, status, genres, rating, and external catalog ids |
| `tvmaze_get_show` | Full profile for one TVmaze id — weekly slot, season list, and the previous and next episode |
| `tvmaze_lookup_show` | Resolve a show from its IMDb, TheTVDB, or TVRage id into the matching TVmaze profile |
| `tvmaze_get_next_episode` | When a show's next episode airs, by TVmaze id or title, converted to a viewer timezone |
| `tvmaze_get_episodes` | Episode guide for one season, one air date, or the whole run, with air times, runtimes, and synopses |
| `tvmaze_get_schedule` | Episodes airing on a date — broadcast and cable networks in one country, streaming services, or both |
| `tvmaze_get_cast` | A show's credited cast and the characters they play, optionally crew; or one episode's guest cast, optionally with its director and writers |
| `tvmaze_get_person` | Search person names for candidates, or page one person's cast, guest, or crew credits |
## Capability reference
### `tvmaze_search_shows` <sub>tool</sub>
- Fuzzy-matches `query` against every show title, so minor misspellings still resolve; blank titles are rejected
- Returns at most 10 shows with no pagination; output reports `shown` / `cap` and points a full or empty result to a narrower title or `tvmaze_lookup_show`
- Rows carry the shared show summary plus `match_score`, comparable only within one result set
---
### `tvmaze_get_show` <sub>tool</sub>
- `show_id` from `tvmaze_search_shows`, `tvmaze_lookup_show`, or a schedule row; optional IANA `timezone` for episode times
- Returns the full profile (`schedule_days` / `schedule_time`, optional `schedule_timezone`, `official_site`, `externals`) with every season plus `next_episode` / `previous_episode`. A `Running` show with nothing announced comes back with a notice pointing at `previous_episode`
- An unknown `show_id` fails as `show_not_found`
---
### `tvmaze_lookup_show` <sub>tool</sub>
- `source` is `imdb` (a `tt` id), `thetvdb`, or `tvrage` (defunct, present only in older records), paired with `external_id`
- IMDb ids also accept bare digits, uppercase `TT`, and `imdb.com` title URLs
- A show absent from TVmaze is a result, not an error: `found: false` plus `guidance` routing to `tvmaze_search_shows`. A hit echoes `source` and `external_id` and returns the same show summary as search
---
### `tvmaze_get_next_episode` <sub>tool</sub>
- `by: "id"` takes a TVmaze id; `by: "title"` resolves a non-blank title through a stricter single-match search than `tvmaze_search_shows`
- Air times render in the requested IANA `timezone`; `time_known: false` means no clock time was announced, so only the date is reliable
- Misses carry a typed `miss_reason`: `show_not_found` (title) or `no_scheduled_episode` (between seasons, with `previous_episode`). A `show_id` that resolves to nothing throws `show_not_found_by_id`
---
### `tvmaze_get_episodes` <sub>tool</sub>
- `season` lists one season; `air_date` (`YYYY-MM-DD`) lists one day's episodes; omit both for the whole run. They cannot be combined. An empty day returns an empty list with a notice, an impossible date fails as `invalid_date`, and `season_not_found` names the seasons that exist
- `air_date` matches the source's `airdate`, which can differ by a day from an episode's `local_date` on late-night slots
- `include_specials` defaults to false, and each listing reports how many specials it filtered out. `limit` (default 50, max 250) sets page size; `next_cursor` / `has_more` continue the listing and `totalCount` counts before paging
---
### `tvmaze_get_schedule` <sub>tool</sub>
- `scope` picks the feed: `linear` is one country's broadcast and cable networks plus its own streaming services, `streaming` is global services when `country` is omitted and that country's local ones when given, `all` merges both
- `date` defaults to today in the requested `timezone`; `country` is ISO 3166-1 alpha-2 (the United Kingdom is `GB`) and falls back to the configured default for `linear` and `all`; optional `channel` filters by exact, case-insensitive channel name; `limit` defaults to 50 (max 250)
- Entries carry `feed` (`linear` / `streaming`) and a compact `show` reference (`tvmaze_get_show` returns the full profile). `applied_feeds` names the feeds that answered, e.g. `["linear:GB","web:GB","web:global"]`; one failing feed degrades to a notice instead of failing the call
---
### `tvmaze_get_cast` <sub>tool</sub>
- `scope: "show"` returns the main cast with character names; `scope: "episode"` returns that episode's guest cast. `include_crew` adds the show's crew, or the episode's guest crew (director, writers)
- Optional `name` filters by substring of performer, character, or crew role (crew only with `include_crew: true`). Paged with `limit` (default 50, max 250) and `cursor`; `cast_total` / `crew_total` count all matching credits
- Cast credits carry `as_self` and `voice_only`; crew credits carry `credit_type` and no character. A show's cast list does not separate recurring from guest performers, so check an episode's guest cast. Missing credits arrive as a notice, not an error
### `tvmaze_get_person` <sub>tool</sub>
- Supply exactly one of `query` (name search) or `person_id` (from cast credits or a candidate). Search returns compact candidates with source scores and never picks one for you; ten candidates may mean the source cap, so narrow the name
- A `person_id` returns the person and one `credit_kind`: `cast` (default), `guest`, or `crew`; `limit` (default 50, max 250) and `cursor` apply only here. Guest `episode_id` chains to `tvmaze_get_cast`; `show_id` chains to `tvmaze_get_show` or `tvmaze_get_episodes`
- No candidates or credits is an empty success; a missing id is `person_not_found`
## Features
Built on [`@cyanheads/mcp-ts-core`](https://github.com/cyanheads/mcp-ts-core): stdio and Streamable HTTP transports, pluggable auth (`none` / `jwt` / `oauth`), swappable storage (`in-memory`, `filesystem`, `Supabase`, `Cloudflare KV/R2/D1`), structured logging with optional OpenTelemetry tracing.
TVmaze-specific:
- Keyless — no account, no API key, and every tool works on a fresh install with nothing configured
- Requests are paced under TVmaze's per-IP limit, back off on 429 (honoring `Retry-After`), and are cached in-process
- Air times are computed from `airstamp` alone, since `airdate` / `airtime` can be a full day off on overnight slots
- Community-authored HTML summaries are stripped to plain text
Agent-friendly output:
- No fabricated clock times: a record with no announced time reports `time_known: false` and a date only, and absent fields render as `Not available` rather than `0` or `""`
- Typed errors on every tool, each with a `reason` and a recovery hint
- Resolution misses are results: `tvmaze_lookup_show` and the title arm of `tvmaze_get_next_episode` return `found: false` with `guidance` for the next call
- Output states what a call covered: the echoed query, `applied_feeds`, pre-page totals, and truncation against the source's caps
## Data and licensing
Data comes from [TVmaze](https://www.tvmaze.com) and is licensed **CC BY-SA**. Credit TVmaze as the source and keep the `url` field that every show, episode, and person record carries — linking back is what satisfies attribution. Under ShareAlike, an adaptation of this data must be shared under the same licence.
TVmaze rate-limits to at least 20 calls every 10 seconds per IP address and answers a burst past that with HTTP 429; the server paces itself under that budget and backs off when one arrives. Upstream caches its output for 60 minutes, so a schedule change or a newly announced episode can take up to an hour to appear; the local response cache (`TVMAZE_CACHE_TTL_S`, default 300 s) sits well inside that window.
## Getting started
### Public Hosted Instance
A public instance is available at `https://tvmaze.caseyjhand.com/mcp` — no installation required. Point any MCP client at it via Streamable HTTP:
```json
{
"mcpServers": {
"tvmaze-mcp-server": {
"type": "streamable-http",
"url": "https://tvmaze.caseyjhand.com/mcp"
}
}
}
```
### Self-Hosted / Local
Add the following to your MCP client configuration file. No API key is required.
```json
{
"mcpServers": {
"tvmaze-mcp-server": {
"type": "stdio",
"command": "bunx",
"args": ["@cyanheads/tvmaze-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info"
}
}
}
}
```
Or with npx (no Bun required):
```json
{
"mcpServers": {
"tvmaze-mcp-server": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@cyanheads/tvmaze-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info"
}
}
}
}
```
Or with Docker:
```json
{
"mcpServers": {
"tvmaze-mcp-server": {
"type": "stdio",
"command": "docker",
"args": ["run", "-i", "--rm", "-e", "MCP_TRANSPORT_TYPE=stdio", "ghcr.io/cyanheads/tvmaze-mcp-server:latest"]
}
}
}
```
For Streamable HTTP, set the transport and start the server:
```sh
MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 bun run start:http
# Server listens at http://localhost:3010/mcp
```
### Prerequisites
- [Bun v1.4.0](https://bun.sh/) or higher (or Node.js v24+).
- No TVmaze account or API key. Set `TVMAZE_DEFAULT_TIMEZONE` and `TVMAZE_DEFAULT_COUNTRY` once if the calls should default to somewhere other than UTC and the US.
### Installation
1. **Clone the repository:**
```sh
git clone https://github.com/cyanheads/tvmaze-mcp-server.git
```
2. **Navigate into the directory:**
```sh
cd tvmaze-mcp-server
```
3. **Install dependencies:**
```sh
bun install
```
4. **Configure environment (optional):**
```sh
cp .env.example .env
# every variable is optional — edit only what you want to override
```
## Configuration
| Variable | Description | Default |
|:---------|:------------|:--------|
| `TVMAZE_BASE_URL` | TVmaze API base URL. Override to point at an enterprise endpoint. | `https://api.tvmaze.com` |
| `TVMAZE_USER_AGENT` | `User-Agent` sent on every upstream request; TVmaze asks that clients identify themselves. | server name, version, and repository URL |
| `TVMAZE_DEFAULT_TIMEZONE` | IANA timezone used when a tool call omits `timezone`. | `UTC` |
| `TVMAZE_DEFAULT_COUNTRY` | ISO 3166-1 alpha-2 country used for `tvmaze_get_schedule` scopes `linear` and `all` when `country` is omitted. | `US` |
| `TVMAZE_CACHE_TTL_S` | Seconds to hold an upstream response in the in-process cache. `0` disables caching. | `300` |
| `TVMAZE_MAX_CONCURRENCY` | Concurrent upstream requests (1–16). | `4` |
| `TVMAZE_REQUEST_TIMEOUT_MS` | Per-request timeout in milliseconds (1000–120000). | `10000` |
| `MCP_TRANSPORT_TYPE` | Transport: `stdio` or `http`. | `stdio` |
| `MCP_HTTP_PORT` | Port for the HTTP server. | `3010` |
| `MCP_HTTP_ENDPOINT_PATH` | Path the MCP server is mounted at. | `/mcp` |
| `MCP_SESSION_MODE` | HTTP session mode; this server is always `stateless`. | `stateless` |
| `MCP_AUTH_MODE` | Auth mode: `none`, `jwt`, or `oauth`. | `none` |
| `MCP_LOG_LEVEL` | Log level (RFC 5424). | `info` |
| `LOGS_DIR` | Directory for log files (Node.js only). | `<project-root>/logs` |
| `LOG_TOOL_FAILURE_PAYLOADS` | Log a failed tool call's arguments and result (key-name redaction only). | `false` |
| `STORAGE_PROVIDER_TYPE` | Storage backend. | `in-memory` |
| `OTEL_ENABLED` | Enable [OpenTelemetry instrumentation](https://github.com/cyanheads/mcp-ts-core/tree/main/docs/telemetry) (spans, metrics, completion logs). | `false` |
See [`.env.example`](./.env.example) for the full list of optional overrides.
## Running the server
### Local development
- **Build and run:**
```sh
# One-time build
bun run rebuild
# Run the built server
bun run start:stdio
# or
bun run start:http
```
- **Run checks and tests:**
```sh
bun run devcheck # Lint, format, typecheck, security
bun run test # Vitest test suite
bun run lint:mcp # Validate MCP definitions against spec
```
### Docker
```sh
docker build -t tvmaze-mcp-server .
docker run --rm -p 3010:3010 tvmaze-mcp-server
```
The Dockerfile defaults to HTTP transport, stateless session mode, and logs to `/var/log/tvmaze-mcp-server`. OpenTelemetry peer dependencies are installed by default — build with `--build-arg OTEL_ENABLED=false` to omit them.
## Project structure
| Directory | Purpose |
|:----------|:--------|
| `src/index.ts` | `createApp()` entry point — registers the eight tools, server instructions, and the service lifecycle. |
| `src/config` | Server-specific environment variable parsing and validation with Zod. |
| `src/mcp-server/tools` | Tool definitions (`*.tool.ts`) and the output schemas they share. |
| `src/services/tvmaze` | TVmaze REST client — pacing, retries, response cache, and normalization into the domain types. |
| `tests/` | Unit and integration tests mirroring `src/`. |
| `docs/` | Design document and the generated project tree. |
## Development guide
See [`CLAUDE.md`/`AGENTS.md`](./CLAUDE.md) for development guidelines and architectural rules. The short version:
- Handlers throw, framework catches — no `try/catch` in tool logic
- Use `ctx.log` for request-scoped logging; every upstream call goes through `TvmazeService`, never `fetch` from a handler
- Register new tools in the `createApp()` arrays in `src/index.ts`
- Keep the upstream boundary in the service: validate raw → normalize to the domain type → return the output schema, and never fabricate a missing field — an absent air time stays absent
## Contributing
Issues are welcome. Run checks and tests before submitting:
```sh
bun run devcheck
bun run test
```
## License
Apache-2.0 — see [LICENSE](./LICENSE) for details.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessWithin a week