Skip to main content
Glama
README.md
# mcp-medusa

MCP server that exposes Medusa anime automation tools.

## Tools

### `add_anime`

Adds an anime to Medusa using `POST /api/v2/anime/add`.

Common arguments:

```json
{
  "anime_id": 62076,
  "source": "myanimelist",
  "root_dir": "/media/videos/Anime",
  "anime": true,
  "scene": false,
  "status": "wanted",
  "initial_release_group": "SubsPlease",
  "fallback_release_groups": ["SubsPlease", "Erai-raws"],
  "release_group_fallback_days": 7
}
```

Arguments:

| Argument | Required | Default | Allowed values / notes |
| --- | --- | --- | --- |
| `anime_id` | yes | none | Source-specific anime ID. For `myanimelist`, this is the MAL anime ID. |
| `root_dir` | yes | none | Medusa root directory path for the new series. |
| `source` | no | `myanimelist` | Case-insensitive: `myanimelist`, `livechart` |
| `anime` | no | `true` | Usually keep `true`. |
| `scene` | no | `false` | Whether to enable scene numbering. |
| `status` | no | `wanted` | Medusa episode status string, commonly `wanted`, `skipped`, or `ignored`. |
| `initial_release_group` | no | none | First/active release group to whitelist, for example `SubsPlease`. |
| `fallback_release_groups` | no | none | Ordered list of release groups to rotate through. Include the initial group first for deterministic behavior. |
| `release_group_fallback_days` | no | `7` | Days after episode airdate before switching to the next fallback group. |
| `directory_name` | no | Medusa-generated | Optional series folder name override. |

### `seasonal_anime`

Queries Medusa seasonal anime using `GET /api/v2/anime/seasonal`.

```json
{
  "year": 2026,
  "season": "SPRING",
  "source": "myanimelist",
  "source_sort": "anime_num_list_users",
  "page": 1,
  "limit": 10,
  "fields": ["animeId", "displayTitle", "year", "animeType", "genres", "score", "imageUrl"]
}
```

Minimal call using current anime season defaults:

```json
{}
```

Arguments:

| Argument | Required | Default | Allowed values / notes |
| --- | --- | --- | --- |
| `year` | no | current year | If omitted, calculated from the MCP server's current date. |
| `season` | no | current anime season | Case-insensitive: `WINTER`, `SPRING`, `SUMMER`, `FALL` or values like `Summer`. If omitted, calculated from the MCP server's current month. |
| `source` | no | `myanimelist` | Case-insensitive: `myanimelist`, `livechart` |
| `source_sort` | no | `anime_num_list_users` | Case-insensitive: `anime_num_list_users`, `anime_score` |
| `page` | no | `1` | Medusa result page number. Must be `1` or greater. |
| `limit` | no | `10` | Results per page. Medusa allows `1` through `1000`; default is intentionally small to avoid MCP client truncation. |
| `fields` | no | `null` | Optional list of response fields to keep. If omitted/null, the full Medusa anime objects are returned. |

Current anime season defaults use this month mapping:

| Months | Season |
| --- | --- |
| January-March | `WINTER` |
| April-June | `SPRING` |
| July-September | `SUMMER` |
| October-December | `FALL` |

Useful `fields` values include:

```json
["animeId", "source", "displayTitle", "titleRomanji", "titleEnglish", "year", "season", "animeType", "status", "episodes", "episodeInfo", "genres", "studios", "score", "numListUsers", "imageUrl", "anidbId", "tvdbId", "malId", "url", "directoryName"]
```

### `resolve_anime_title`

Resolves a title to enriched Medusa/MyAnimeList candidates using `GET /api/v2/anime/search` with `source`, `includeDetails=true`, and `limit`.

```json
{
  "title": "Frieren",
  "source": "myanimelist",
  "limit": 10,
  "min_score": 88,
  "score_gap": 8
}
```

Returns `decision` as `match`, `ambiguous`, or `no_match`, plus scored candidates.

### `anime_info`

Returns compact anime details and Medusa presence by title or MAL ID without adding anything.

```json
{
  "title": "Frieren",
  "source": "myanimelist"
}
```

