mal-mcp

mal-mcp

Exposes a user's MyAnimeList data (watch list, scores, statistics) as MCP tools for AI assistants to analyze taste, build statistics, and make recommendations.

Category
访问服务器

README

mal-mcp

A stateless MCP (Model Context Protocol) server that exposes a user's personal MyAnimeList data — watch list, scores, watch status, episode progress — as MCP tools, so an AI assistant (e.g. Claude) can analyze taste, build statistics, and make recommendations.

Built with Python 3.12, FastMCP 3 (streamable-http transport) and httpx. Designed to run as a container behind an Obot MCP gateway.

Architecture

MCP client (Claude) ──> Obot gateway ──> mal-mcp (this server) ──> MAL API v2
                         │                │
                         │ OAuth flow,    │ reads Authorization: Bearer <token>
                         │ token storage/ │ from each request and forwards it
                         │ refresh        │ to api.myanimelist.net — nothing stored
  • No OAuth login flow in this server. The gateway performs the interactive OAuth flow (PKCE, callback, token exchange) and forwards Authorization: Bearer <MAL access token> with every MCP request. Alternatively — because Obot cannot drive MAL's plain-PKCE flow today (see below) — the server can renew its own access token from a provisioned MAL_REFRESH_TOKEN via the standard refresh_token grant: a single POST, no login flow, tokens held in memory only.
  • Stateless. Each tool call resolves the token (request header first), uses it for the MAL API calls of that one invocation, and persists nothing to disk. No sessions are kept (stateless_http=True), so replicas can scale freely.
  • Rate-limit friendly. The MAL rate limit is undocumented (community practice: ~1 req/s; abuse surfaces as HTTP 403 "DoS detected"). Every list-based tool fetches the user's whole library in a single paginated pass with an explicit fields parameter — there are no per-anime requests. 403/429 responses are retried with exponential backoff (1s/2s/4s).

Tools

Anime

Tool Description
get_my_anime_list(status_filter?, sort?, limit=100, offset=0) A page of the user's list (bounded; has_more + offset for paging): title, watch status, score, episode progress, genres, community mean, studios.
get_user_stats() Locally computed summary: status/score/genre/media-type/decade distributions, total episodes, estimated watch time, user-vs-community score deviation, top studios.
search_anime(query, limit=10) Public catalog search (compact results, truncated synopsis).
get_anime_detail(anime_id) Full public detail incl. related anime, recommendations, statistics, and the user's own list entry if present.
analyze_taste() Token-efficient raw export of the whole list (grouped by status, sorted by score) for the calling model to analyze — this tool itself performs no analysis.

Manga

Tool Description
search_manga(query, limit=10) Public manga catalog search (chapters/volumes, authors, genres).
get_manga_detail(manga_id) Full manga detail incl. authors, serialization magazines, related works, recommendations, and the user's own entry if present.
get_my_manga_list(status_filter?, sort?, limit=100, offset=0) A page of the user's manga list with chapter/volume progress.

Discovery

Tool Description
get_anime_ranking(ranking_type, limit=25) MAL's official rankings: all, airing, upcoming, tv, ova, movie, special, bypopularity, favorite.
get_manga_ranking(ranking_type, limit=25) Manga rankings: all, manga, novels, oneshots, doujin, manhwa, manhua, bypopularity, favorite.
get_seasonal_anime(year, season, sort?, limit=25) Anime of one broadcast season (winter/spring/summer/fall).
get_suggested_anime(limit=25) MAL's personalized suggestions for the authenticated user.

Users

Tool Description
get_my_profile() The authenticated user's profile + lifetime anime statistics. MAL exposes this only for @me.
get_user_anime_list(user_name, ...) Another user's public anime list (403 usually = private list or unknown user).
get_user_manga_list(user_name, ...) Another user's public manga list.

Write tools — these modify the user's MAL list

Tool Description
update_my_anime_entry(anime_id, ...) Update score/status/episode progress/tags of a list entry — or add the anime to the list (MAL creates the entry if absent). Only provided fields change.
delete_my_anime_entry(anime_id) Permanently remove an anime from the list (score/progress/tags are lost; cannot be undone).
update_my_manga_entry(manga_id, ...) Same as the anime variant, with chapter/volume progress.
delete_my_manga_entry(manga_id) Permanently remove a manga from the list.

Aggregate tools (get_user_stats, analyze_taste) fetch the entire list in one paginated pass (safety cap: 20,000 entries — beyond that a truncated/WARNING marker is included). paging.next URLs are validated (https + api.myanimelist.net) before being followed, so the bearer token can never be sent elsewhere.

