Lorcana MCP Server
An MCP server for searching and aggregating Disney Lorcana cards.
Startup behavior
On startup, the server fetches a JSON list of cards from https://danielenricocahall.github.io/lorcana-mcp/allCards.json. The snapshot is refreshed daily by data_pipeline/fetch_cards.py, which pulls from the Lorcast API, normalizes each card into our internal schema, and publishes the list to the gh-pages branch. The middle layer insulates running containers from Lorcast's availability and rate limits — the runtime never calls Lorcast directly.
Cards are kept in-memory as a Python list for fast filtering. With ~2,270 unique cards (each carrying a printings array for its alternate sets/rarities) this is lightweight and requires no external database. A local JSON file cache (LORCANA_CACHE_PATH, default cards.json) lets the server skip the network fetch on subsequent startups.
Startup data loading is controlled by:
LORCANA_REFRESH_ON_STARTUP:true: always fetch from API and repopulate storagefalse: use existing cache if available
LORCANA_SKIP_IF_DB_EXISTS:true(default): skip API fetch if the cache file already contains cardsfalse: fetch and repopulate
Quick start (no clone required)
The server is published to GHCR and the MCP Registry. Pull and run it directly:
docker pull ghcr.io/danielenricocahall/lorcana-mcp:latest
docker run --rm -i ghcr.io/danielenricocahall/lorcana-mcp:latest
To persist the card cache across container restarts, mount a volume:
docker run --rm -i \
-e LORCANA_CACHE_PATH=/data/cards.json \
-e LORCANA_SKIP_IF_DB_EXISTS=true \
-v lorcana_mcp_data:/data \
ghcr.io/danielenricocahall/lorcana-mcp:latest
Run locally (stdio MCP)
uv run python main.py
Docker
Build image
docker build -t lorcana-mcp:latest .
Run as stdio MCP server
docker run --rm -i lorcana-mcp:latest
Docker Compose
Start with compose
docker compose build
docker compose run --rm -T lorcana-mcp
Notes:
- No port is exposed; MCP communication is over stdio.
- Use a volume to persist the JSON cache across restarts.
Config
LORCANA_API(default:https://danielenricocahall.github.io/lorcana-mcp/allCards.json)LORCANA_CACHE_PATH(default:cards.json) — local file for caching fetched cardsLORCANA_HTTP_TIMEOUT_SECONDS(default:60)LORCANA_REFRESH_ON_STARTUP(falsedefault)LORCANA_SKIP_IF_DB_EXISTS(truedefault)
MCP client setup examples
Local process (Claude Desktop-style)
{
"mcpServers": {
"lorcana": {
"command": "uv",
"args": ["run", "python", "/absolute/path/to/lorcana-mcp/main.py"]
}
}
}
Published image — GHCR (Claude Desktop-style, no clone required)
{
"mcpServers": {
"lorcana": {
"command": "docker",
"args": [
"run",
"--rm",
"-i",
"ghcr.io/danielenricocahall/lorcana-mcp:latest"
]
}
}
}
Docker process (Claude Desktop-style, locally built)
{
"mcpServers": {
"lorcana": {
"command": "docker",
"args": [
"run",
"--rm",
"-i",
"lorcana-mcp:latest"
]
}
}
}
Docker Compose process (Claude Desktop-style)
{
"mcpServers": {
"lorcana": {
"command": "docker",
"args": ["compose", "run", "--rm", "-T", "lorcana-mcp"]
}
}
}
Via the Claude CLI — published image (global, no clone required)
claude mcp add --scope user \
-- lorcana docker run --rm -i \
ghcr.io/danielenricocahall/lorcana-mcp:latest
Via the Claude CLI — locally built
claude mcp add --scope user \
-- lorcana docker run --rm -i lorcana-mcp:latest
Example questions
Once connected to an MCP client, you can ask natural language questions like:
Card lookup
- "Show me all cards named Moana"
- "What does the card Maui - Hero to All do?"
- "Find all legendary amber cards"
Deck building
- "What are the cheapest ruby characters with at least 3 strength?"
- "Show me inkable sapphire cards that cost 4 or less"
- "Find steel characters with 5 or more willpower"
- "What 3-lore characters exist in emerald?"
Keyword & ability search
- "How many Singer cards cost exactly 5?"
- "How many Evasive characters are there in the first set?"
- "How many ruby cards have Reckless?"
- "Find all cards with Ward in their text"
- "Show me Shift cards in amethyst"
Stats & aggregations
- "How many cards are in each set?"
- "What's the color distribution across all cards?"
- "What are the most common traits?"
- "Show me the ink curve — how many cards exist at each cost?"
- "How many legendary cards are inkable?"
Cross-filter queries
- "How many amber characters have 3 or more lore?"
- "Find cheap (cost 2-3) characters with high strength (4+) in steel"
- "How many cards in set 1 have Evasive and cost less than 4?"
Note: For plain keyword queries (Evasive, Bodyguard, Shift, etc.) use the
keywordparameter — it filters against the structured ability list and is more reliable than substring search. For value-specific queries likeSinger 5orResist +2, usebody_text(keyword values live in the card's full text, not the ability list).
MCP tools
search_cards— filter and retrieve card objects (supportsresponse_format="toon"for ~10% fewer tokens)count_cards— count cards matching a filter without returning full objectsaggregate_cards— card counts grouped bycost(ink curve),rarity,color,set_code, ortyperesolve_card— fuzzy-match an informal/partial/misspelled card name to the closest cards (returns full card data)top_traits— most common traits across all cardsexport_deck— render a deck as a Dreamborn/Pixelborn-compatible text deck listimport_deck— parse a Dreamborn/Pixelborn-style deck list, returning resolved cards plus any unresolved lines with fuzzy candidatesvalidate_deck— check a deck against the format rules (≥60 cards, max 4 copies, ≤2 inks); returns{legal, total_cards, inks, violations}deck_stats— compute ink curve, color split, inkable count, and type breakdown for a deckserver_status— startup metadata (card count, config)
MCP prompts
build_deck(colors, playstyle="balanced")— guides the model through assembling a legal Lorcana deck (60-card minimum, ≤2 inks, max 4 copies of any card) for the requested color(s) and playstyle (aggressive/control/lore-race/balanced). Uses the search/aggregate tools above plus the rules embedded in the server instructions.
TOON response format
search_cards accepts a response_format argument:
"json"(default) — list of card objects, unchanged from prior versions."toon"— a TOON string with one column header line and one row per card, encoded by thetoonsRust-backed library (the official community reference implementation).
Example (search_cards(name="elsa", limit=2, response_format="toon")):
cards[2]:
- id: crd_01c4835a62df4960bb973aeff81f2bb2
name: Elsa
version: Ice Maker
full_name: Elsa - Ice Maker
cost: 7
...
printings[3]{set_code,set_name,number,rarity}:
"7",Archazia's Island,69,Super Rare
C2,Lorcana Challenge Year 3,2,Promo
C2,Lorcana Challenge Year 3,6,Promo
- id: crd_04bca46a8e2d4e9ba0fbdbfc6c99e51e
name: Elsa
...
The outer cards[2]: falls back to YAML-style per-card blocks (rather than a single tabular table) because card shapes vary — Actions and Items don't carry strength/willpower/lore, for example. The inner printings[N]{...}: block is fully tabular since every printing has the same four fields.
Benchmark
Measured with benchmarks/bench_toon.py against the live ~2,270-card dataset (post-consolidation), tokenizing with tiktoken cl100k_base (used as a proxy for Claude's tokenizer):
| query | rows | JSON tokens | TOON tokens | Δ |
|---|---|---|---|---|
color="amber", limit=200 | 200 | 43,672 | 39,282 | −10.1% |
color="ruby", limit=50 | 50 | 10,446 | 9,464 | −9.4% |
card_type="action", limit=50 (sparse cols) | 50 | 10,150 | 9,265 | −8.7% |
body_text="when", limit=50 (long full_text) | 50 | 11,574 | 10,380 | −10.3% |
name="elsa", limit=20 | 14 | 3,456 | 2,925 | −15.4% |
| total | 79,298 | 71,316 | −10.1% |
Note: TOON's relative savings are smaller here than they were before the printings consolidation (pre-PR-#29 the same queries showed ~50% reductions). That gap is structural to the nested printings array — TOON's columnar encoding wins on the top-level fields but falls back to JSON-style encoding inside the per-printing entries, so the array dilutes the relative gain. Absolute token counts are still down meaningfully versus the equivalent count of pre-consolidation rows since each unique card is now represented once with a small printings list rather than as 1-3 separate full rows.
Reproduce with PYTHONPATH=. uv run python benchmarks/bench_toon.py (requires a populated cards.json cache).
Disclaimer
This is a personal, unofficial fan and engineering project. It is not affiliated with, endorsed by, sponsored by, or reviewed by Disney, Ravensburger, or the Disney Lorcana TCG team. I worked only with publicly available/community data sources. All Disney Lorcana TCG names, card text, trademarks, and related intellectual property belong to Disney and Ravensburger. This project is non-commercial and reflects my personal views only, not those of my employer.