HoYoLAB Genshin Battle Chronicle MCP Server
Provides read-only access to a Genshin Impact Battle Chronicle via HoYoLAB, with tools for linked game accounts, account overview statistics, character rosters, equipped artifacts, Spiral Abyss records, Imaginarium Theater progress, and exploration progress.
Click on "Deploy 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., "@HoYoLAB Genshin Battle Chronicle MCP ServerWhat are my Abyss stars this rotation?"
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.
HoYoLAB Genshin Battle Chronicle — MCP server
Read-only access to a Genshin Impact Battle Chronicle, exposed as MCP tools so a model can answer questions like "what are my Abyss stars this rotation?" or "which regions am I least finished with?".
Genshin Impact only — Star Rail, ZZZ and Honkai Impact 3rd are named by
list_game_accounts but have no tools here.
Works with Open WebUI, Claude Code, Codex, and opencode.
cp .env.example .env
printf 'HOYOLAB_MCP_SECRET=%s\n' "$(openssl rand -hex 32)" >> .env
docker compose up -d --build
curl -s http://localhost:8899/health # {"status":"ok"}Unofficial. This project is not affiliated with, endorsed by, or supported by HoYoverse. It calls the same internal web endpoints the Battle Chronicle page uses. Those endpoints are undocumented and can change or be restricted at any time.
Handle credentials carefully. Access requires HoYoLAB session cookies, which are equivalent to being logged into the account — not a read-only token. Use a secondary account if you are unsure, and be aware that using unofficial clients may conflict with HoYoverse's Terms of Service. You accept that risk by using this software.
Contents
Related MCP server: steam-mcp
Why this exists
The page at act.hoyolab.com/app/community-game-records-sea/ is a web app, not an API.
To let a model read that data it has to be turned into tools, which is what this server
does: it calls the same endpoints the page calls and returns condensed JSON.
Requirements
Python | 3.10+ (only for the non-container path) |
Container | Docker with Compose v2, or Podman 4+ with |
Client | Any MCP client that speaks Streamable HTTP |
HoYoLAB | A logged-in account with Battle Chronicle visibility enabled |
No HoYoLAB developer account, API key, or OAuth application is needed — and none exists to be obtained (see the next section).
Quick start
Docker or Podman, both work:
git clone https://github.com/danisec/hoyolab-mcp.git
cd hoyolab-mcp
cp .env.example .env
printf 'HOYOLAB_MCP_SECRET=%s\n' "$(openssl rand -hex 32)" >> .env
docker compose up -d --build # or: podman compose up -d
curl -s http://localhost:8899/healthThen connect a client — see Connecting a client. For a command-line client that is two commands:
docker compose exec hoyolab-mcp python -m hoyolab_mcp.cli link-url --user alice
docker compose exec hoyolab-mcp python -m hoyolab_mcp.cli issue-token --user aliceAuthentication: cookies, not OAuth
The Battle Chronicle API does not use OAuth. It authenticates with HoYoLAB session
cookies (ltuid_v2 / ltoken_v2 / ltmid_v2, and the legacy ltuid / ltoken).
Open WebUI's MCP support does speak OAuth 2.1, but that authenticates Open WebUI to an MCP server; it cannot mint a HoYoLAB credential. HoYoverse publishes no OAuth authorization server for third-party Battle Chronicle access — its standard discovery endpoints do not exist:
account.hoyoverse.com/.well-known/openid-configuration -> HTTP 403
api-account-os.hoyoverse.com/.well-known/openid-configuration -> HTTP 404
bbs-api-os.hoyolab.com/.well-known/oauth-authorization-server -> HTTP 404So cookies are the only mechanism. They are handled as follows.
Per-user, encrypted, never logged
Cookies are stored per user id, in SQLite, encrypted with AES-GCM. The user id is bound as associated data, so a ciphertext copied onto another user's row fails to decrypt instead of leaking an account.
API tokens (for command-line clients) are stored as SHA-256 hashes, never in plaintext, and each maps to exactly one user id.
There is no server-wide fallback credential. A fallback would mean that as soon as one admin linked an account, every other user of the tools would silently read that person's Genshin data.
Values never reach logs or error messages.
redact()keeps a 4-character prefix; the link page logs cookie names only.The server fails closed without
HOYOLAB_MCP_SECRET: it starts, but every path that would touch credentials returns an error instead./healthanswers503with the reason, and every tool call fails with the same message. Nothing is ever written unencrypted. Compose additionally refuses to start at all (see Option A — Compose), so the usual path fails earlier and more loudly.
Linking an account
The link page is at /link. A user reaches it from the tool message, which includes a
signed, 15-minute token identifying their user id. A raw user id in the URL would let
anyone attach cookies to anyone else's account, so the id is HMAC-signed.
For command-line clients the same URL is printed by
python -m hoyolab_mcp.cli link-url --user <id>, so linking does not require a browser
session with Open WebUI.
Three identity sources, and why unsigned headers are refused
The server resolves the caller in this order:
Authorization: Bearer <token>— a token minted per user id. Used by Claude Code, Codex and opencode. Verified against a stored SHA-256 hash; a revoked token stops working immediately. A bad token is an error, never a reason to fall back to something weaker.X-OpenWebUI-User-Jwt— the signed HS256 token Open WebUI forwards (iss=open-webui), verified with the shared secret.X-OpenWebUI-User-Id— the plain header, honoured only when no JWT secret is configured.
That last condition is a security boundary, not a convenience. If a JWT secret is configured, the unsigned header is ignored outright: honouring it would let anything that can reach the port name itself any user and read that person's Genshin data. The error message says so explicitly, because otherwise it looks like a broken server.
Tools
Tool | Returns |
| Whether this user has linked an account (never the cookie) |
| Game accounts and UIDs linked to the HoYoLAB login |
| All 22 account statistics (see below) + exploration summary |
| Roster: name, level, rarity, element, constellation |
| Equipped artifacts + final stat totals (base/bonus/total) |
| Stars per floor/chamber and the teams used |
| Per-period act, medals, coins, optional round roster |
| Per-region exploration %, incomplete regions, offerings |
All are read-only. Nothing here can modify an account.
get_genshin_overview returns every statistic the page shows
The API's stats object carries 22 top-level fields. Two of them are nested objects,
which is easy to miss — they are why Imaginarium Theater and Stygian Onslaught were once
reported as "not available from the API" when they were present all along:
Page label | API field |
Imaginarium Theater |
|
Stygian Onslaught |
|
Everything else maps one-to-one: achievements, days active, characters, max-friendship
characters, Spiral Abyss, all eight oculi (iceculus_number is Cryoculus,
moonoculus_number is Lunoculus), waypoints, domains, and all five chest tiers
(magic_chest_number is Remarkable Chest).
Why artifacts need their own tool
character/list returns only id, name, level, rarity, element, weapon type and weapon —
no artifacts at all. Relic data lives behind character/detail, which also returns a
property_map that translates numeric property_type codes into readable stat names.
Without that map the stats are a wall of integers, so get_character_artifacts joins the
two. It also distinguishes flat HP (type 2) from HP Percentage (type 3), which both
report the name "HP" on their own, and keeps each substat's roll count so an invested
piece is distinguishable from a fresh one.
Failure modes are specific
HoYoLAB answers HTTP 200 with a retcode even when it refuses, so errors are read from
the body, not the status code:
retcode | Meaning surfaced to the user |
| Credential missing, expired, or for another account |
| Credential does not match this game UID |
| Player disabled their Battle Chronicle — a privacy setting, not a login problem |
| Game account not found |
10102 in particular is called out separately, because telling someone to re-link when
the real fix is an in-game toggle sends them down the wrong path.
Installation
Three paths: Compose (recommended), plain docker run, or a local Python install. A fourth
option covers a long-running Podman host that should supervise the service with systemd
(deploy.sh, Option D).
Option A — Compose (recommended)
Works with Docker Compose and Podman Compose; the same compose.yaml is used by
both.
git clone https://github.com/danisec/hoyolab-mcp.git
cd hoyolab-mcp
cp .env.example .env
printf 'HOYOLAB_MCP_SECRET=%s\n' "$(openssl rand -hex 32)" >> .env
docker compose up -d --build # or: podman compose up -d
curl -s http://localhost:8899/health # {"status":"ok"}Without the docker CLI, podman-compose works directly:
podman-compose up -d --buildBoth were tested against this file: the container reports healthy, /health answers,
and the MCP handshake returns all 8 tools.
If HOYOLAB_MCP_SECRET is missing or empty, Compose refuses to start with a message
naming the variable, so this is caught before a container exists. Run without Compose and
the server itself still refuses to serve: /health returns 503 and every tool call
returns an error, rather than writing cookies unencrypted.
Common operations:
docker compose logs -f # follow logs
docker compose ps # status + health
docker compose down # stop, keep the credential database
docker compose down -v # stop and DELETE the database (all linked accounts)
docker compose exec hoyolab-mcp python -m hoyolab_mcp.cli status --user aliceWith Podman, substitute podman compose or podman-compose for docker compose.
What the Compose file sets up
Concern | Choice | Why |
State | named volume | holds the encrypted cookie database; a bind mount would leave it easy to copy or commit, and needs a manual |
Network exposure |
| the server has no TLS, so it binds to loopback by default |
Root filesystem |
| only |
Capabilities |
| the image already runs as uid 10001 |
Restart |
| survives reboot and crashes |
To run a published image instead of building, set HOYOLAB_MCP_IMAGE in .env:
HOYOLAB_MCP_IMAGE=ghcr.io/you/hoyolab-mcp:1.0.0
docker compose up -d # no --buildIf compose build fails with a metadata-file error
On a snap-confined Docker, docker compose build can fail with:
open /tmp/.tmp-compose-build-metadataFile-*.json: no such file or directoryThat is a BuildKit/snap interaction, not this project — it reproduces with a two-line
FROM alpine project. Build with BuildKit disabled, or build the image directly:
DOCKER_BUILDKIT=0 docker compose up -d --build
# or
docker build -t hoyolab-mcp:latest . && docker compose up -dIf containers exit with operation not permitted
If security_opt: [no-new-privileges:true] is added and every container then fails with
exec ...: operation not permitted — including alpine sleep — the host is blocking it
(e.g. snap-confined Docker). This file therefore leaves that option off; read_only and
cap_drop: ALL are still applied. Re-enable it on a host that supports it.
Option B — plain docker run
Useful without Compose, or when embedding this in another setup.
git clone https://github.com/danisec/hoyolab-mcp.git
cd hoyolab-mcp
cp .env.example .env
printf 'HOYOLAB_MCP_SECRET=%s\n' "$(openssl rand -hex 32)" >> .env
docker build -t hoyolab-mcp .
docker run -d --name hoyolab-mcp \
-p 127.0.0.1:8899:8899 \
--env-file .env \
-v hoyolab-data:/data \
--read-only --tmpfs /tmp --cap-drop ALL \
hoyolab-mcp--env-file .env passes every variable in the file, including HOYOLAB_MCP_DB and
HOYOLAB_MCP_HOST, which compose.yaml sets explicitly.
Option C — local Python
git clone https://github.com/danisec/hoyolab-mcp.git
cd hoyolab-mcp
python -m venv .venv && .venv/bin/pip install -r requirements.txt
export HOYOLAB_MCP_SECRET="$(openssl rand -hex 32)"
export HOYOLAB_MCP_DB="$PWD/hoyolab.db"
export HOYOLAB_MCP_PUBLIC_URL="http://localhost:8899"
.venv/bin/python -m hoyolab_mcp.mainRequires Python 3.10 or newer (the code uses X | Y type syntax and dict[str, str]).
Option D — Podman Quadlet (deploy.sh)
For a long-running Podman host, deploy.sh installs the server as a systemd user
service instead of a Compose container. A Quadlet is turned into a real systemd unit by
podman-user-generator, so it starts on boot, restarts on failure, and needs no login
session (given loginctl enable-linger "$USER"). Compose has no supervision of its own:
after a reboot, nothing brings the container back.
git clone https://github.com/danisec/hoyolab-mcp.git
cd hoyolab-mcp
./deploy.sh # init + build + install, then wait for health
./deploy.sh status # unit, container, health, and link state
./deploy.sh logs # follow the service log
./deploy.sh backup # consistent copy of the credential databaseConfiguration lives in ~/.config/hoyolab-mcp/hoyolab-mcp.env (mode 600), outside the
repository, so a git clean, a re-clone, or a compose down -v cannot destroy the key
that makes already-stored cookies readable. The Quadlet itself is generated from that file
by ./deploy.sh quadlet, so the published port and bind addresses always match the env
file — there is no second copy to drift out of sync.
Set these before ./deploy.sh init to control what it writes:
HOYOLAB_MCP_PORT_HOST=8899 # host port
HOYOLAB_MCP_BIND=127.0.0.1,100.64.0.5 # publish on loopback AND one more address
HOYOLAB_MCP_OWUI_CONTAINER=open-webui # container to adopt the JWT secret fromHOYOLAB_MCP_BIND accepts a comma-separated list. Each address becomes its own
PublishPort line, which is narrower than binding 0.0.0.0: the port stays closed on
every interface not listed. Any non-loopback address is also added to
HOYOLAB_MCP_ALLOWED_HOSTS and used as the host in HOYOLAB_MCP_PUBLIC_URL, because the
DNS-rebinding check compares the full Host header and the link is opened in the user's
browser.
./deploy.sh init adopts FORWARD_USER_INFO_HEADER_JWT_SECRET from the running
Open WebUI container rather than generating one. This matters in both directions: when
this server has the secret, an unsigned X-OpenWebUI-User-Id header is rejected outright,
and when it does not, identity falls back to a header anything reaching the port can
spoof. The two processes have to agree on the same value, and a generated value never
would. Re-running init is safe: it reports a match and rewrites nothing.
Do not also run podman compose up for this project. Both would try to own a container
named hoyolab-mcp and would fight over the port and the volume.
Verified on a Podman host: the generated Quadlet is accepted by podman quadlet -dryrun
(exit 0, no parser errors, the expected PublishPort line), and the adopted secret matches
Open WebUI byte for byte. The status report was exercised against three cases — this
server's real port, a port belonging to another service, and a closed port — and refuses to
print a foreign HTML page as if it were health output.
Not exercised end to end: build and install (they need a free port and would restart the
live service). They are the same podman build and systemctl --user restart steps the
Compose and docker run paths already use.
Configuration
HOYOLAB_MCP_SECRET is not optional, at least 16 characters. Without it the server still
starts but refuses to do anything with credentials: /health returns 503 and tool calls
return the same explanation, so no cookie is ever written unencrypted. Changing the secret
later makes already-stored cookies unreadable (users simply re-link).
HOYOLAB_MCP_PUBLIC_URL is used to build the account-link URL, so it must be reachable
from the user's browser, not just from the container. Behind a reverse proxy:
HOYOLAB_MCP_PUBLIC_URL=https://hoyolab-mcp.example.com
HOYOLAB_MCP_ALLOWED_HOSTS=hoyolab-mcp.example.com,hoyolab-mcp:8899,localhost:8899HOYOLAB_MCP_ALLOWED_HOSTS must contain the exact Host header clients send. A
missing entry is rejected with 421 Misdirected Request, which looks like a broken
server but is MCP's DNS-rebinding protection doing its job.
Every variable, with its default:
Variable | Default | Purpose |
| — (required) | AES-GCM key for stored cookies; ≥16 chars |
| empty | Verify Open WebUI's forwarded identity; must match Open WebUI |
| empty (Compose sets | Base URL for the account-link message; must be browser-reachable |
| loopback + container names | Hosts accepted by the DNS-rebinding check |
|
| Host interface to publish on; |
|
| Host port (Compose only); the container always uses 8899 |
|
| Image to run |
|
| Reported by |
|
| Database path inside the container |
|
| Python log level |
GET /health returns {"status":"ok","version":"..."}, which is the quickest way to
confirm which build a running container is actually using.
Connecting a client
There are two kinds of client, and they authenticate differently. This matters more than it looks: the server never holds a shared credential, so each client must be able to say which user it is.
Client | How identity reaches the server |
Open WebUI | Open WebUI signs and forwards a per-user header on every tool call |
Claude Code / Codex / opencode | A bearer API token you mint for that user |
Open WebUI
In Open WebUI: Settings → Admin → Integrations → External Tool Servers → +,
type MCP (Streamable HTTP), URL http://<host>:8899/mcp, auth None.
Auth is None because identity travels in the forwarded header, not an Authorization
header. Two Open WebUI settings are required:
ENABLE_FORWARD_USER_INFO_HEADERS=true
FORWARD_USER_INFO_HEADER_JWT_SECRET=<a long random string> # strongly recommendedFORWARD_USER_INFO_HEADER_JWT_SECRET must be the same value as this server's
FORWARD_USER_INFO_HEADER_JWT_SECRET. Without it Open WebUI falls back to an unsigned
X-OpenWebUI-User-Id header that anything reaching the port could forge.
There is deliberately no "Connect" button in Open WebUI for this server. Open WebUI
only shows one for OAuth-backed MCP servers, and HoYoverse publishes no OAuth
authorization server — so accounts are linked through the /link page instead, which
the tool message links to.
Claude Code, Codex, opencode
These clients do not send Open WebUI's identity header, so they need a token. A token maps to exactly one user id, and that user id's cookies are the only ones ever used — the per-user isolation guarantee is unchanged.
1. Choose a user id and link a HoYoLAB account
A "user id" here is just a stable name, not necessarily an Open WebUI user id. Pick one per person (or per machine):
# Print a 15-minute link URL, then open it in a browser and paste your cookies
docker compose exec hoyolab-mcp python -m hoyolab_mcp.cli link-url --user alice
# Confirm it worked
docker compose exec hoyolab-mcp python -m hoyolab_mcp.cli status --user alice
# user alice: linked (cookies: ltmid_v2, ltoken_v2, ltuid_v2)docker compose exec picks the right container by service name, so it keeps working if
the container is renamed. With Podman use podman compose exec, or plain
podman exec hoyolab-mcp ....
Without a container at all, drop the prefix and run
.venv/bin/python -m hoyolab_mcp.cli ... from the project directory.
2. Mint a token
docker compose exec hoyolab-mcp python -m hoyolab_mcp.cli issue-token \
--user alice --label "claude-code on laptop"
# hlt_1J2x... <- shown once, never recoverableTokens are stored as SHA-256 hashes, so the database cannot be used to recover a working token. If you lose one, revoke it and mint another.
3. Configure the client
Claude Code — one command, or a JSON file:
claude mcp add --transport http hoyolab http://localhost:8899/mcp \
--header "Authorization: Bearer hlt_your_token_here"Or in .mcp.json (project) / ~/.claude.json (user):
{
"mcpServers": {
"hoyolab": {
"type": "http",
"url": "http://localhost:8899/mcp",
"headers": { "Authorization": "Bearer hlt_your_token_here" }
}
}
}The "type": "http" field is required — an entry with a url but no type is read as
a stdio server and skipped.
Codex — in ~/.codex/config.toml:
[mcp_servers.hoyolab]
url = "http://localhost:8899/mcp"
http_headers = { Authorization = "Bearer hlt_your_token_here" }Codex reads environment variables too, which keeps the token out of the file:
[mcp_servers.hoyolab]
url = "http://localhost:8899/mcp"
bearer_token_env_var = "HOYOLAB_MCP_TOKEN"export HOYOLAB_MCP_TOKEN=hlt_your_token_hereopencode — in opencode.json (project) or ~/.config/opencode/opencode.json:
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"hoyolab": {
"type": "remote",
"url": "http://localhost:8899/mcp",
"enabled": true,
"headers": { "Authorization": "Bearer hlt_your_token_here" }
}
}
}4. Verify
# Ask the tool to confirm which account it sees
curl -s http://localhost:8899/mcp \
-H "Authorization: Bearer hlt_your_token_here" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"hoyolab_link_status","arguments":{}}}'Then in the client itself: "Use the hoyolab tools to tell me my Spiral Abyss stars."
Managing tokens
docker compose exec hoyolab-mcp python -m hoyolab_mcp.cli list-tokens --user alice
docker compose exec hoyolab-mcp python -m hoyolab_mcp.cli revoke-token --user alice --id 54a5448353b4
docker compose exec hoyolab-mcp python -m hoyolab_mcp.cli revoke-all --user alice
docker compose exec hoyolab-mcp python -m hoyolab_mcp.cli unlink --user aliceRevoking is immediate — the next tool call with that token is refused. There is no HTTP route for issuing tokens on purpose: issuing a token turns a user id into a credential, so it stays a local command requiring filesystem access to the database.
unlink deletes the stored cookies and revokes that user's tokens; see
Removing your data.
What the model sees
The server tells the model its own usage rules through MCP instructions, so no client
needs a custom prompt. If a user has not linked an account, every tool returns a message
containing their personal link URL rather than an empty result — the model will surface
it.
Usage
Once connected, ask in natural language. The model picks the tool.
Ask | Tool used |
"Do I have a HoYoLAB account linked?" |
|
"Which Genshin accounts do I have?" |
|
"How many achievements and chests do I have?" |
|
"What are my Spiral Abyss stars this rotation?" |
|
"How did I do in Imaginarium Theater?" |
|
"Which regions are least explored?" |
|
"Show my Furina's artifacts and total CRIT" |
|
"List my characters by level" |
|
get_character_artifacts answers build questions from your own account's numbers
rather than a wiki, which is why it exists separately from get_characters.
Upgrading
The stored credential database is forward compatible: new versions read databases written by older ones without migration, so upgrading is replacing the image and restarting.
# If you run a published image (HOYOLAB_MCP_IMAGE points at a registry):
docker compose pull
docker compose up -d
# If you build locally (the default), pull would fail: the image is not in a registry.
docker compose up -d --buildThen confirm what is actually running:
curl -s http://localhost:8899/health # {"status":"ok","version":"1.0.0"}version comes from the HOYOLAB_MCP_VERSION build argument, so it reports the image
you are running rather than whatever the source tree says. If it is missing, the image
was built without it — see Configuration.
Before a major version, back up the volume — it is the only copy of every linked account:
docker run --rm -v hoyolab-mcp_hoyolab-data:/data -v "$PWD":/backup \
docker.io/library/alpine:3.20 tar czf /backup/hoyolab-backup.tgz -C /data .The image is named in full (docker.io/library/alpine:3.20) rather than as alpine,
because Podman refuses a bare short name when no unqualified-search registry is
configured.
To undo an upgrade, run the previous image tag again against the same volume:
HOYOLAB_MCP_IMAGE=hoyolab-mcp:1.0.0 docker compose up -dRolling back is safe because the database is only ever extended, never rewritten in a way older versions cannot read. Restoring the backup above is only needed if a version wrote something incompatible — which would be a major version, and is called out in the changelog.
What is stable between versions. These are treated as public interface, and changing one would be a breaking change called out in the changelog:
Interface | Value |
MCP endpoint path |
|
Link page path |
|
Health path |
|
Environment variables |
|
Container port |
|
Tool names | the eight names in Tools |
Database format | readable by later versions |
If you upgrade an existing install, nothing needs to be re-registered. The URL, the auth mode and the tool names above are unchanged, so an Open WebUI connection created against an earlier build keeps working as-is.
Release notes for each version are in CHANGELOG.md.
Removing your data
Because this server stores a login credential, there is an explicit way to take it back — you do not have to delete the whole database.
# Delete this user's stored cookies (and their tokens)
docker compose exec hoyolab-mcp python -m hoyolab_mcp.cli unlink --user alice
# Keep the tokens, remove only the cookies
docker compose exec hoyolab-mcp python -m hoyolab_mcp.cli unlink --user alice --keep-tokens
# Revoke tokens without touching the cookie
docker compose exec hoyolab-mcp python -m hoyolab_mcp.cli revoke-all --user aliceRemoving the credential from this database does not invalidate it at HoYoLAB. To do that, log out of HoYoLAB in the browser the cookies came from — that invalidates the session for every client, including this one.
To remove everything at once (all users, all tokens):
docker compose down -v # -v deletes the named volume holding the databaseunlink is deliberate about scope: it deletes only the named user's row. The database
is shared, so there is no "delete everything" flag that could be run by accident.
Troubleshooting
Symptom | Cause | Fix |
| MCP's DNS-rebinding protection; the | Add the exact |
| Client sent no bearer token and Open WebUI is not forwarding headers | Mint a token, or set |
| A JWT secret is configured, so unsigned identity is refused | Send the signed |
| Token typo, or revoked |
|
|
|
|
| Running outside Compose with no secret, so the server fails closed instead of writing cookies unencrypted | Export a secret of 16+ characters, or use Compose, which refuses to start without one |
Container is | Healthcheck timeout too tight on an emulated host | Raise |
Container exits: | Host blocks | Remove that |
| Cookies expired or belong to another account | Re-link via |
| Battle Chronicle is disabled in-game | Enable "Show my Battle Chronicle on my profile" in HoYoLAB settings — re-linking will not help |
| The URL was truncated when copied | Run |
Tools report | Different | Check |
Diagnostics:
docker compose logs -f # server logs
docker compose exec hoyolab-mcp python -m hoyolab_mcp.cli status --user alice
curl -s http://localhost:8899/healthSecurity
What this server does to protect the credentials it holds:
Cookies are encrypted at rest with AES-GCM; the user id is bound as associated data, so a ciphertext moved to another user's row fails to decrypt.
API tokens are stored as SHA-256 hashes — a database dump yields no usable token.
Cookie values never reach logs or error text; only names are logged.
No shared or fallback credential exists, so one user's link can never serve another.
The container runs as uid 10001 with
read_onlyroot,cap_drop: ALL, and only/datawritable.
What it does not do, so you can plan around it:
It is not a public-internet service. It speaks plain HTTP with no TLS, and a bearer token is the only thing protecting the data. Put it behind a TLS-terminating proxy and keep the port on loopback unless you have a reason not to.
It cannot revoke a HoYoLAB session. Only logging out of HoYoLAB does that.
HOYOLAB_MCP_SECRETprotects the database, not the host. Anyone with root on the host can read the key and decrypt the cookies.
To report a vulnerability, please do not open a public issue — use GitHub's private vulnerability reporting (Security → Report a vulnerability). See SECURITY.md for scope and what to include.
Limitations
Read-only. Nothing here can modify a Genshin account.
Genshin Impact only. Honkai: Star Rail, ZZZ and Honkai Impact 3rd are listed by
list_game_accountsbut have no tools.Unofficial endpoints. HoYoverse can change or restrict them without notice; a breaking change here would surface as an unexpected
retcode, not a version warning.Overseas accounts only. The
DSsalt and endpoints used are the overseas (os_*) set; mainland China (Miyoushe) is untested.No caching. Every call hits HoYoLAB, so a chatty model can hit rate limits.
Tests
python -m venv .venv && .venv/bin/pip install -r requirements.txt
.venv/bin/python tests/test_hoyolab_mcp.pyCovers cross-user isolation, ciphertext binding, plaintext-on-disk, log redaction,
credential validation, link-token tampering/expiry, API-token issue/resolve/revoke and
hash-only storage, unlinking, identity resolution across all three sources (including
that an unsigned header is refused once a JWT secret exists), UID→server mapping, and
the exact wire shape of each API call (two real bugs were found this way:
character/list takes its parameters in the JSON body, and the account list lives on
the community host).
For a live server, verify_e2e.py checks the deployed behaviour end to end. It is a
host-side script and is not baked into the image, and the container's root filesystem
is read-only, so it cannot simply be copied in. Run it in a throwaway container that
shares the server's volume and can reach it over the network:
docker run --rm --network host \
-v hoyolab-mcp_hoyolab-data:/data \
-v "$PWD":/src:ro \
-e HOYOLAB_MCP_SECRET="$(grep '^HOYOLAB_MCP_SECRET=' .env | cut -d= -f2-)" \
-e HOYOLAB_MCP_DB=/data/hoyolab.db \
-w /src --entrypoint python hoyolab-mcp:latest verify_e2e.py \
--url http://127.0.0.1:8899Add --jwt-secret "<Open WebUI's value>" to include the Open WebUI identity checks.
The token checks need the shared volume, which is why it is mounted rather than pointing
the script at a copy of the database.
Contributing
Issues and pull requests are welcome. Full guidance is in CONTRIBUTING.md; the short version:
Keep the tests green —
python tests/test_hoyolab_mcp.pymust pass. New behaviour needs a test; a bug fix needs a test that fails without the fix.Preserve the security properties listed under Security. In particular do not add a shared credential, do not log cookie values, and do not let an unsigned header identify a user once a JWT secret is configured.
No new runtime dependencies without a reason in the PR — the small dependency surface is deliberate.
Match the surrounding style: comments explain why, not what.
There is no CLA. By contributing you agree your work is licensed under the MIT licence below.
Credits and attribution
Prior work this project learned from
genshin.py — MIT, originally by sadru, now maintained by seriaati.
This project does not depend on or vendor genshin.py; the client here is written against the HTTP API directly. However, genshin.py was the reference used to work out parts of the protocol, and three things in this codebase derive from reading it:
The
DSheader algorithm (make_dsinhoyolab_mcp/client.py) —md5("salt=…&t=…&r=…")with a 6-character random component. The algorithm belongs to HoYoverse's web client, but this implementation was written after readinggenshin/utility/ds.py.The UID → server mapping (
GENSHIN_SERVERS) — the prefix ranges are HoYoverse facts, but the table was transcribed fromgenshin/utility/uid.py.The
exploration_percentagedivisor — genshin.py divides by 10, which is what revealed that an earlier version of this project dividing by 100 was wrong.
Its MIT licence text is reproduced in THIRD_PARTY_LICENSES.md.
genshinstats — the predecessor library by sadru, superseded by genshin.py. Same lineage for the API knowledge above.
UIGF-org/mihoyo-api-collect —
community documentation of the HoYoLAB/Miyoushe token and record APIs. Used to confirm
which cookie names carry authentication (ltuid_v2 / ltoken_v2 / ltmid_v2 and the
legacy pair).
Libraries
Library | Licence | Used for |
mcp (Anthropic) | MIT | MCP server framework and client (tests) |
BSD-3-Clause | HTTP client for HoYoLAB | |
MIT | Verifying Open WebUI's forwarded identity token | |
Apache-2.0 OR BSD-3-Clause | AES-GCM encryption of stored cookies | |
BSD-3-Clause | ASGI server and routes (via mcp) |
All are permissively licensed; none require attribution in documentation, but they are
listed for transparency. Full texts in THIRD_PARTY_LICENSES.md.
Data and trademarks
Genshin Impact, HoYoLAB and related marks are trademarks of HoYoverse. This project is unofficial and unaffiliated. Endpoint URLs, property-type codes and game constants are facts about HoYoverse's own services, reproduced here for interoperability.
What is original
Everything else: the MCP tool design, per-user encrypted credential store, the account-linking flow with signed tokens, the Open WebUI integration, response shaping, and the test suite.
Licence
MIT — see LICENSE. Third-party licence texts are in
THIRD_PARTY_LICENSES.md.
Using this software does not grant any rights to Genshin Impact, HoYoLAB, or HoYoverse services, and this project is not affiliated with them.
This server cannot be deployed
Maintenance
Related MCP Connectors
Read and edit GA4, Search Console and Google Tag Manager from any MCP client. 29 tools.
Search your AI chat history (ChatGPT, Claude, Codex) from any MCP client. Remote, private, read-only
Read-only MCP tools for AI agent discovery, structured resources, and NIULAI information.
Read-only search of 2,189 AI agent tools (MCP servers, x402 APIs) with daily automated trust scores.
Related MCP Servers
- FlicenseNot gradedqualityFmaintenanceA comprehensive MCP server that wraps the complete World of Warcraft retail API into 197 tools for Game Data and Profile information. It enables users to query character statistics, achievements, collections, and game mechanics across all global regions and locales.11-
- AlicenseAqualityBmaintenanceExposes Steam Web API tools as MCP resources for Claude Code, Claude Desktop, and Gemini CLI, enabling profile lookups, game searches, achievement tracking, and more.1118 npm1MIT
- AlicenseNot gradedqualityDmaintenanceMCP server for World of Warcraft that provides 26 tools for accessing characters, guilds, realms, game data, and auction house info via Blizzard's Battle.net APIs. It enables natural-language queries for character audits, Mythic+ reviews, market analysis, and more.MIT
- AlicenseNot gradedqualityBmaintenanceProvides read-only access to HoYoLAB and HoYoWiki APIs, enabling search and retrieval of posts, wiki entries, and article content converted to Markdown.3AGPL 3.0