mcp-scryfall
This MCP server provides live Magic: The Gathering card lookups and data from the Scryfall API.
Exact-name lookup (
card_named): fetch a full card object by exact name, optionally restricted to a specific set, with printing history and first-printing info.Fuzzy-name lookup (
card_fuzzy): find cards despite typos, partial names, or alternate spellings.Query-syntax search (
card_search): search cards using Scryfall's full query language, returning compact summaries or raw objects, with paging and sort options.Batch decklist resolution (
card_collection): resolve up to 75+ card names/identifiers in one call (automatically chunked), returning found cards plus anot_foundlist for misses.Random card (
card_random): get a random card, optionally filtered by a Scryfall query.Rulings lookup (
card_rulings): fetch official rulings/errata for a card by exact name or Scryfall ID.Bulk data endpoints (
bulk_default): list Scryfall bulk-data downloads for building offline corpora.API etiquette built in: automatic rate-limit spacing, caching, retries with backoff, and timeout handling—so you don't have to manage Scryfall's constraints yourself.
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., "@mcp-scryfalllook up the card 'Black Lotus'"
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.
mcp-scryfall
MCP server for live Magic: The Gathering card lookup via the Scryfall API. Built on the MCP TypeScript SDK.
LLMs misremember card names, costs, and rules text. This looks them up on live Scryfall instead. I use it daily for deckbuilding.
Tools
Tool | What it does |
| Exact-name lookup (optional set code). Full card object for one printing (the most recent unless |
| Fuzzy-name lookup. Handles typos and partial names. Same one-printing object, |
| Scryfall query-syntax search. Returns compact summaries by default (pass |
| Batch lookup ( |
| A random card, optionally filtered by a query. |
| Official Wizards rulings for one card, by exact name or Scryfall id. The errata and corner-case answers that aren't in the oracle text. |
| Lists Scryfall bulk-data endpoints for offline corpus building. |
The compact summary returned by card_search and card_collection is name, mana_cost, type_line, cmc, set, collector_number, oracle_text, power, toughness, color_identity, legal_commander. Every key is always present, null when the card has no value, so "this creature has no power" is distinguishable from "that field wasn't returned".
Related MCP server: Scryfall MCP Server
Install
git clone https://github.com/haksanlulz/mcp-scryfall
cd mcp-scryfall
npm installRuns directly with tsx; no build step.
Use it from an MCP client
Add it to your client's MCP config:
{
"mcpServers": {
"scryfall": {
"command": "npx",
"args": ["tsx", "/absolute/path/to/mcp-scryfall/index.ts"],
"env": { "SCRYFALL_CONTACT": "you@example.com" }
}
}
}Configuration
All four knobs are environment variables, all optional. None is a credential; Scryfall's API needs no key.
Variable | Default | Effect |
| this repository's URL | Added to the |
|
| Lifetime of a cached GET response. |
|
| LRU entry cap for that cache. Floor 1. |
|
| Attempts per request, counting the first. Floor 1, ceiling 10. |
The three numeric ones are parsed once at load. A value that isn't an integer, or outside its range, is rejected with a line on stderr and the default is used. The check earns its place: an unparseable value used to pass straight into the arithmetic, where a NaN TTL silently disabled the cache, a NaN cap silently removed the LRU bound, and a NaN or zero attempt cap issued no request at all.
SCRYFALL_MAX_ATTEMPTS is the only one with a ceiling, because it is the only one whose too-large direction is the dangerous one: retries wait inside the same serialized queue, and each wait can be a honoured Retry-After of up to 10 s, so a mistyped 3000 holds every later tool call in the process behind one failing request. It's rejected, never clamped, so a value that's bad only in magnitude gets the same notice as one that's bad in form.
The ceiling bounds each wait, not the total, so the total is worth knowing before raising it. Against an upstream that answers nothing usefully, one call holds the queue for up to (attempts - 1) x 10 s of honoured Retry-After plus attempts x 15 s of request timeout: up to 65 s at the default 3, up to 240 s at the ceiling of 10. The MCP SDK's own default request timeout is 60 s (DEFAULT_REQUEST_TIMEOUT_MSEC), so even the default can outlast the client that asked: the client gives up while this process is still holding every later call behind the dead one. Raise it only if your client waits longer than that.
Install as a bundle (.mcpb)
npm install
npm run bundleWrites build/mcp-scryfall-<version>.mcpb, then unpacks it and drives the packed entry point over stdio. Open the .mcpb with an MCPB host to install. The install dialog offers one optional field, Contact, which the manifest maps to SCRYFALL_CONTACT; leave it blank and the User-Agent falls back to this repository's URL. There's no API key; Scryfall needs none.
Sizes as of 2026-09-15: 3.2 MB packed, 10.5 MB unpacked, 2,268 files, nearly all of it the MCP SDK's dependency tree. The staging install is npm ci --omit=dev off this repo's lockfile, so tsx, vitest and typescript are not in it.
The bundle is the only place this repo emits JavaScript. Everything else runs .ts through tsx, but an MCPB host runs node <entry_point> with no toolchain of its own, so manifest.json (MCPB manifest version 0.3) points at dist/index.js built by tsconfig.build.json. Two shipping paths for one server is a drift risk, so npm run bundle doesn't stop at packing. It unpacks what it just wrote and asserts the handshake and all seven tools against it, offline. It also removes dist/ before building, because tsc doesn't: staging copies the whole directory, so anything a bare npx tsc or a since-renamed file left there would otherwise pack silently. For a live round-trip through the built entry point instead of the source:
SMOKE_SERVER_PATH=/abs/path/to/mcp-scryfall/dist/index.js npm run smoketest/bundle-manifest.test.ts pins the manifest against the code in the offline gate: entry_point and args naming the same file, SCRYFALL_CONTACT wired to a declared optional non-sensitive field, a privacy policy present (the bundle reaches Scryfall), and the manifest's tool list equal to what tools/list serves. What it can't check is whether a host's install dialog really maps that field into the environment. That's host behavior, and only a real install exercises it.
Built and probed on Node 20 and 22 (the CI matrix), which is why compatibility.runtimes.node says >=20. package.json's engines still says >=18; nothing here has been run on 18.
Examples
Shapes below are exact. The volatile values (total_cards, and the set and collector_number of whatever the latest printing was) are what Scryfall returned on 2026-09-14. Card text and P/T are the card's own.
card_search with q = "c:rb cmc<=2 t:creature o:haste" returns compact rows, rules text included, plus paging metadata. One of the 12 rows:
{
"total_cards": 12,
"has_more": false,
"page": 1,
"data": [
{
"name": "Dreadhorde Butcher",
"mana_cost": "{B}{R}",
"type_line": "Creature — Zombie Warrior",
"cmc": 2,
"set": "war",
"collector_number": "194",
"oracle_text": "Haste\nWhenever this creature deals combat damage to a player or planeswalker, put a +1/+1 counter on this creature.\nWhen this creature dies, it deals damage equal to its power to any target.",
"power": "1",
"toughness": "1",
"color_identity": ["B", "R"],
"legal_commander": "legal"
}
]
}card_collection with identifiers = ["Lightning Bolt", "Counterspell", "Zzzz Definitely Not A Card"] resolves the whole list in one call and leads with what it couldn't find:
{
"requested": 3,
"found": 2,
"not_found": [{ "name": "Zzzz Definitely Not A Card" }],
"data": [
{ "name": "Lightning Bolt", "mana_cost": "{R}", "type_line": "Instant", "cmc": 1, "set": "msc", "collector_number": "806", "oracle_text": "Lightning Bolt deals 3 damage to any target.", "power": null, "toughness": null, "color_identity": ["R"], "legal_commander": "legal" },
{ "name": "Counterspell", "mana_cost": "{U}{U}", "type_line": "Instant", "cmc": 2, "set": "dsc", "collector_number": "114", "oracle_text": "Counter target spell.", "power": null, "toughness": null, "color_identity": ["U"], "legal_commander": "legal" }
]
}Scryfall caps one collection POST at 75 identifiers; longer lists are split into sequential rate-limited POSTs automatically, so a 100-card decklist is one tool call (two requests under the hood).
Pass full: true to either tool to get the raw Scryfall objects instead.
The loop this exists for
Checking a decklist is the whole point: a model writing one produces names that are nearly right, and a near-miss is the failure mode that reads as success. card_collection is the batch form of that check: every name either comes back with its canonical spelling or appears in not_found, and nothing passes silently.
Run live 2026-09-14, five identifiers, one request. The set and collector_number values are whatever the latest printing was that day; the rest is exact:
{
"requested": 5,
"found": 3,
"not_found": [
{ "name": "Sylvan Libary" },
{ "name": "Teferi, Hero of Dominara" }
],
"data": [
{ "name": "Sol Ring", "mana_cost": "{1}", "type_line": "Artifact", "cmc": 1, "set": "msc", "collector_number": "211", "oracle_text": "{T}: Add {C}{C}.", "power": null, "toughness": null, "color_identity": [], "legal_commander": "legal" },
{ "name": "Arcane Signet", "mana_cost": "{2}", "type_line": "Artifact", "cmc": 2, "set": "msc", "collector_number": "191", "oracle_text": "{T}: Add one mana of any color in your commander's color identity.", "power": null, "toughness": null, "color_identity": [], "legal_commander": "legal" },
{ "name": "Cyclonic Rift", "mana_cost": "{1}{U}", "type_line": "Instant", "cmc": 2, "set": "rvr", "collector_number": "40", "oracle_text": "Return target nonland permanent you don't control to its owner's hand.\nOverload {6}{U} (You may cast this spell for its overload cost. If you do, change \"target\" in its text to \"each.\")", "power": null, "toughness": null, "color_identity": ["U"], "legal_commander": "legal" }
]
}Two of the five were wrong, and both are the shape that gets past a reader: Sylvan Libary is a dropped letter, and Teferi, Hero of Dominara is a subtitle recalled one letter off from Dominaria. Neither resolves, since exact-name matching doesn't guess, so the builder's next move is to fix those two names and re-run. legal_commander on the three that did resolve answers the other half in the same response.
If a name is wrong in a way you can't see, card_fuzzy one identifier at a time will find the intended card. card_collection won't, deliberately.
Testing
npm test # offline: vitest over an in-memory MCP transport, fetch mocked, no network
npm run typecheck
npm run bundle # offline: packs the .mcpb, then drives the PACKED artifact over stdio
npm run smoke # live: spawns the real server over stdio and calls Scryfall once per toolTiers split by script, with no markers. npm test, npm run typecheck and npm run bundle are the gate and all run in CI; npm run smoke is a manual check against the live API and costs about 13 requests, so space repeated runs; three back to back earned a real 429 on 2026-09-14.
Counts as of 2026-09-15: 643 lines of server source (wc -l index.ts server.ts; smoke.ts is 283 more and is the live tier, and scripts/bundle.mjs 197 more and is build tooling, neither of them app code), 1,670 lines of tests (wc -l test/*.ts), 83 tests across 5 files, the number npm test reports. grep -c "^\s*it(" test/*.ts sums to 75, because two cases are table-driven: a for loop over three values, and an it.each over six that the pattern does not match at all. 75 + 2 + 6 = 83. Trust the runner.
What they cover:
test/server.test.tsdrives every tool through a real MCP client over the in-memory transport and asserts on the URLs and POST bodies sent to the mockedfetchand on the JSON returned: error surfacing (404, 429, non-JSON), the 75-identifier chunking incard_collection, the abort timeout, rate-limit serialization, the response cache (hit, never forcard_random, never for errors), the 429/5xx retry and its attempt cap, a body that dies mid-read being retried like a connection that never opened, argument validation before any request is issued (a non-stringset,orderorq, a non-booleanfull, apagethat isn't a number or numeric string, none of them coerced into the URL, while a blank argument of any declared type means absent, which is what a client that fills every declared property sends), and thatcard_search's outgoingpageand echoedpageare the same value.test/retry-timing.test.tsmeasures the retry waits on a fake clock, which is the only way to tell a honouredRetry-Afterfrom an ignored one, including that a honoured header never waits less than the backoff it replaced, soRetry-After: 0waits 250 ms on the first retry and 1000 ms on the second. It also measures the gap BETWEEN two calls across a retry: a retried call issues several requests, and the next queued call must be paced from the last of them. Its own file because a fake clock advanced by N ms leaves the rate limiter's timestamp N ms in the future, and vitest isolates module state per file.test/env-config.test.tsre-imports the server with stubbed environment variables, since the knobs are read once at load: bad values falling back,SCRYFALL_CACHE_TTL_MS=0as the off switch, LRU eviction, the attempt cap still issuing a request,SCRYFALL_MAX_ATTEMPTS=3000rejected against the ceiling while a knob without one still takes that magnitude, andSCRYFALL_CONTACTreaching theUser-Agent(including the empty-value fallback an optional bundle field produces).test/bundle-manifest.test.tspinsmanifest.jsonagainst the code: entry point, theuser_configwiring, the privacy policy, and the manifest's tool list against whattools/listactually serves. It also pins the version, which has four owners:package.json,manifest.json, theserverInfoliteral inserver.tsand theUser-Agentproduct token. The last two are asserted through a live server, so a bump touching only the JSON files cannot pass.test/no-http-stack.test.tspins that this repo's own source imports only the stdio transport and never an HTTP one (the SDK still pulls hono and express into the tree; that test can't prove they never load).
Nothing in npm test touches the network.
Mutation probes, all re-run 2026-09-15 against the 83-test suite, one at a time, source restored after each. The counts below are the runner's. An earlier revision of this section carried three remembered figures that were stale and one probe that killed nothing.
On the retry layer, because those cases don't share one kill:
retryAfterMsreturningnullfails three: the two asserting a honouredRetry-After(2 s, and the 10 s cap), and the one asserting a staleRetry-Afteris not carried into the next call. 80 pass. It leaves theRetry-After: 0cases green, correctly: ignoring the header and honouring a 0 now produce the same 250 ms wait, which is the invariant.retryAfterMsreturning the parsed number unguarded (Math.min(secs * 1000, 10_000), no condition) fails exactly one,ignores the HTTP-date form and falls back to the backoff; 82 pass. Neither half of that condition is pinned on its own. DroppingNumber.isFinite(secs)and keepingsecs >= 0fails nothing, and so does the reverse: an HTTP date parses toNaN, andNaN >= 0is already false, exactly asNumber.isFinite(NaN)is. The two halves are redundant for the case the test covers, and they earn their place on the ones it doesn't: a negative and anInfinityare each rejected by one half only, and both are capped downstream anyway. This entry previously claimed droppingNumber.isFinitealone failed the HTTP-date case. It doesn't; that probe passes 83/83.Dropping the
Math.maxfloor where the wait is chosen (pendingRetryAfter ?? wait) fails three, 80 pass: the twoRetry-After: 0cases (first retry and second) and the cross-call pacing case, which reads the shortened retry as a shortened gap.Stamping the pacer once per tool call again (in
rateLimited, instead of at eachfetch) fails exactly the cross-call pacing case, measuring a 0 ms gap where 100 ms is required; 82 pass.
On argument validation: neutering optionalString's type check fails exactly the six rejects a non-string optional argument cases, 77 pass. Restoring its blank-string rejection fails exactly two, 81 pass (the blank set and blank card_rulings id); it leaves the three blank page/full cases green, because those are guarded in resolvePage and optionalBoolean instead. Removing both of those guards fails exactly those three, 80 pass. Reading full by bare truthiness again fails two card_search cases, 81 pass.
On the request path: moving the body read back outside the retry wrapper (const raw = await res.text() with no catch of its own) fails exactly two, 81 pass: a body that dies mid-read is then seen once instead of three times. Changing COLLECTION_MAX from 75 to 74 fails exactly one, card_collection chunks past Scryfall's 75-identifier cap and merges pages; 82 pass.
The bundle rung is probed the same way, through its own channel: renaming one tool in server.ts's TOOLS array makes npm run bundle print FAIL bundle: tools/list is exactly the 7 tools and exit 1 (measured unpiped, since $? after a pipe is the last stage's), and the offline suite fails 2 of 83 on the same mutation. Restored, npm run bundle exits 0 and packs 3.2 MB / 10.5 MB unpacked / 2,268 files.
Call-count assertions (toHaveBeenCalledTimes, not.toHaveBeenCalled) appear at 31 sites, 21 in test/server.test.ts and 10 in test/env-config.test.ts (grep -c "toHaveBeenCalledTimes\|not\.toHaveBeenCalled" test/*.ts). Each one pins a contract: one POST per 75-chunk, no request on rejected input, cache hit vs miss, retry attempts, an env knob taking effect. Most sit next to an assertion on the payload or result; never caches card_random is the case where the call count is the only assertion, because two fetches is the not-cached contract. Policy: assert behavior and payloads, never bare invocation.
API etiquette
Follows Scryfall's guidelines: a 100 ms delay between requests, a descriptive User-Agent, and Accept: application/json. Between requests, not between tool calls: a retried call issues several, and each one advances the delay, so a call queued behind a retry still waits its 100 ms from that retry. card_collection never posts more than Scryfall's cap of 75 identifiers per request; chunked requests go through the same delay queue.
Backing off is part of that. A 429, any 5xx, and network or timeout failures are retried up to SCRYFALL_MAX_ATTEMPTS (3 by default, counting the first) with a 250 ms then 1000 ms backoff. That includes a failure that lands after the response headers, while the body is still streaming, which is a separate code path from one that lands before them and used to be the only kind not retried. A numeric Retry-After header replaces that backoff, capped at 10 s so an upstream number can't wedge the queue and never shorter than the backoff it replaced: Retry-After: 0 waits the 250 ms that attempt would have waited anyway, because honouring the header must not pace faster than ignoring it would. The HTTP-date form is ignored. Retries run inside the same serialized queue, so when Scryfall asks this process to slow down, every later call waits too. A 404 and other 4xx are not retried: "no such card" and "bad query" are real answers, and asking twice spends the rate limit to learn the same thing.
Requests time out after 15 s. GET responses are cached in memory, which Scryfall's guidelines also ask for; /cards/random never is.
Limitations
In-memory GET cache only (defaults and how to change them: Configuration above;
card_randomis never cached); nothing persists across restarts and there is no offline store.bulk_defaultlists the bulk-data endpoints; downloading them is the caller's job.The npm package named
mcp-scryfallis a different project. That unscoped name was published by an unrelated maintainer in February 2025 and sits at 0.1.1 with its ownmcp-scryfallbin, sonpx mcp-scryfallruns that server, not this one. Install this one by cloning, or from the.mcpbbundle. Publishing from this repo under that name would 403. Picking a scope is an open decision.Requests never run in parallel. Everything funnels through the one 100 ms-spaced queue, so a large
card_collection(sequential 75-identifier POSTs) takes proportionally longer. Each request times out after 15 s.Thin passthrough: beyond the compact summaries, results are Scryfall's data as returned. No legality checking, no rules logic.
AI assistance
This project was built with AI assistance (Claude). Correctness is established by the mocked-transport test suite (npm test: every tool, error paths, chunking, throttle serialization; no network), a strict typecheck, a smoke run that spawns the real server over stdio and calls live Scryfall once per tool (npm run smoke), and daily real use for deckbuilding. I review the code and stand behind it.
License
MIT © Abishai James. Card data © Scryfall; this project is unofficial and not affiliated with Scryfall or Wizards of the Coast.
Available Tools
7 toolsbulk_defaultA
List Scryfall bulk-data endpoints. Returns an array of items carrying type, name, updated_at, compressed_size and jsonl_download_uri; the caller fetches jsonl_download_uri directly. Payloads are gzipped JSONL — one card object per line, not a single JSON array. Useful for offline corpus building.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations present, the description carries the full burden. It discloses the return shape, fields, direct-fetch requirement for jsonl_download_uri, and the gzipped JSONL format with one card per line. This is substantive behavioral detail, though it omits potential details like rate limits or auth requirements.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is compact and front-loaded: primary purpose first, then response structure, then payload format, then intended use. Every sentence adds useful information without repetition or fluff.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a zero-parameter tool with no output schema, the description is thorough: it explains what the tool returns, what the fields represent, how to obtain the actual data, the file format, and why an agent would use it. No critical information for correct invocation appears missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The tool has zero parameters and 100% schema coverage, so there is nothing parameter-related for the description to explain. Per the baseline for 0-parameter tools, a 4 is appropriate because the schemas and description together leave no parameter ambiguity.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description opens with a specific verb and resource: 'List Scryfall bulk-data endpoints.' It clearly distinguishes itself from the card lookup siblings by describing a bulk-data listing capability rather than individual card queries.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The statement 'Useful for offline corpus building' provides clear context for when to choose this tool. It does not explicitly name sibling alternatives or state when-not-to-use, but the bulk-data framing makes the intended use obvious.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
card_collectionA
Batch lookup of many cards in one call (POST /cards/collection) — use this for decklists instead of one card_named call per card. Identifiers are exact-name strings (not fuzzy) and/or Scryfall identifier objects: {name}, {id}, {name, set}, {set, collector_number}. Scryfall caps one POST at 75 identifiers; longer lists are chunked into sequential rate-limited POSTs transparently. Returns {requested, found, not_found, data}: check not_found — it lists the identifiers Scryfall could not resolve. data holds compact summaries (with oracle_text) by default; pass full:true for raw card objects.
| Name | Required | Description | Default |
|---|---|---|---|
| full | No | Return raw Scryfall card objects instead of compact summaries (default false) | |
| identifiers | Yes | Cards to fetch: exact-name strings and/or identifier objects ({name}, {id}, {name, set}, {set, collector_number}) |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description carries full responsibility for behavioral disclosure. It reveals the rate-limit chunking, the exact return structure ({requested, found, not_found, data}), the importance of checking not_found, and the full:true switch for raw objects. It also clarifies that identifiers are exact-name strings (not fuzzy), adding precision. This is thorough behavioral transparency.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Three well-organized sentences. The first sentence states the purpose and the alternative; the second covers the identifier formats and rate-limit handling; the third explains the return structure and the full parameter. No wasted words, and the key usage guidance is front-loaded.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given no output schema, the description explains the return shape, the not_found field to check, and the full option. It also covers chunking and rate limits, which are critical for a batch tool. For a tool of this complexity, nothing an agent needs to call it correctly is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so the baseline is 3. The description adds value by explaining the identifier object formats in a compact way and clarifying that names must be exact (not fuzzy), which is partially redundant with the schema but reinforces the nuance. It also explains the full parameter's effect on output, going beyond the schema's bare description. This justifies a 4.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description states a specific verb and resource: 'Batch lookup of many cards in one call (POST /cards/collection)' and immediately differentiates it from the sibling card_named by saying 'use this for decklists instead of one card_named call per card.' This leaves no ambiguity about what the tool does or how it differs from alternatives.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicitly instructs when to use this tool: 'use this for decklists' and contrasts with the alternative card_named. It also mentions the rate-limit chunking behavior, giving context on how long lists are handled. The guidance is clear and actionable.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
card_fuzzyA
Fuzzy-name lookup of a Magic card. Handles typos, partial names, and alternate spellings. Returns the full card object; no close match surfaces as an error carrying Scryfall's details. Use when the caller's spelling may be wrong or incomplete.
| Name | Required | Description | Default |
|---|---|---|---|
| name | Yes | Approximate card name |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full burden. It discloses the supported input variations, the return shape ('full card object'), and the failure behavior ('no close match surfaces as an error carrying Scryfall's details'). This is strong behavioral disclosure for a lookup tool, though it omits details like rate limits or any selection/preference when multiple matches are equally close.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Three sentences, each earning its place: the core function, the behavioral details, and the usage condition. The most important identifier, 'Fuzzy-name lookup', is front-loaded, and there is no filler or repetition.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a single-parameter lookup tool with no output schema, the description covers all essentials: what it does, what input variations it accepts, what the return looks like, how errors surface, and when to reach for it. Nothing an agent needs to invoke it correctly is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100% and the schema already labels 'name' as an approximate card name. The description adds value by elaborating what 'approximate' means: typos, partial names, and alternate spellings. This enriches the parameter beyond the bare schema text.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description opens with the specific verb 'Fuzzy-name lookup' and a clear resource ('a Magic card'), then details what 'fuzzy' means (typos, partial names, alternate spellings). This distinguishes it from card_named, card_search, and card_collection without needing to open the schema.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It explicitly states when to use the tool ('Use when the caller's spelling may be wrong or incomplete'), giving a clear context. However, it does not name the alternative (e.g., card_named for exact names) or state an exclusion condition, so it stops short of a full when/when-not directive.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
card_namedA
Exact-name lookup of a Magic card. Returns the full Scryfall card object (oracle_text, mana_cost, type_line, P/T, legalities, etc.) on a hit. A miss (no card by that exact name) surfaces as an error carrying Scryfall's details; try card_fuzzy instead. Use when the caller has the exact card name.
| Name | Required | Description | Default |
|---|---|---|---|
| set | No | Optional 3-letter set code (e.g. 'mh2'); restricts to that printing | |
| name | Yes | Exact card name (case-insensitive) |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description carries the burden of behavioral disclosure. It states the miss behavior (error with Scryfall's details) and suggests using card_fuzzy as fallback, which is helpful. However, it doesn't describe edge cases like case-insensitivity (which is in schema), network behavior, or whether the tool handles partial matches. It also doesn't mention that legalities field exists but not detailed.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is concise and front-loaded: it starts with the main purpose, then describes return on hit, miss behavior, and usage guidance. Each sentence adds value without redundancy. It's tightly written and easy to scan.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a lookup tool with simple parameters (2, 1 required) and no output schema, the description is fairly complete. It covers the main use case, the error case, and suggests an alternative. However, it could be more complete by mentioning that the returned object includes many fields (which it does mention), so a 4 is justified. The lack of annotations is compensated reasonably well.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so the schema already documents both parameters. The description adds a bit of context (exact-name lookup implies the name parameter's role) but doesn't provide additional semantics beyond the schema, such as how set interacts with name or any formatting requirements. Baseline 3 is appropriate given the high schema coverage.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool does an exact-name lookup of a Magic card, identifies the resource (Scryfall card object) and differentiates from the sibling card_fuzzy. It explicitly mentions the behavior on hit (returns full card object) and on miss (error with details), which is specific and distinguishes it from other lookup tools.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description explicitly says to use when the caller has the exact card name, and mentions card_fuzzy as an alternative for when exact name is not available. However, it does not describe other siblings like card_search or card_collection, so the guidance is somewhat limited but clear.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
card_randomA
A random Magic card. An optional q= filter uses the same Scryfall query syntax as card_search. Returns the full card object.
| Name | Required | Description | Default |
|---|---|---|---|
| q | No | Optional Scryfall query to restrict the random pool |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations are provided, so the description carries the full burden of disclosure. It does usefully state the output ('Returns the full card object') and confirms the q filter reuses card_search syntax, which is meaningful behavioral context. However, it doesn't disclose that results are non-deterministic across calls, potential error behavior on invalid queries, or any Scryfall request implications.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Three tight sentences with the core purpose front-loaded and the q filter explained second. No filler or redundancy — every sentence contributes meaning.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a simple single-optional-param random fetcher, the description covers what's needed: the return shape is stated, the query construction approach is referenced, and the schema documents the sole parameter. Minor gaps like query-with-no-matches behavior and non-determinism are not addressed, but these are secondary for this tool's simplicity.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100% and the schema already documents q as an optional filter restricting the random pool. The description adds genuine value by stating that q follows the exact same Scryfall query syntax as card_search, giving the agent a concrete reference for constructing complex filters.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly identifies the resource ('a random Magic card') and the core verb ('returns'), and the word 'random' immediately differentiates it from siblings like card_search, card_named, and card_fuzzy. It's not a tautology. It could be slightly stronger by explicitly contrasting with deterministic lookup tools, but the 'random' framing carries enough distinction.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Usage intent is implied rather than stated — the tool's random nature signals when an agent would reach for it. The explicit reference to card_search's query syntax is a useful pointer for constructing the q filter, but there's no explicit when/when-not guidance (e.g., 'use card_named for a specific card, use this for arbitrary selection').
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
card_rulingsA
Official Wizards/Scryfall rulings for one card — the errata and corner-case answers that are not in the oracle text. Give an exact name (resolved via an exact lookup first) or a Scryfall card id to skip that hop. Returns Scryfall's rulings list {object:'list', data:[{published_at, comment, source}]}; empty data means the card has no rulings, which is itself an answer.
| Name | Required | Description | Default |
|---|---|---|---|
| id | No | Scryfall card id (skips the name-resolution request) | |
| name | No | Exact card name |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description carries the full burden of behavioral disclosure. It transparently describes the return format (list with data array containing published_at, comment, source), the meaning of empty data (no rulings), and the internal resolution step for names. This is thorough for a read-only lookup tool.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is two sentences: the first states the purpose and distinguishes it from oracle text, the second explains input options and return structure. It is front-loaded with the core purpose, contains no filler, and every clause earns its place.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a simple lookup tool with two optional parameters, no output schema, and no annotations, the description fully covers what an agent needs: input resolution, output format, and the edge case of no rulings. It is complete and self-sufficient.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so the schema already documents both parameters clearly. The description adds minor redundancy by repeating the exact-name and skip-hop details, but does not provide additional semantic meaning beyond the schema. Baseline 3 is appropriate.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool returns official Wizards/Scryfall rulings for a single card, distinguishing it from oracle text. It names the resource (rulings), the verb (get/fetch implied), and specifies input requirements (exact name or card id). This differentiates it from sibling tools like card_named or card_search which serve different purposes.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description explains how to invoke it: provide an exact name (which resolves via an exact lookup first) or a card id to skip that hop. This implies the correct usage context but does not explicitly mention when not to use it or name alternatives like card_named for fuzzy searches. Still, the usage context is clear enough for an agent to decide correctly.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
card_searchA
Scryfall query-syntax search (e.g. 'is:fetchland t:land', 'c:rb cmc<=2 t:creature', 'o:"draw a card" pow=1'). Returns compact per-card summaries plus total_cards/has_more by default to keep responses small; pass full:true for the raw Scryfall response. A query that matches no cards is NOT an empty list — Scryfall answers it with a 404, which surfaces as an error carrying Scryfall's details. That error is the answer 'no cards match'; do not retry it as a tool failure. Full syntax: https://scryfall.com/docs/syntax
| Name | Required | Description | Default |
|---|---|---|---|
| q | Yes | Scryfall query string | |
| full | No | Return the raw Scryfall response instead of compact summaries (default false) | |
| page | No | Page number: an integer >= 1 (default 1). Any other value is an error. | |
| order | No | Sort order: name, cmc, color, released, etc. (default: name) |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full burden and excels: it discloses the compact-summary default, the full:true override, the 404-on-no-match behavior, and instructs not to retry as a tool failure. It also points to external documentation for full syntax, giving the agent complete behavioral awareness.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is dense but efficient: it front-loads the purpose and examples, then addresses output format and error handling in a few sentences. Every sentence contributes, with no fluff or redundancy.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a query-language tool with complex syntax and non-standard error behavior, the description covers all essential aspects: query examples, output format options, pagination (via schema), sort order (via schema), and the 404 semantics. The link to full syntax ensures complete coverage.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so the baseline is 3, but the description adds significant value: it gives illustrative examples for the 'q' parameter and clarifies the 'full' parameter's default behavior. Page and order are adequately covered by schema descriptions, so no gap remains.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states it performs a Scryfall query-syntax search with concrete examples, and the sibling tools card_named/card_fuzzy are implicitly distinguished by their name-based nature. The verb 'search' and resource 'cards' are explicit, making the tool's purpose unambiguous and differentiable from siblings.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description implies usage for complex query-syntax searches through examples but does not explicitly contrast with card_named, card_fuzzy, or other siblings. It does provide a critical usage note about the 404 error being a valid 'no matches' result, which is a usage guideline, but lacks direct alternative-selection guidance.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
7 tool updates
v1.1.0- First observed
bulk_default - First observed
card_collection - First observed
card_fuzzy - First observed
card_named - First observed
card_random - First observed
card_rulings - First observed
card_search
TDQS
Scored across 7 tools
Each tool targets a distinct lookup mode: exact name, fuzzy name, query search, batch collection, random, rulings, and bulk data. Even the two name-based lookups are clearly separated by exact vs. fuzzy intent, and the descriptions explicitly call out when to use which.
Six of seven tools follow a consistent 'card_' prefix and snake_case naming (card_named, card_fuzzy, card_search, card_collection, card_random, card_rulings). The outlier 'bulk_default' breaks the prefix pattern, but it's still readable and the rest form a clear family.
Seven tools is well-scoped for a card-database server: single-card lookups, search, batch, random, rulings, and bulk data. Each tool earns its place with a distinct function, and the count is neither thin nor overwhelming.
Core card access is fully covered: exact/fuzzy lookup, query search, batch decklist resolution, rulings, random, and bulk corpus download. Minor gaps exist (e.g., no direct set/expansion list or card-by-ID endpoint), but these are workarounds via card_collection or card_search, and the typical Scryfall workflows are complete.
Maintenance
Related MCP Connectors
Scryfall MCP — Magic: The Gathering card database.
Official remote MCP server for Archivist AI TTRPG campaign memory: characters, sessions, and more.
Official MCP server for subfeed.app — the cloud for agents. 15+ tools for AI agents to register, build, and deploy other agents. Zero human required. Start here: subfeed.app/skill.md
Official MCP server for Certifier to issue, manage, and track certificates and badges.
Related MCP Servers
- AlicenseAqualityCmaintenanceMagic: The Gathering MCP server with card search, rules lookup, deck analysis, and Commander intelligence1430 npm4MIT
- AlicenseNot gradedqualityDmaintenanceMCP server for searching and retrieving Magic: The Gathering card information via the Scryfall API, with support for field presets, multiple output formats, and automatic rate limiting.72,932 npmMIT
- AlicenseNot gradedqualityDmaintenanceMCP server that searches and retrieves Magic: The Gathering card data from the Scryfall API.72,932 npmMIT
- AlicenseNot gradedqualityBmaintenanceUnified MCP server for Magic: The Gathering, combining Scryfall card search and pricing, EDHRec commander recommendations, Archidekt deck reading, and decklist validation into a single service.MIT