Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 27 additions & 40 deletions AGENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,30 +33,24 @@ env:
- name: SEERR_MCP_AUTH_TOKEN
description: >-
Bearer token for MCP HTTP transport clients. When using HTTP transport,
at least one of SEERR_MCP_AUTH_TOKEN, SEERR_MCP_ROUTE_TOKEN, or
SEERR_MCP_NO_AUTH=true must be set. Not used for stdio transport.
at least one of SEERR_MCP_AUTH_TOKEN, SEERR_MCP_ALLOW_API_KEY_QUERY_PARAM,
or SEERR_MCP_NO_AUTH=true must be set. Not used for stdio transport.
required: false
- name: SEERR_MCP_ROUTE_TOKEN
- name: SEERR_MCP_NO_AUTH
description: >-
Secret path prefix that replaces Bearer auth for clients that cannot send
custom headers (e.g. claude.ai remote MCP). Endpoint becomes
http://<addr>/<token>/mcp.
Set to "true" to disable all MCP HTTP authentication. Only use in trusted
environments where the endpoint is access-controlled by other means.
required: false
- name: SEERR_MCP_NO_AUTH
- name: SEERR_MCP_ALLOW_API_KEY_QUERY_PARAM
description: >-
Set to "true" to disable all MCP HTTP authentication. Requires
SEERR_MCP_ROUTE_TOKEN to be set or explicit acknowledgement that the
endpoint is access-controlled by other means.
Set to "true" to accept the Seerr API key via the api_key query parameter
in addition to the X-Api-Key header. Useful for clients that cannot send
custom headers (e.g. claude.ai remote MCP). The MCP endpoint is always
/mcp; append ?api_key=<key> to authenticate. HTTP transport only.
required: false
- name: SEERR_MCP_CORS
description: Set to "true" to enable CORS headers for browser-based MCP clients (e.g. claude.ai)
required: false
- name: SEERR_MCP_MULTI_TENANT
description: >-
Set to "true" to enable multi-tenant mode (HTTP transport only). The
endpoint becomes /{seerr-api-token}/mcp and the path segment is used as
the per-user Seerr API key instead of SEERR_API_KEY.
required: false
- name: SEERR_MCP_TLS_CERT
description: Path to a TLS certificate file for HTTPS on the MCP HTTP server
required: false
Expand Down Expand Up @@ -109,22 +103,21 @@ docker run --rm \

MCP endpoint: `http://localhost:8811/mcp` — set `Authorization: Bearer your-secret-token` in your MCP client.

For clients that cannot send custom headers (e.g. claude.ai remote MCP), use a secret path prefix:
For clients that cannot send custom headers (e.g. claude.ai remote MCP), use query parameter transport:

```bash
docker run --rm \
-e SEERR_SERVER=http://your-seerr-instance:5055 \
-e SEERR_API_KEY=your-api-key \
-e SEERR_MCP_ROUTE_TOKEN=your-secret-path \
-e SEERR_MCP_NO_AUTH=true \
-e SEERR_MCP_ALLOW_API_KEY_QUERY_PARAM=true \
-e SEERR_MCP_CORS=true \
-p 8811:8811 \
ghcr.io/electather/seerr-cli:latest
```

MCP endpoint: `http://localhost:8811/your-secret-path/mcp` — no auth header required.
MCP endpoint: `http://localhost:8811/mcp?api_key=your-api-key` — no auth header required.

At least one of `SEERR_MCP_AUTH_TOKEN`, `SEERR_MCP_ROUTE_TOKEN`, or `SEERR_MCP_NO_AUTH=true` must be set for HTTP transport.
At least one of `SEERR_MCP_AUTH_TOKEN`, `SEERR_MCP_ALLOW_API_KEY_QUERY_PARAM`, or `SEERR_MCP_NO_AUTH=true` must be set for HTTP transport.

### docker-compose deployment

Expand Down Expand Up @@ -402,32 +395,26 @@ seerr-cli mcp serve --transport http --addr :8811 --auth-token mysecrettoken

Endpoint: `http://localhost:8811/mcp` — set `Authorization: Bearer mysecrettoken` in your client.

For clients that cannot send custom headers (e.g. claude.ai remote MCP), use a secret path prefix via `--route-token` (or `SEERR_MCP_ROUTE_TOKEN`):
For clients that cannot send custom headers (e.g. claude.ai remote MCP), use `--allow-api-key-query-param` (or `SEERR_MCP_ALLOW_API_KEY_QUERY_PARAM`):