or:

```json
{
  "mal_id": 52991,
  "source": "myanimelist"
}
```

### `seasonal_candidates`

Fetches seasonal candidates using Medusa's server-side seasonal filters before applying residual OpenClaw-specific heuristics.

```json
{
  "year": 2026,
  "season": "SUMMER",
  "source": "myanimelist",
  "limit": 25,
  "min_num_list_users": 3000,
  "fields": ["animeId", "displayTitle", "animeType", "genres", "numListUsers", "synopsis"]
}
```

The MCP tool sends Medusa filters such as `animeType=TV`, `minNumListUsers`, `excludeGenres=Kids,Boys Love`, `matched=false`, `firstSeasonOnly=true`, and `fields` to reduce payload size.

### `prepare_seasonal_review`

Compacts seasonal candidate items into an AI review packet with conservative review instructions.

```json
{
  "items": [{"animeId": 62076, "displayTitle": "Example Title"}],
  "max_items": 25
}
```

### `resolve_and_add_anime`

Resolves and dry-runs or executes a single add through Medusa's bulk-add endpoint. Writes require `execute: true`.

```json
{
  "title": "Frieren",
  "root_dir": "/media/videos/Anime",
  "execute": false
}
```

### `scheduler_status`

Returns Medusa scheduler thread health and queue state. Use when anime adds appear stuck or unverified.

```json
{}
```

### `search_tvdb`

Searches TVDB by show name for a TVDB ID. Use as fallback when an anime add fails due to AniDB-to-TVDB mapping issues.

```json
{ "query": "Dorohedoro", "language": "en" }
```

### `add_series`

Adds a series directly by TVDB ID via `POST /api/v2/series`. Bypasses anime resolution entirely. Use after `search_tvdb` when `add_anime` fails.

Arguments:

| Argument | Required | Default | Notes |
| --- | --- | --- | --- |
| `tvdb_id` | yes | none | TVDB show ID (from `search_tvdb`). |
| `root_dir` | yes | none | Medusa root directory path. |
| `anime` | no | `true` | |
| `status` | no | `wanted` | Accepts strings like `wanted`, `skipped`, `ignored` — converted to numeric internally. |
| `language` | no | none | Indexer language override. |
| `show_dir` | no | none | Custom series folder name override. |
| `scene` | no | `false` | |
| `paused` | no | `false` | |

```json
{ "tvdb_id": 370761, "root_dir": "/media/videos/Anime" }
```

### `set_episode_status`

Sets episode statuses for a series via `POST /api/v2/internal/updateEpisodeStatus`. Use when TVDB metadata is out of sync and Medusa is not downloading episodes that should be available.

Arguments:

| Argument | Required | Default | Notes |
| --- | --- | --- | --- |
| `series_slug` | yes | none | Series slug (e.g. `tvdb370761`). |
| `episodes` | yes | none | List of episode slugs (e.g. `["s01e01", "s01e02"]`). |
| `status` | no | `wanted` | Target status: `wanted`, `skipped`, `ignored`. |

```json
{ "series_slug": "tvdb370761", "episodes": ["s01e01", "s01e02"], "status": "wanted" }
```

### `force_search`

Force a manual search for specific episodes via `PUT /api/v2/search/manual`. Queues a provider search that actively queries enabled providers and snatches matching releases. Use after setting episodes to "wanted".

Arguments:

| Argument | Required | Default | Notes |
| --- | --- | --- | --- |
| `series_slug` | yes | none | Series slug (e.g. `tvdb370761`). |
| `episodes` | yes | none | List of episode slugs (e.g. `["s01e01", "s01e02"]`). |

```json
{ "series_slug": "tvdb370761", "episodes": ["s01e01"] }
```

### `update_series_quality`

Update allowed or preferred qualities for an existing series via `PATCH /api/v2/series/{slug}`. Uses human-readable quality names. Omitted fields are left unchanged.

Arguments:

| Argument | Required | Default | Notes |
| --- | --- | --- | --- |
| `series_slug` | yes | none | Series slug (e.g. `tvdb370761`). |
| `allowed_qualities` | no | none | Quality names for the allowed list. |
| `preferred_qualities` | no | none | Quality names for the preferred list. |

