mal-mcp
Provides tools to access and analyze a user's MyAnimeList data including watch list, scores, episode progress, and public anime catalog.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@mal-mcpanalyze my anime taste"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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 storedNo 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'splain-PKCE flow today (see below) — the server can renew its own access token from a provisionedMAL_REFRESH_TOKENvia the standardrefresh_tokengrant: 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
fieldsparameter — there are no per-anime requests. 403/429 responses are retried with exponential backoff (1s/2s/4s).
Related MCP server: mal-mcp
Tools
Tool | Description |
| A page of the user's list (bounded; |
| Locally computed summary: status/score/genre/media-type/decade distributions, total episodes, estimated watch time, user-vs-community score deviation, top studios. |
| Public catalog search (compact results, truncated synopsis). |
| Full public detail incl. related anime, recommendations, statistics, and the user's own list entry if present. |
| 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. |
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
Go to https://myanimelist.net/apiconfig → Create ID.
App Type:
Web— this is what makes MAL issue a Client Secret (Android/iOS/Other types are public clients without a secret).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 ashttp://localhost:8080/callbackworks.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 |
|
Token URL |
|
PKCE |
|
Scopes | None (a token grants the full API surface for that user). |
Token usage |
|
Lifetimes | Docs say access = 1 hour, refresh = 1 month; in practice MAL returns |
⚠️ The
plain-PKCE constraint matters. Any OAuth client that hardcodesS256(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 |
|
Port |
|
Path |
|
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: multiUserthe 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 ownMAL_REFRESH_TOKEN, or use a Remote registration where each user sends their ownAuthorizationheader (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 PKCES256(verified innanobotandmcp-oauth-proxysources), while MAL supports onlyplain. Obot's built-in OAuth flow therefore cannot drive MAL directly today.
Working options, in order of practicality:
Self-renewing refresh token (recommended — set up once). Run the manual flow below ONCE and keep the
refresh_tokenfrom its output. Provision three env fields on the containerized server:MAL_REFRESH_TOKEN,MAL_CLIENT_ID, andMAL_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.Static access token (quick test). Set
MAL_ACCESS_TOKENinstead — 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-suppliedAuthorizationheader (Bearer <token>) works too and always takes precedence over env-based tokens.A bridging OAuth proxy in front of this server that speaks the MCP auth spec toward Obot and
plainPKCE toward MAL. Out of scope for this repository.Static OAuth, later. If Obot gains configurable/
plainPKCE (or MAL gainsS256+ metadata discovery), switch to Static OAuth with the MAL Client ID/Secret and callbackhttps://<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 |
|
| HTTP listen port |
|
| Bind address |
| (unset) | Enables self-renewing tokens: the server mints/renews access tokens via the |
| (unset) | MAL app Client ID, needed for the refresh grant. |
| (unset) | MAL app Client Secret — required for "Web"-type apps, omit for public clients. |
| (unset) | Static fallback access token (expires ~31 days). Used only when no |
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/inspectorIn 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:latestOr build locally:
docker build -t mal-mcp .
docker run --rm -p 8000:8000 mal-mcpThe 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 planThis server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Latest Blog Posts
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/UmutKDev/myanimelist-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server