```bash
# Add --cors if connecting from a browser-based client (e.g. claude.ai)
seerr-cli mcp serve --transport http --addr :8811 --route-token abc123 --no-auth --cors
# Endpoint becomes: http://localhost:8811/abc123/mcp
```

Both methods can be combined for defense in depth:

```bash
seerr-cli mcp serve --transport http --route-token abc123 --auth-token mysecrettoken
seerr-cli mcp serve --transport http --addr :8811 --allow-api-key-query-param --cors
# Endpoint: http://localhost:8811/mcp?api_key=YOUR_SEERR_API_KEY
```

All flags are configurable via environment variables:

| Flag | Environment variable | Default |
| --------------- | ----------------------- | ------- |
| `--transport` | `SEERR_MCP_TRANSPORT` | `stdio` |
| `--addr` | `SEERR_MCP_ADDR` | `:8811` |
| `--auth-token` | `SEERR_MCP_AUTH_TOKEN` | — |
| `--no-auth` | `SEERR_MCP_NO_AUTH` | `false` |
| `--route-token` | `SEERR_MCP_ROUTE_TOKEN` | |
| `--cors` | `SEERR_MCP_CORS` | `false` |
| `--tls-cert` | `SEERR_MCP_TLS_CERT` | — |
| `--tls-key` | `SEERR_MCP_TLS_KEY` | — |
| Flag | Environment variable | Default |
| ----------------------------- | ------------------------------------- | ------- |
| `--transport` | `SEERR_MCP_TRANSPORT` | `stdio` |
| `--addr` | `SEERR_MCP_ADDR` | `:8811` |
| `--auth-token` | `SEERR_MCP_AUTH_TOKEN` | — |
| `--no-auth` | `SEERR_MCP_NO_AUTH` | `false` |
| `--allow-api-key-query-param` | `SEERR_MCP_ALLOW_API_KEY_QUERY_PARAM` | `false` |
| `--cors` | `SEERR_MCP_CORS` | `false` |
| `--tls-cert` | `SEERR_MCP_TLS_CERT` | — |
| `--tls-key` | `SEERR_MCP_TLS_KEY` | — |

> Pass `--cors` (or `SEERR_MCP_CORS=true`) to enable CORS headers for browser-based clients (e.g. claude.ai). Disabled by default.

Expand Down
67 changes: 36 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -372,41 +372,48 @@ seerr-cli mcp serve --transport http --addr :8811 \
seerr-cli mcp serve --transport http --addr :8811 --no-auth
```

The MCP endpoint will be `http://localhost:8811/mcp`. Configure your client with `Authorization: Bearer mysecrettoken`.
The MCP endpoint is always `http://localhost:8811/mcp`. Configure your client with `Authorization: Bearer mysecrettoken`.

#### Secret path prefix (for clients that cannot send custom headers)
> **Note:** The HTTP transport does not implement OAuth 2.0 and is not compatible with clients that require OAuth. Use stdio for Claude Desktop.

#### API key via query parameter (opt-in)

Some MCP clients (e.g. claude.ai remote MCP integration) do not support custom `Authorization` headers. Use `--route-token` to embed a secret in the URL path instead:
For clients that cannot set custom headers, the Seerr API key can be passed via the `api_key` query parameter when `--allow-api-key-query-param` is enabled. The `X-Api-Key` header takes precedence when both are present; requests with neither are rejected.

```sh
# Endpoint becomes http://localhost:8811/abc123/mcp — no auth header needed
seerr-cli mcp serve --transport http --addr :8811 --route-token abc123 --no-auth
# Enable query parameter API key transport
seerr-cli mcp serve --transport http --allow-api-key-query-param

# Add --cors for browser-based clients (e.g. claude.ai)
seerr-cli mcp serve --transport http --addr :8811 --route-token abc123 --no-auth --cors

# Combine with Bearer auth for defense in depth
seerr-cli mcp serve --transport http --addr :8811 --route-token abc123 --auth-token mysecrettoken
seerr-cli mcp serve --transport http --allow-api-key-query-param --cors
```

> **Note:** A secret path is weaker than a proper Bearer token since it may appear in proxy logs. For production use, combine it with TLS.
MCP endpoint: `http://localhost:8811/mcp?api_key=YOUR_SEERR_API_KEY`

> **Note:** The HTTP transport does not implement OAuth 2.0 and is not compatible with clients that require OAuth. Use stdio for Claude Desktop.
> **Security note:** Query parameters may appear in proxy logs and browser history. Always serve over HTTPS when using query parameter transport.

