Stateless MCP server for deckbuilding against Archidekt collections, personal decks, and Scryfall.
The server is designed for LLM-driven workflows:
- optional authenticated Archidekt access through explicit request payloads
- optional MCP OAuth auth for ChatGPT and other remote MCP clients
- no server-side deckbuilding session persistence
- no per-user environment variables
- every request passes the collection locator explicitly
- private deck access can use either an explicit
accountobject or the active MCP auth session - collection snapshots are cached in Redis for 24 hours by default
- authenticated collection snapshots, personal deck overlap data, and optional MCP OAuth state are also cached in Redis by default
- personal deck cache TTL defaults to 900 seconds; Archidekt request budgets default to 30 requests per 60 seconds, 3 retries, 1 second retry backoff, and 900 seconds for exact-name lookups
- OAuth access tokens, refresh tokens, session records, and the Archidekt login credential are stored in Redis without a TTL by default, so authenticated MCP logins can be renewed until they are explicitly revoked or Redis data is cleared
MCP tools:
login_archidekt([account])list_personal_decks([account])search_archidekt_cards(filters)get_personal_deck_cards(deck_id, include_deleted, [account])create_personal_deck(deck, [account])update_personal_deck(deck_id, deck, [account])delete_personal_deck(deck_id, [account])modify_personal_deck_cards(deck_id, cards, [account])upsert_collection_entries(entries, [account])delete_collection_entries(entries, [account])get_collection_overview(collection)read_collection(collection, [options], [account])check_collection_card_availability(collection, cards, [options], [account])refresh_collection_cache(collection)search_owned_cards(collection, filters)search_unowned_cards(collection, filters)
HTTP routes:
/English Web UI with copy buttons for generated code blocks/healthhealth check/api/loginstateless HTTP test forlogin_archidekt/api/personal-decksstateless HTTP test forlist_personal_decks/api/cards/searchstateless HTTP test forsearch_archidekt_cards/api/personal-deck-cardsstateless HTTP test forget_personal_deck_cards/api/personal-decks/createstateless HTTP test forcreate_personal_deck/api/personal-decks/updatestateless HTTP test forupdate_personal_deck/api/personal-decks/deletestateless HTTP test fordelete_personal_deck/api/personal-decks/modify-cardsstateless HTTP test formodify_personal_deck_cards/api/collection/upsertstateless HTTP test forupsert_collection_entries/api/collection/deletestateless HTTP test fordelete_collection_entries/api/overviewstateless HTTP test forget_collection_overview/api/search-ownedstateless HTTP test forsearch_owned_cards/api/search-unownedstateless HTTP test forsearch_unowned_cards/auth/archidekt-loginOAuth authorization page used when MCP auth is enabled/mcpstreamable HTTP MCP endpoint
Every tool call must include collection with one of:
collection_idcollection_urlusername
Optional fields:
gamewhere1 = Paper,2 = MTGO,3 = Arena
Authenticated requests may also include optional account with either:
tokenusernameoremail, pluspassword
If the MCP client connected through OAuth, private tools may omit account and use the active MCP auth session instead.
That same session-scoped identity can be reused for private deck and collection writes too.
Recommended auth flow:
- If MCP OAuth is enabled, connect the app through ChatGPT and sign in on
/auth/archidekt-login. - Call
login_archidekt()without anaccountpayload, or calllogin_archidekt(account)when you are not using MCP OAuth. - Read the
personal_decksblock from the login response so the model immediately knows which decks already exist on the account. - Reuse the returned
accountobject in later tool calls only when you are not relying on MCP auth. - For the logged-in user's collection, reuse the returned
collection.collection_id.
Example:
{
"collection": {
"username": "your_archidekt_username",
"game": 1
}
}Authenticated example:
{
"collection": {
"collection_id": 123456,
"game": 1
},
"account": {
"token": "your_archidekt_token",
"username": "your_archidekt_username",
"user_id": 123456
}
}When search_owned_cards is called with account, or when an MCP auth session is active, the response may include personal_deck_usage,
personal_deck_count, and personal_deck_total_quantity on each owned result so the LLM can warn
that a candidate card is already committed to other personal decks.
For a complete raw collection export, use read_collection instead of direct Archidekt API calls. It posts to
Archidekt's /api/collection/export/v2/{user_id}/ endpoint, follows export pages, returns a parsed preview,
and can write the CSV locally when options.export_to_file=true or options.file_path is provided. Set
options.include_csv_content=true only when the model needs the full CSV in the MCP tool result.
For collection-only deckbuilding, use check_collection_card_availability before adding candidates to a deck.
It calculates available_quantity = collection_quantity - used_in_decks_quantity from the authenticated collection
and personal deck usage. Cards with enough_copies=false or must_not_use=true should be replaced with another
owned card unless the user explicitly allows reusing copies already committed to existing decks.
To let ChatGPT keep the Archidekt identity attached to the MCP session instead of passing credentials in tool arguments, enable MCP OAuth:
$env:ARCHIDEKT_MCP_AUTH_ENABLED = "true"
$env:ARCHIDEKT_MCP_PUBLIC_BASE_URL = "https://your-public-domain"Required notes:
ARCHIDEKT_MCP_PUBLIC_BASE_URLmust be the public base URL ChatGPT reaches, without the/mcpsuffix- when auth is enabled, the MCP endpoint stays at
/mcp, but ChatGPT will also use/.well-known/oauth-authorization-server,/authorize,/token,/register,/revoke, and/auth/archidekt-login - the authorization page asks for Archidekt username/email plus password once, exchanges that for an Archidekt token, and stores the resulting Archidekt token plus login credential in Redis-backed OAuth state
- by default, the login credential is retained so the MCP server can re-login to Archidekt and replace an invalid Archidekt token without asking the user to sign in again; set
ARCHIDEKT_MCP_AUTH_PERSIST_LOGIN_CREDENTIALS=falseto disable renewal and return to token-only persistence - with the bundled
compose.yml, Redis uses append-only persistence on theredis-datavolume, so OAuth logins and renewal state survive MCP service restarts and remain valid until disconnect/revocation as long as that volume is preserved
After the app is connected through OAuth, private MCP tools can omit account, for example:
login_archidekt()
list_personal_decks()
search_owned_cards(collection=..., filters=...)
Owned card results may also include archidekt_card_ids, which can be reused directly in
modify_personal_deck_cards and upsert_collection_entries without guessing Archidekt ids.
For fully automated deck/account management, the recommended sequence is:
- Call
login_archidekt()if MCP OAuth is active, otherwise calllogin_archidekt(account). - Use the returned
personal_deckslist as the first source of truth for which decks already exist on the account. - Use
search_owned_cardsand/orsearch_archidekt_cardsto resolve Archidektcard_idvalues. - Use
list_personal_decksorget_personal_deck_cardsonly when you need a fresh deck listing or the contents of a specific deck. - Create or update the deck with
create_personal_deck,update_personal_deck, andmodify_personal_deck_cards. - Update the account collection with
upsert_collection_entrieswhen needed.
get_personal_deck_cards returns deck_relation_id values for cards already in a deck. Those ids
should be reused for modify and remove actions in modify_personal_deck_cards.
search_archidekt_cards returns the numeric Archidekt card_id used by both deck card mutations and
collection v2 upserts. Its exact_name filter accepts either one exact card name or a list of exact
card names, so the model can batch several catalog checks in one request. Batched exact-name results
also include requested_exact_name to show which requested card name each returned printing came from.
The current authenticated write surface is focused on the account's personal decks and collection v2 entries. It does not yet expose every Archidekt endpoint such as folders, tags, or text-import flows.
For sorting, prefer canonical sort_by values such as unit_price, total_value, cmc, edhrec_rank,
rarity, added_at, and updated_at, but the server also normalizes common aliases such as price,
price_desc, mana_value_desc, and rarity_desc.
Unless the user explicitly asks for another format, the model should respond with:
- A short strategy guide.
- Card recommendations grouped by category.
- One card per line in the exact format
N Card Name.
Example:
Strategy Guide
Use early ramp to fix mana, interact efficiently in the mid game, and convert your engine pieces into sustained card advantage and closing power.
Ramp
1 Sol Ring
1 Arcane Signet
Removal
1 Swords to Plowshares
The bundled Web UI is fully in English and is meant to help you:
- enter a public Archidekt collection locator
- connect an Archidekt account through the same OAuth flow used by the MCP server
- generate the exact
collectionJSON for MCP tool calls - generate an LLM instruction block for the current request
- test authenticated login and personal deck listing over HTTP with the connected OAuth session
- test overview, owned, and unowned searches over HTTP
- inspect the authenticated write endpoints available for card lookup, deck edits, and collection upserts
- copy generated JSON, instructions, and API responses with one click
The server-side UI remains stateless. When OAuth is enabled on the deployment, the browser page keeps the OAuth session in browser storage until you disconnect it, so private API tests can reuse the bearer token without any pasted credential JSON.
Create a virtual environment and install the project:
python -m venv .venv
.venv\Scripts\activate
python -m pip install --upgrade pip
python -m pip install -e .Run a local Redis instance before starting the server.
Start the server directly:
$env:ARCHIDEKT_MCP_HOST = "127.0.0.1"
$env:ARCHIDEKT_MCP_PORT = "8000"
$env:ARCHIDEKT_MCP_REDIS_URL = "redis://127.0.0.1:6379/0"
$env:ARCHIDEKT_MCP_CACHE_TTL_SECONDS = "86400"
$env:ARCHIDEKT_MCP_PERSONAL_DECK_CACHE_TTL_SECONDS = "900"
$env:ARCHIDEKT_MCP_ARCHIDEKT_RATE_LIMIT_MAX_REQUESTS = "30"
$env:ARCHIDEKT_MCP_ARCHIDEKT_RATE_LIMIT_WINDOW_SECONDS = "60"
$env:ARCHIDEKT_MCP_ARCHIDEKT_RETRY_MAX_ATTEMPTS = "3"
$env:ARCHIDEKT_MCP_ARCHIDEKT_RETRY_BASE_DELAY_SECONDS = "1.0"
$env:ARCHIDEKT_MCP_ARCHIDEKT_EXACT_NAME_CACHE_TTL_SECONDS = "900"
$env:ARCHIDEKT_MCP_USER_AGENT = "archidekt-mcp-server/0.3 (+mailto:you@example.com)"
$env:ARCHIDEKT_MCP_FORWARDED_ALLOW_IPS = "127.0.0.1"
# Optional MCP OAuth for ChatGPT / remote clients:
# $env:ARCHIDEKT_MCP_AUTH_ENABLED = "true"
# $env:ARCHIDEKT_MCP_PUBLIC_BASE_URL = "https://your-public-domain"
.venv\Scripts\python.exe -m archidekt_commander_mcp.serverThen open:
- Web UI:
http://127.0.0.1:8000/ - MCP endpoint:
http://127.0.0.1:8000/mcp
Run the test suite:
.venv\Scripts\python.exe -m ruff check src/archidekt_commander_mcp
.venv\Scripts\python.exe -m unittest discover -s tests -vBuild the image:
docker build -t archidekt-mcp-server:latest .For actual local runtime, prefer docker compose so the app and Redis start together with the correct network wiring.
This repository also includes a Containerfile.
Build with Podman:
podman build -f Containerfile -t archidekt-mcp-server:latest .Run with Podman Compose using the same compose.yml file:
podman compose up --build -dThe repository ships with compose.yml for a two-service deployment:
appfor the MCP serverredisfor the shared 24-hour cache
Start the stack:
docker compose up --build -dStop the stack:
docker compose downWith the stack running:
- Web UI:
http://127.0.0.1:8000/ - MCP endpoint:
http://127.0.0.1:8000/mcp
The Redis service is configured with append-only persistence and a named volume. That same persistent Redis volume also keeps MCP OAuth login sessions across app restarts without expiring them automatically.
The app service uses environment variables instead of a long command override:
ARCHIDEKT_MCP_REDIS_URLARCHIDEKT_MCP_USER_AGENTARCHIDEKT_MCP_FORWARDED_ALLOW_IPS, set to*in the bundled compose file so access logs use the client IP from trusted reverse proxy headers- plus image defaults for host, port, transport, and cache TTL
If you access the container directly through Docker's published port, Uvicorn may still show the Docker bridge
gateway address, such as 192.168.x.1. Docker NAT does not pass the original browser IP to the container.
To log the real external client IP, put the app behind a reverse proxy that sets X-Forwarded-For or
X-Real-IP, or use host networking on Linux; then keep ARCHIDEKT_MCP_FORWARDED_ALLOW_IPS limited to the
trusted proxy address or CIDR instead of *.
The workflow in .github/workflows/docker.yml does two things:
- Installs the project and runs the Python unit tests.
- Validates
compose.yml, builds the Docker image with Buildx, and pushes it to GHCR onmain.
It runs on:
- pushes to
main - pull requests targeting
main - manual dispatch
Published image:
ghcr.io/dnviti/archidekt-mcp-server:latest
Example Codex MCP configuration:
[mcp_servers.archidekt-commander]
url = "http://127.0.0.1:8000/mcp"
tool_timeout_sec = 60Because the server is stateless, the model must pass collection on every call.
--transport--host--port--log-level--cache-ttl-seconds--personal-deck-cache-ttl-seconds--redis-url--redis-key-prefix--http-timeout-seconds--max-search-results--scryfall-max-pages--user-agent--streamable-http-path--forwarded-allow-ips
The same runtime options can also be provided as environment variables with the ARCHIDEKT_MCP_ prefix, for example:
ARCHIDEKT_MCP_TRANSPORTARCHIDEKT_MCP_HOSTARCHIDEKT_MCP_PORTARCHIDEKT_MCP_REDIS_URLARCHIDEKT_MCP_CACHE_TTL_SECONDSARCHIDEKT_MCP_PERSONAL_DECK_CACHE_TTL_SECONDSARCHIDEKT_MCP_AUTH_PERSIST_LOGIN_CREDENTIALSARCHIDEKT_MCP_FORWARDED_ALLOW_IPSARCHIDEKT_MCP_USER_AGENT
- Set a real contact in the
User-Agentwhen exposing the server publicly. - When running behind a reverse proxy, set
ARCHIDEKT_MCP_FORWARDED_ALLOW_IPSto that proxy's IP/CIDR list, or*only when every direct connection reaches the app through a trusted proxy. - Redis is the cache backend. The server no longer uses local file-based collection snapshots.
- Authenticated collection snapshots and personal deck overlap data are cached in Redis with account-scoped keys.
- MCP OAuth sessions are stored in Redis as access-token, refresh-token, and session records with no automatic expiration, so restarting the Python service does not force every user to sign in again if the Redis volume is still intact.
- When
ARCHIDEKT_MCP_AUTH_PERSIST_LOGIN_CREDENTIALSis enabled, Redis also stores the Archidekt login credential for silent Archidekt token renewal. Protect the Redis instance and volume accordingly. Reuse the returnedaccount.tokenafter login instead of resending credentials when you are not using MCP OAuth. - The server is stateless with respect to user identity and collection context. Always pass the locator explicitly.