All tools are read-only. Facts about the MAL API this server relies on (fields syntax, pagination, limits, error shapes) are documented in NOTES.md.

MAL application registration

  1. Go to https://myanimelist.net/apiconfigCreate ID.
  2. App Type: Web — this is what makes MAL issue a Client Secret (Android/iOS/Other types are public clients without a secret).
  3. App Redirect URL: the callback of whatever performs the OAuth flow. For Obot's built-in flow that is https://<your-obot-host>/oauth/mcp/callback; if you obtain tokens manually (see below), a localhost URL such as http://localhost:8080/callback works.
  4. Note the Client ID (32 chars) and Client Secret (64 chars). Never bake them into this server or its image — they belong to the gateway/flow side only.

MAL OAuth2 reference (for the gateway configuration)

Item Value
Authorize URL https://myanimelist.net/v1/oauth2/authorize
Token URL https://myanimelist.net/v1/oauth2/token
PKCE plain only — MAL does not support S256. code_challenge must equal code_verifier (43–128 chars).
Scopes None (a token grants the full API surface for that user).
Token usage Authorization: Bearer <access_token>
Lifetimes Docs say access = 1 hour, refresh = 1 month; in practice MAL returns expires_in=2678400 (31 days) for access tokens.

⚠️ The plain-PKCE constraint matters. Any OAuth client that hardcodes S256 (most modern ones, including Obot today — see below) cannot complete MAL's flow.

Deploying behind Obot

Register the server (Containerized runtime)

Build and push the image (see Docker below), then in the Obot admin UI: MCP Management → MCP Servers → Add MCP Server, runtime Containerized:

Field Value
Image ghcr.io/umutkdev/myanimelist-mcp:latest
Port 8000
Path /mcp

In the same form, add environment fields for the token setup you chose (see "Getting the token to the server" below) — recommended: MAL_REFRESH_TOKEN, MAL_CLIENT_ID, MAL_CLIENT_SECRET (all sensitive).

Or as a catalog entry:

name: MyAnimeList
serverUserType: multiUser
runtime: containerized
containerizedConfig:
  image: ghcr.io/umutkdev/myanimelist-mcp:latest
  port: 8000
  path: /mcp
env:
  - key: MAL_REFRESH_TOKEN
    name: MAL Refresh Token
    required: true
    sensitive: true
    description: MyAnimeList OAuth refresh token (obtained once; see README)
  - key: MAL_CLIENT_ID
    name: MAL Client ID
    required: true
    sensitive: false
    description: MyAnimeList API app Client ID
  - key: MAL_CLIENT_SECRET
    name: MAL Client Secret
    required: false
    sensitive: true
    description: MyAnimeList API app Client Secret (Web app type only)

⚠️ Env-provisioned tokens mean ONE shared MAL account. With serverUserType: multiUser the admin configures the env values once and every user of this registration talks to the token owner's private MAL list (scores, watch history, plan_to_watch/dropped) and shares that account's rate limit. Use env tokens only for a personal / single-operator gateway. For a multi-person gateway, register the server as single-user so each user supplies their own MAL_REFRESH_TOKEN, or use a Remote registration where each user sends their own Authorization header (which always takes precedence over env tokens).

Getting the token to the server — current reality (read this)

This server just needs Authorization: Bearer <MAL access token> on each request; it does not care who put it there. As of Obot v0.23.x there is a real incompatibility to be aware of:

  • Obot's OAuth support ("Static OAuth") takes only a Client ID/Secret and discovers authorize/token endpoints via the MCP auth spec (401 + WWW-Authenticate → RFC 9728 → RFC 8414). MAL publishes no such metadata, and Obot's OAuth client hardcodes PKCE S256 (verified in nanobot and mcp-oauth-proxy sources), while MAL supports only plain. Obot's built-in OAuth flow therefore cannot drive MAL directly today.

Working options, in order of practicality:

  1. Self-renewing refresh token (recommended — set up once). Run the manual flow below ONCE and keep the refresh_token from its output. Provision three env fields on the containerized server: MAL_REFRESH_TOKEN, MAL_CLIENT_ID, and MAL_CLIENT_SECRET (omit the secret for non-Web public clients). The server then mints and renews access tokens itself before they expire — no monthly re-pasting. Rotated tokens live in memory only; MAL keeps previously issued refresh tokens valid after rotation (verified empirically), so the env value keeps working across container restarts.
  2. Static access token (quick test). Set MAL_ACCESS_TOKEN instead — simplest possible wiring, but MAL access tokens last ~31 days in practice, after which you must paste a fresh one. For a Remote registration, a user-supplied Authorization header (Bearer <token>) works too and always takes precedence over env-based tokens.
  3. A bridging OAuth proxy in front of this server that speaks the MCP auth spec toward Obot and plain PKCE toward MAL. Out of scope for this repository.
  4. Static OAuth, later. If Obot gains configurable/plain PKCE (or MAL gains S256 + metadata discovery), switch to Static OAuth with the MAL Client ID/Secret and callback https://<obot-host>/oauth/mcp/callback — no changes needed in this server.