#### Migration from `--route-token` or `--multi-tenant`

Both flags and their path-based routing have been removed. The MCP endpoint is now always `/mcp`. Clients that previously relied on these mechanisms should migrate to:

- **Header transport** — send the Seerr API key as `X-Api-Key: <key>` on each request.
- **Query parameter transport** — enable `--allow-api-key-query-param` and append `?api_key=<key>` to the `/mcp` URL.
- **Bearer token** — use `--auth-token` for MCP server access control (separate from the Seerr API key).

#### Environment variables

All `mcp serve` flags can be set via environment variables, which is especially useful for Docker deployments:

| Flag | Environment variable | Default |
| --------------- | ----------------------- | ------- |
| `--transport` | `SEERR_MCP_TRANSPORT` | `stdio` |
| `--addr` | `SEERR_MCP_ADDR` | `:8811` |
| `--auth-token` | `SEERR_MCP_AUTH_TOKEN` | — |
| `--no-auth` | `SEERR_MCP_NO_AUTH` | `false` |
| `--route-token` | `SEERR_MCP_ROUTE_TOKEN` | |
| `--cors` | `SEERR_MCP_CORS` | `false` |
| `--tls-cert` | `SEERR_MCP_TLS_CERT` | — |
| `--tls-key` | `SEERR_MCP_TLS_KEY` | — |
| Flag | Environment variable | Default |
| ----------------------------- | ------------------------------------- | ------- |
| `--transport` | `SEERR_MCP_TRANSPORT` | `stdio` |
| `--addr` | `SEERR_MCP_ADDR` | `:8811` |
| `--auth-token` | `SEERR_MCP_AUTH_TOKEN` | — |
| `--no-auth` | `SEERR_MCP_NO_AUTH` | `false` |
| `--allow-api-key-query-param` | `SEERR_MCP_ALLOW_API_KEY_QUERY_PARAM` | `false` |
| `--cors` | `SEERR_MCP_CORS` | `false` |
| `--tls-cert` | `SEERR_MCP_TLS_CERT` | — |
| `--tls-key` | `SEERR_MCP_TLS_KEY` | — |

### Docker (HTTP transport)

Expand All @@ -428,23 +435,22 @@ Configure your MCP client with:
- **URL:** `http://localhost:8811/mcp`
- **Authorization:** `Bearer mysecrettoken`

For clients that cannot send custom headers (e.g. claude.ai remote MCP), use a secret path prefix instead:
For clients that cannot send custom headers (e.g. claude.ai remote MCP), use query parameter transport instead:

```sh
docker run -d \
--name seerr-mcp \
-p 8811:8811 \
-e SEERR_SERVER=https://your-seerr-instance.com \
-e SEERR_API_KEY=your-api-key \
-e SEERR_MCP_ROUTE_TOKEN=abc123 \
-e SEERR_MCP_NO_AUTH=true \
-e SEERR_MCP_ALLOW_API_KEY_QUERY_PARAM=true \
-e SEERR_MCP_CORS=true \
ghcr.io/electather/seerr-cli:latest
```

Configure your MCP client with:

- **URL:** `http://localhost:8811/abc123/mcp`
- **URL:** `http://localhost:8811/mcp?api_key=your-api-key`

To bind to a different port or address, pass `--addr` explicitly:

Expand All @@ -460,20 +466,19 @@ docker run -d \

### Claude web (claude.ai)

Claude.ai connects to remote MCP servers over HTTPS. Since the browser cannot send custom `Authorization` headers to external MCP endpoints, the recommended approach is to embed a secret in the URL path using `--route-token` and expose the server via an HTTPS reverse proxy.
Claude.ai connects to remote MCP servers over HTTPS. Since the browser cannot send custom headers, use `--allow-api-key-query-param` and expose the server via an HTTPS reverse proxy.

#### 1. Start the MCP server

```sh
seerr-cli mcp serve \
--transport http \
--addr :8811 \
--route-token YOUR_SECRET_TOKEN \
--no-auth \
--allow-api-key-query-param \
--cors
```

The MCP endpoint will be `http://localhost:8811/YOUR_SECRET_TOKEN/mcp`.
The MCP endpoint will be `http://localhost:8811/mcp`.

#### 2. Expose via HTTPS with a reverse proxy