Quality names: `"SDTV"`, `"SD DVD"`, `"720p HDTV"`, `"RawHD"`, `"1080p HDTV"`, `"720p WEB-DL"`, `"1080p WEB-DL"`, `"720p BluRay"`, `"1080p BluRay"`, `"4K UHD TV"`, `"8K UHD TV"`, `"4K UHD WEB-DL"`, `"8K UHD WEB-DL"`, `"4K UHD BluRay"`, `"8K UHD BluRay"`.

```json
{ "series_slug": "tvdb370761", "allowed_qualities": ["1080p BluRay", "720p HDTV"] }
```

### `organize_season_folders`

Enable season folders and move episode files into `Season XX/` subdirectories without renaming files. Calls `POST /api/v2/series/{slug}/operation` with `type=ORGANIZE_SEASON_FOLDERS`.

Use when a multi-season anime has all files in a flat directory and Plex is missing episodes due to poor multi-season-in-one-folder support.

Arguments:

| Argument | Required | Default | Notes |
| --- | --- | --- | --- |
| `series_slug` | yes | none | Series slug (e.g. `tvdb370761`). |

```json
{ "series_slug": "tvdb370761" }
```

### `postprocess`

Trigger Medusa manual post-processing on a directory. Scans a download/staging folder and moves matched episodes into their show folders. Reuses the `MEDUSA_URL` / `MEDUSA_API_KEY` credentials. Use this to clear files stranded in staging after a failed auto-post-process (e.g. a mis-named release the download handler bailed on). Calls `POST /api/v2/postprocess`.

**IMPORTANT:** the `directory` argument is interpreted by **Medusa core**, so it must be the path as seen inside the Medusa container (e.g. `/data/medusa`), NOT the path visible from a separate Hermes container.

Arguments:

| Argument | Required | Default | Notes |
| --- | --- | --- | --- |
| `directory` | no | `/data/medusa` | Medusa-container path to scan. Must match Medusa core's view, not the Hermes mount. |
| `process_type` | no | `auto` | `auto`, `manual`, or `failed`. |
| `force_replace` | no | `false` | Overwrite existing episode files. |
| `process_method` | no | `copy` | `copy` or `move`. |

```json
{ "proc_dir": "/data/medusa", "proc_type": "auto" }
```

### `bulk_add_anime`

Dry-runs or executes multiple anime adds through `POST /api/v2/anime/bulk-add`. Writes require `execute: true`.

```json
{
  "items": [{"animeId": 62076, "displayTitle": "Example Title", "aiDecision": "add"}],
  "decision_field": "aiDecision",
  "allowed_decisions": ["add"],
  "execute": false
}
```

## Configuration

Environment variables:

| Variable | Required | Description |
| --- | --- | --- |
| `MEDUSA_URL` | yes | Base URL for Medusa, for example `http://medusa:8081` |
| `MEDUSA_API_KEY` | recommended | Medusa API key sent as `X-Api-Key` |
| `MEDUSA_WEB_ROOT` | no | Web root if Medusa is hosted below a path |
| `MEDUSA_TIMEOUT` | no | HTTP timeout in seconds, default `30` |
| `MCP_TRANSPORT` | no | `stdio` or `sse`, default `stdio` |
| `MCP_HOST` | no | SSE bind host, default `0.0.0.0` |
| `MCP_PORT` | no | SSE bind port, default `8000` |
| `MCP_DNS_REBINDING_PROTECTION` | no | Enable MCP SDK Host/Origin validation, default `false` |
| `MCP_ALLOWED_HOSTS` | no | Comma-separated allowed `Host` headers when DNS rebinding protection is enabled, for example `daniel-nas.localdomain:3001,localhost:3001` |
| `MCP_ALLOWED_ORIGINS` | no | Comma-separated allowed `Origin` headers when DNS rebinding protection is enabled |

For container/SSE deployments, keep `MCP_HOST=0.0.0.0` or set it explicitly. DNS rebinding protection is disabled by default because mapped ports, reverse proxies, and LAN hostnames otherwise commonly cause `421 Misdirected Request` errors. If you enable it, include the externally visible host and port in `MCP_ALLOWED_HOSTS`.

