Skip to main content
Glama
README.md
<img src="https://raw.githubusercontent.com/githendrik/sf-mm-mcp/main/assets/logo-256.png" alt="" width="96" align="left">

# sf-mm-mcp

An MCP server that exposes [moviemagnet.site](https://moviemagnet.site) (movies) and
[showfeed.site](https://showfeed.site) (TV shows) to LLM clients, with put.io as the
download backend.

Search a title, find magnet links ranked by seeders, and start the download on put.io —
all from inside Claude Desktop, Claude Code, opencode, or any other MCP client.

```
npx sf-mm-mcp login    # one-time put.io sign-in
npx sf-mm-mcp          # start the server on stdio
npx sf-mm-mcp http     # ...or on streamable HTTP, for clients that connect by URL
```

## Setup

### 1. Add the server to your MCP client

The put.io app id (`9525`) is built in, so there is nothing to configure for a default
install.

Claude Desktop (`claude_desktop_config.json`), Claude Code (`.mcp.json`), and opencode
(`opencode.json`) all use the same shape:

```json
{
  "mcpServers": {
    "sf-mm": {
      "command": "npx",
      "args": ["-y", "sf-mm-mcp"]
    }
  }
}
```

#### opencode

```json
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "sfmm": {
      "type": "local",
      "command": ["npx", "-y", "sf-mm-mcp"],
      "enabled": true
    }
  }
}
```

opencode prefixes tool names with the server name, so the tools appear as
`sfmm_movies_search`, `sfmm_putio_login`, and so on. Verify the connection with
`opencode mcp list`.

Note that `opencode mcp auth` does **not** apply here. That command drives OAuth for
*remote* MCP servers via dynamic client registration; this is a local stdio server that
manages its own put.io credential. Use the sign-in below instead.

See [`docs/clients.md`](docs/clients.md) for Cursor, VS Code, Windsurf, Zed,
Cline and others.

### 2. Sign in

Either run `npx sf-mm-mcp login` in a terminal, or ask your assistant to call the
`putio_login` tool and follow the instructions it returns.

`putio_login` is non-blocking by design, so the in-chat flow works: the tool returns a
code immediately, you enter it at put.io/link, and `putio_auth_status` confirms. The token
is stored per-machine, so signing in once covers every MCP client on that machine.

## Authentication

The default is put.io's **OOB (out-of-band) device flow**: the server requests a short
code, you enter it at <https://put.io/link>, and the server polls until it is linked.

This is the default rather than the more familiar redirect flow for a specific reason: an
npx-distributed package cannot hold a client secret. Anything baked into the tarball is
readable by everyone who installs it. The OOB flow needs no secret and no loopback
listener, so it also works over SSH and inside containers.

If you control the environment and prefer the redirect flow, set:

```bash
SF_MM_AUTH_FLOW=loopback
PUTIO_CLIENT_SECRET=...          # supply it yourself; never commit it
PUTIO_REDIRECT_URI=http://127.0.0.1:41830/callback
```

The redirect URI must match one registered on your put.io app. `state` is generated per
handshake and verified on the callback.

### Where the token is stored

In order of preference:

1. `PUTIO_TOKEN` environment variable, if set (nothing is persisted).
2. OS keychain — macOS Keychain via `security`, Linux via `secret-tool`.
3. A `0600` file at `~/.config/sf-mm-mcp/token.json`.

`npx sf-mm-mcp status` reports which backend is in use. `npx sf-mm-mcp logout` clears it.

## Tools

### Discovery — no sign-in required

| Tool | Purpose |
|---|---|
| `movies_search` | Find movies by title, returns IMDb ids |
| `movies_get` | Full details for one movie |
| `movies_popular` | Most-viewed movies on moviemagnet.site |
| `shows_search` | Find TV shows by title |
| `shows_get` | Show details plus episode list, optionally one season |
| `shows_popular` | Most-viewed shows on showfeed.site |

### Torrents — sign-in required

| Tool | Purpose |
|---|---|
| `movies_find_torrents` | Magnets for a movie, filterable by resolution and seeders |
| `shows_find_torrents` | Magnets for a show, filterable by season, episode, resolution |

Results are normalised to `{title, magnet, sizeHuman, seeders, resolution, season, episode, ageDays}`
and sorted by seeder count.

### put.io — sign-in required

| Tool | Purpose |
|---|---|
| `putio_add_transfer` | Start a download from a magnet or URL |
| `putio_list_transfers` | List transfers with progress |
| `putio_get_transfer` | Poll a single transfer |
| `putio_cancel_transfers` | Cancel or remove transfers |
| `putio_clean_transfers` | Clear completed transfer entries |
| `putio_account` | Username and disk usage |
| `putio_list_files` | Browse files and folders |
| `favorites_list` / `favorites_update` | Read and edit the favorites list |

Favorites are stored in the put.io account config under the same `favorites` key the two
websites use, so the list stays in sync across all three.

### Auth

`putio_login`, `putio_auth_status`, `putio_logout`.

`putio_login` returns instructions immediately rather than blocking — waiting for a human
to finish a browser flow would hold the tool call open for minutes and most clients time
out first. Poll `putio_auth_status` to confirm.

## Configuration

All optional.

| Variable | Default | Purpose |
|---|---|---|
| `PUTIO_CLIENT_ID` | `9525` | put.io OAuth app id (public); override to use your own app |
| `PUTIO_TOKEN` | — | Use a token directly, skipping sign-in |
| `SF_MM_AUTH_FLOW` | `oob` | `oob` or `loopback` |
| `PUTIO_CLIENT_SECRET` | — | Only for `loopback` |
| `PUTIO_REDIRECT_URI` | `http://127.0.0.1:41830/callback` | Only for `loopback` |
| `MOVIEMAGNET_BASE_URL` | `https://moviemagnet.site` | Point at a dev instance |
| `SHOWFEED_BASE_URL` | `https://showfeed.site` | Point at a dev instance |
| `PUTIO_API_BASE_URL` | `https://api.put.io/v2` | |
| `SF_MM_CONFIRM_TRANSFERS` | `false` | Require `confirm=true` on `putio_add_transfer` |
| `SF_MM_TORRENT_CACHE_TTL` | `900` | Torrent result cache, seconds |
| `SF_MM_METADATA_CACHE_TTL` | `3600` | Metadata cache, seconds |
| `SF_MM_TOKEN_STORE` | `auto` | `auto`/`keychain`/`file`/`none`; `none` disables persistence |
| `SF_MM_LOG_LEVEL` | `info` | `debug`/`info`/`warn`/`error`/`silent` |
| `SF_MM_TRANSPORT` | `stdio` | `stdio` or `http` (same as the `http` subcommand) |
| `SF_MM_HTTP_HOST` | `127.0.0.1` | HTTP mode bind address |
| `SF_MM_HTTP_PORT` | `8009` | HTTP mode port |
| `SF_MM_HTTP_PATH` | `/mcp` | HTTP mode endpoint path |
| `SF_MM_HTTP_TOKEN` | — | Require `Authorization: Bearer <token>` in HTTP mode |

## Transports

**stdio** (default) is what desktop MCP clients spawn. **Streamable HTTP** is for
clients that connect by URL and cannot spawn a process — Open WebUI, for example:

```bash
SF_MM_HTTP_TOKEN=$(openssl rand -hex 32) npx -y sf-mm-mcp http
# → http://127.0.0.1:8009/mcp
```

`GET /health` is available for supervisors and returns the running version.

> **HTTP mode shares one put.io account across every client that can reach the
> port.** Unlike stdio, where the server belongs to a single desktop user, there
> is no per-user authorization: anyone who can call the endpoint can spend the
> linked account's storage and bandwidth. It therefore binds to loopback by
> default and refuses nothing else — bind wider only together with
> `SF_MM_HTTP_TOKEN`, and consider `SF_MM_CONFIRM_TRANSFERS=true`.

See [`docs/clients.md`](docs/clients.md#open-webui) for the Open WebUI setup and
a systemd unit.

## Caching and rate limits

Both websites validate the put.io token on every `/api/torrent` request, and that endpoint
fans out to torrent indexers. An LLM will call these far more eagerly than a human
clicking through a web page, so this server:

- caches torrent results (15 min) and metadata (1 hour) in process,
- de-duplicates concurrent identical requests,
- caches token-validation results for 10 minutes rather than revalidating per call,
- honours put.io's `429` plus `X-RateLimit-Reset` headers with backoff,
- never retries a write, so a transfer cannot be silently created twice.

## Development

```bash
npm install
npm run build
npm run typecheck
npm test           # boots the server, asserts tool surface; no network
npm run smoke      # exercises the live public endpoints over MCP stdio
```

`npm test` is network-free on purpose, so CI does not go red because a torrent
indexer is having a bad day. It runs with `SF_MM_TOKEN_STORE=none` so it cannot
accidentally pick up a developer's real token and skip the auth-gating checks.

## Releasing

```bash
npm version patch && git push --follow-tags
```

Publishing runs from GitHub Actions on a version tag. There is no npm token
anywhere: the workflow authenticates with npm via OIDC
[trusted publishing](https://docs.npmjs.com/trusted-publishers), which also
generates a provenance attestation automatically.

```bash
npm version patch     # or minor / major - commits and tags
git push --follow-tags
```

`.github/workflows/publish.yml` then refuses to continue unless:

- the tag matches `package.json` version exactly,
- that version is not already on npm,
- typecheck, build and the tool-surface test all pass,
- no `.env` file made it into the tarball.

To rehearse without publishing, run the workflow manually from the Actions tab
with **dry-run** left checked. Full detail in
[`docs/releasing.md`](docs/releasing.md).

## Documentation

| | |
|---|---|
| [`docs/architecture.md`](docs/architecture.md) | Design decisions and why the alternatives lost |
| [`docs/auth.md`](docs/auth.md) | Credential model; why there is no client secret |
| [`docs/upstream-apis.md`](docs/upstream-apis.md) | The two sites' API surface and its quirks |
| [`docs/clients.md`](docs/clients.md) | Configuration for every common MCP client |
| [`docs/releasing.md`](docs/releasing.md) | Release procedure and CI guards |

Agents should start at [`AGENTS.md`](AGENTS.md).

Point at local dev servers with `MOVIEMAGNET_BASE_URL=http://localhost:5173`.

## Notes on the upstream APIs

Neither site publishes a schema, so the shapes in `src/schemas.ts` were derived by reading
the SvelteKit route handlers and probing the live endpoints. Two things worth knowing:

- Both sites expect the put.io token in a **bare `Authorization` header with no `Bearer`
  prefix**. This client matches that.
- Neither site has a "create transfer" endpoint — the web apps do it client-side. So
  transfers, account info, and favorites go directly to `api.put.io`.

Validation failures are logged rather than swallowed, so upstream schema drift shows up as
a warning on stderr instead of a silent "no results found".

### A bug this client used to work around

Both sites once returned `500` on the **first** request for any given title, because
`torrentService.js` destructured the Jackett response as `const { instance, data: results }`
— declaring a new block-scoped `results` that shadowed the outer one, leaving it undefined
on every cache miss. It hid well: the shadowed value was still written to Redis, so a retry
hit the cache and succeeded. Titles with zero indexer results were never cached and so
failed permanently.

**Fixed and deployed in both apps** (`showfeed-app@6640310`, `moviemagnet-app@77b22fe`),
along with `queryMultipleServices`, which only ever called `resolve` and therefore hung
forever if every indexer failed.

The `5xx` retry in this client predates those fixes. It is kept because indexers still fail
transiently, but it is no longer masking a deterministic upstream failure. Full write-up in
[`docs/upstream-apis.md`](docs/upstream-apis.md#bugs-found-and-fixed-upstream).

## Credits

Logo icons from [pepicons](https://pepicons.com) by CyCraft, licensed CC BY 4.0.
See [`assets/README.md`](assets/README.md).

TDQS

A3.9/5.0

Scored across 20 tools

Disambiguation4/5

Tools are cleanly namespaced by domain (movies_, shows_, putio_, favorites_) and each targets a distinct resource or action. The only potential source of confusion is putio_clean_transfers versus putio_cancel_transfers, but their descriptions clearly separate bulk cleanup of completed entries from targeted cancellation/removal.

Naming Consistency4/5

Most tools follow a consistent [resource]_[action] pattern, with actions like get, search, add, list, cancel, update. A few outliers use adjectives or nouns for the suffix (movies_popular, putio_account, putio_auth_status), which breaks the verb_noun convention, but the overall prefix scheme remains predictable.

Tool Count3/5

With 20 tools, the server is at the heavy end of the scale, but the count is justified by the multiple subdomains (movies, shows, put.io, favorites). It borders on feeling bloated, yet each tool serves a distinct function and the logical grouping helps manageability.

Completeness4/5

The tool surface covers the core lifecycle for discovering media, finding torrents, managing put.io transfers, and maintaining favorites. Notable minor gaps include no direct file deletion or transfer pause/resume, but these are workarounds.

Maintenance

ActivitySlowing
ResponsivenessNo issues