Expand Down Expand Up @@ -509,10 +514,10 @@ server {

1. Go to **claude.ai → Settings → Integrations**.
2. Click **Add integration**.
3. Enter the MCP URL: `https://mcp.example.com/YOUR_SECRET_TOKEN/mcp`
3. Enter the MCP URL: `https://mcp.example.com/mcp?api_key=YOUR_SEERR_API_KEY`
4. Save. The Seerr tools will appear in new conversations.

> **Security note:** The route token is the only secret protecting this endpoint. Use a long random value (e.g. `openssl rand -hex 32`) and always serve over HTTPS.
> **Security note:** The Seerr API key in the query string is the credential protecting this endpoint. Always serve over HTTPS and use a key with appropriate permissions.

#### Health check

Expand Down
12 changes: 3 additions & 9 deletions cmd/mcp/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,6 @@ var ServeFlags = []FlagDef{
IsBool: true,
Usage: "Disable authentication (insecure — must be explicit) (env: SEERR_MCP_NO_AUTH)",
},
{
Name: "route-token",
ViperKey: "mcp.route_token",
Default: "",
Usage: "Secret path prefix for the MCP endpoint (e.g. 'abc123' → /abc123/mcp) (env: SEERR_MCP_ROUTE_TOKEN)",
},
{
Name: "tls-cert",
ViperKey: "mcp.tls_cert",
Expand All @@ -75,11 +69,11 @@ var ServeFlags = []FlagDef{
Usage: "Enable CORS headers (required for browser-based clients such as claude.ai) (env: SEERR_MCP_CORS)",
},
{
Name: "multi-tenant",
ViperKey: "mcp.multi_tenant",
Name: "allow-api-key-query-param",
ViperKey: "mcp.allow_api_key_query_param",
Default: "false",
IsBool: true,
Usage: "Route /{seerr-api-token}/mcp for per-user API keys (HTTP transport only)",
Usage: "Accept the Seerr API key via the api_key query parameter in addition to the X-Api-Key header (HTTP transport only)",
},
{
Name: "log-file",
Expand Down
41 changes: 17 additions & 24 deletions cmd/mcp/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,35 +87,25 @@ func (r *statusRecorder) WriteHeader(code int) {
r.ResponseWriter.WriteHeader(code)
}

// SafeLogPath returns a redacted version of path that omits sensitive tokens.
//
// In route-token mode the raw token in the URL prefix is replaced with
// {redacted}. In multi-tenant mode the per-user API key that occupies the
// first path segment is replaced with {tenant}. Plain /mcp paths are returned
// unchanged.
func SafeLogPath(path, routeToken string, multiTenant bool) string {
if routeToken != "" {
prefix := "/" + routeToken
if strings.HasPrefix(path, prefix+"/") {
return "/{redacted}" + path[len(prefix):]
}
if path == prefix {
return "/{redacted}"
}
// SafeLogQuery returns a redacted version of a raw query string, replacing the
// value of the api_key parameter with {redacted} to prevent credential leakage
// in logs.
func SafeLogQuery(rawQuery string) string {
if rawQuery == "" {
return ""
}
if multiTenant {
trimmed := strings.TrimPrefix(path, "/")
idx := strings.Index(trimmed, "/")
if idx > 0 {
return "/{tenant}" + trimmed[idx:]
// Replace api_key=<value> with api_key={redacted}.
parts := strings.Split(rawQuery, "&")
for i, part := range parts {
if strings.HasPrefix(part, "api_key=") {
parts[i] = "api_key={redacted}"
}
}
return path
return strings.Join(parts, "&")
}

// httpLoggingMiddleware logs every HTTP request at Info level (Warn for 4xx/5xx).
// routeToken and multiTenant are used to redact sensitive tokens from the logged path.
func httpLoggingMiddleware(next http.Handler, routeToken string, multiTenant bool) http.Handler {
func httpLoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK}
Expand All @@ -124,11 +114,14 @@ func httpLoggingMiddleware(next http.Handler, routeToken string, multiTenant boo

args := []any{
"method", r.Method,
"path", SafeLogPath(r.URL.Path, routeToken, multiTenant),
"path", r.URL.Path,
"remote_addr", r.RemoteAddr,
"status", rec.status,
"duration_ms", duration.Milliseconds(),
}
if r.URL.RawQuery != "" {
args = append(args, "query", SafeLogQuery(r.URL.RawQuery))
}
if rec.status >= 400 {
mcpLog.Warn("http request", args...)
} else {
Expand Down
Loading
Loading