## Run locally

```bash
pip install -e .
MEDUSA_URL=http://localhost:8081 MEDUSA_API_KEY=... mcp-medusa
```

For SSE:

```bash
MEDUSA_URL=http://localhost:8081 MEDUSA_API_KEY=... mcp-medusa --transport sse --port 8000
```

## Docker

The image defaults to SSE on port `8000`.

```bash
docker build -t mcp-medusa .
docker run --rm -p 8000:8000 \
  -e MEDUSA_URL=http://medusa:8081 \
  -e MEDUSA_API_KEY=... \
  mcp-medusa
```

For stdio instead:

```bash
docker run --rm -i \
  -e MEDUSA_URL=http://medusa:8081 \
  -e MEDUSA_API_KEY=... \
  mcp-medusa --transport stdio
```

## CI/CD

The repo uses a fully automated pipeline (no manual release steps):

1. **Open a PR** against `main`. The `ci.yml` workflow runs `tests/test_boot.py`,
   which actually starts the server (stdio + SSE) and asserts `FastMCP`
   constructs, the expected tools register, and both transports bind. This is
   the gate that catches boot/import crashes before they ship.
2. **Auto-merge.** Once the boot test passes, the `enable-auto-merge` job merges
   the PR (squash) using the `ACTIONS_PAT` repo secret (the default
   `GITHUB_TOKEN` cannot merge PRs in this repo).
3. **Auto-release + build.** Pushing to `main` triggers `merge-release.yml`, which
   re-runs the boot test, creates a patch-bumped release (`v0.0.N+1`) off `main`,
   and **builds + pushes the Docker image in the same workflow** (the build is
   folded into `merge-release.yml` rather than a separate release-triggered
   workflow, because GitHub suppresses the `release: published` event when the
   release is created by a workflow via `GITHUB_TOKEN`).

### Running the tests locally

```bash
uv venv
uv pip install -e ".[test]"
uv run pytest tests/ -v
```

The boot test spawns the real server, so it needs no Docker. If you change
anything in `server.py` startup path, run this before pushing — CI will block
the merge if the server fails to boot.

### Required secrets

- `ACTIONS_PAT` — a fine-grained PAT (or classic `repo` token) with
  **Contents: read+write** and **Pull requests: read+write**, stored as a repo
  secret. Used by `ci.yml` to merge PRs. (The default `GITHUB_TOKEN` lacks
  merge capability here.)
- `DOCKER_USERNAME` / `DOCKER_PASSWORD` — used by `docker-release.yml` to push
  the image.

TDQS

B3.3/5.0

Scored across 21 tools

Disambiguation3/5

The core Medusa operations (aliases, release groups, searches, postprocessing) are clearly separated, but the anime-related tools have overlapping boundaries: seasonal_anime, seasonal_candidates, prepare_seasonal_review, resolve_anime_title, and resolve_and_add_anime all seem to cover similar discovery/dedup/resolution steps. Add flows are also split across add_series, add_anime, resolve_and_add_anime, and bulk_add_anime, so an agent may struggle to pick the right one without careful reading.

Naming Consistency4/5

Tool naming is overwhelmingly snake_case with a verb_noun pattern (create_alias, set_episode_status, update_series_quality, bulk_add_anime). Only a few tools deviate into noun-first or adjective-first forms such as anime_info and seasonal_anime, but the convention is still predictable overall.

Tool Count3/5

At 21 tools, the surface is on the upper edge of comfort and feels heavy for a cohesive server. The count is justifiable given the two intertwined domains of TV-series management and anime seasonal workflows, but some tools appear to cover niche pipeline stages that a smaller set could absorb.

Completeness3/5

The set covers many operational needs: adding series, adjusting statuses, searching, postprocessing, quality, season folders, aliases, scheduler status, and anime fetching. However, it lacks a general way to list or retrieve an existing series' slug/config, and there is no update path for aliases—only create/delete—so agents cannot fully reconcile or inspect existing state before modifying it.

Maintenance

ActivityMaintained
ResponsivenessNo issues