Obtaining a MAL access token manually (documentation only — not part of the server)

Because MAL uses plain PKCE, the verifier and challenge are the same string:

# 1) Generate a code verifier (43-128 chars)
VERIFIER=$(python3 -c "import secrets; print(secrets.token_urlsafe(64)[:100])")

# 2) Open this in a browser, log in, and approve (redirect_uri is required in practice —
#    MAL can answer "400 Bad Request" when it is omitted, even with a single registered URL;
#    the value must exactly match the App Redirect URL and be URL-encoded):
#    https://myanimelist.net/v1/oauth2/authorize?response_type=code&client_id=<CLIENT_ID>&code_challenge=$VERIFIER&code_challenge_method=plain&state=x&redirect_uri=<URL_ENCODED_REDIRECT_URL>
#    You'll be redirected to your registered redirect URL with ?code=<CODE>

# 3) Exchange the code for tokens:
curl -s https://myanimelist.net/v1/oauth2/token \
  -d client_id=<CLIENT_ID> -d client_secret=<CLIENT_SECRET> \
  -d grant_type=authorization_code -d code=<CODE> \
  -d code_verifier=$VERIFIER -d redirect_uri=<REDIRECT_URL>
# → {"token_type":"Bearer","expires_in":2678400,"access_token":"...","refresh_token":"..."}

Keep the refresh_token — that is what goes into MAL_REFRESH_TOKEN for the set-up-once option; the access_token is what you'd use for the static/header options.

Running locally

uv sync                          # install dependencies
uv run pytest                    # unit tests (pure helpers, no network)
uv run python -m mal_mcp.server  # serves http://0.0.0.0:8000/mcp (streamable-http)

Environment variables

Variable Default Purpose
PORT 8000 HTTP listen port
HOST 0.0.0.0 Bind address
MAL_REFRESH_TOKEN (unset) Enables self-renewing tokens: the server mints/renews access tokens via the refresh_token grant (requires MAL_CLIENT_ID). In-memory only.
MAL_CLIENT_ID (unset) MAL app Client ID, needed for the refresh grant.
MAL_CLIENT_SECRET (unset) MAL app Client Secret — required for "Web"-type apps, omit for public clients.
MAL_ACCESS_TOKEN (unset) Static fallback access token (expires ~31 days). Used only when no Authorization header arrives and no refresh setup exists.

Token precedence per request: Authorization header → refresh-token manager → MAL_ACCESS_TOKEN. The server never writes any of these anywhere.

Test with MCP Inspector

npx @modelcontextprotocol/inspector

In the Inspector UI: transport Streamable HTTP, URL http://localhost:8000/mcp, and add a custom header Authorization: Bearer <your MAL access token>. tools/list should show the five tools; get_my_anime_list / get_user_stats return your real data.

Quick smoke test with curl

# List tools (stateless mode: no session handshake needed)
curl -s -X POST http://localhost:8000/mcp \
  -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

# Call a tool with your token
curl -s -X POST http://localhost:8000/mcp \
  -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
  -H "Authorization: Bearer $MAL_TOKEN" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_user_stats","arguments":{}}}'

Calls without an Authorization header return an actionable error; an expired/invalid token surfaces MAL's 401 as "MAL rejected the access token…".

Docker

Prebuilt multi-arch image:

docker run --rm -p 8000:8000 ghcr.io/umutkdev/myanimelist-mcp:latest

Or build locally:

docker build -t mal-mcp .
docker run --rm -p 8000:8000 mal-mcp

The image is python:3.12-slim + uv, runs as a non-root user, exposes port 8000, and serves the MCP endpoint at /mcp.

Project layout

src/mal_mcp/
├── server.py       # FastMCP app, bearer-token helper, 5 tools, stats/format helpers
└── mal_client.py   # MAL API wrapper: fields, pagination (paging.next), retries, error mapping
tests/test_stats.py # unit tests for the pure helpers
NOTES.md            # verified MAL API / FastMCP / Obot facts (source for the choices above)
PLAN.md             # design plan

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选