Skip to main content
Glama

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

card_named

Exact-name lookup (optional set code). Full card object for one printing (the most recent unless set is given), plus printings (every printing oldest first: set, set_name, released_at, rarity, collector_number), printings_count, first_printing, and a record_scope sentence saying which printing the object is and where the card debuted. One extra Scryfall request per lookup, cached.

card_fuzzy

Fuzzy-name lookup. Handles typos and partial names. Same one-printing object, printings list and record_scope as card_named.

card_search

Scryfall query-syntax search. Returns compact summaries by default (pass full: true for raw objects). A query matching no cards is an error, not an empty list, because Scryfall answers a zero-result search with a 404.

card_collection

Batch lookup (POST /cards/collection): resolve a whole decklist in one call. Takes exact-name strings and/or {name} / {id} / {name, set} / {set, collector_number} identifiers; misses come back in not_found.

card_random

A random card, optionally filtered by a query.

card_rulings

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.

bulk_default

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 install

Runs 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

SCRYFALL_CONTACT

this repository's URL

Added to the User-Agent, per Scryfall's API guidelines, so they can reach you about traffic.

SCRYFALL_CACHE_TTL_MS

86400000 (24 h)

Lifetime of a cached GET response. 0 turns the cache off.

SCRYFALL_CACHE_MAX

500

LRU entry cap for that cache. Floor 1.

SCRYFALL_MAX_ATTEMPTS

3

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 bundle

Writes 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 smoke

test/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 tool

Tiers 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.ts drives every tool through a real MCP client over the in-memory transport and asserts on the URLs and POST bodies sent to the mocked fetch and on the JSON returned: error surfacing (404, 429, non-JSON), the 75-identifier chunking in card_collection, the abort timeout, rate-limit serialization, the response cache (hit, never for card_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-string set, order or q, a non-boolean full, a page that 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 that card_search's outgoing page and echoed page are the same value.

  • test/retry-timing.test.ts measures the retry waits on a fake clock, which is the only way to tell a honoured Retry-After from an ignored one, including that a honoured header never waits less than the backoff it replaced, so Retry-After: 0 waits 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.ts re-imports the server with stubbed environment variables, since the knobs are read once at load: bad values falling back, SCRYFALL_CACHE_TTL_MS=0 as the off switch, LRU eviction, the attempt cap still issuing a request, SCRYFALL_MAX_ATTEMPTS=3000 rejected against the ceiling while a knob without one still takes that magnitude, and SCRYFALL_CONTACT reaching the User-Agent (including the empty-value fallback an optional bundle field produces).

  • test/bundle-manifest.test.ts pins manifest.json against the code: entry point, the user_config wiring, the privacy policy, and the manifest's tool list against what tools/list actually serves. It also pins the version, which has four owners: package.json, manifest.json, the serverInfo literal in server.ts and the User-Agent product 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.ts pins 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:

  • retryAfterMs returning null fails three: the two asserting a honoured Retry-After (2 s, and the 10 s cap), and the one asserting a stale Retry-After is not carried into the next call. 80 pass. It leaves the Retry-After: 0 cases green, correctly: ignoring the header and honouring a 0 now produce the same 250 ms wait, which is the invariant.

  • retryAfterMs returning 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. Dropping Number.isFinite(secs) and keeping secs >= 0 fails nothing, and so does the reverse: an HTTP date parses to NaN, and NaN >= 0 is already false, exactly as Number.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 an Infinity are each rejected by one half only, and both are capped downstream anyway. This entry previously claimed dropping Number.isFinite alone failed the HTTP-date case. It doesn't; that probe passes 83/83.

  • Dropping the Math.max floor where the wait is chosen (pendingRetryAfter ?? wait) fails three, 80 pass: the two Retry-After: 0 cases (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 each fetch) 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_random is never cached); nothing persists across restarts and there is no offline store. bulk_default lists the bulk-data endpoints; downloading them is the caller's job.

  • The npm package named mcp-scryfall is a different project. That unscoped name was published by an unrelated maintainer in February 2025 and sits at 0.1.1 with its own mcp-scryfall bin, so npx mcp-scryfall runs that server, not this one. Install this one by cloning, or from the .mcpb bundle. 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 tools
bulk_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.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

TDQS

A4.5/5.0
Behavior4/5

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.

Conciseness5/5

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.

Completeness5/5

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.

Parameters4/5

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.

Purpose5/5

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.

Usage Guidelines4/5

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
fullNoReturn raw Scryfall card objects instead of compact summaries (default false)
identifiersYesCards to fetch: exact-name strings and/or identifier objects ({name}, {id}, {name, set}, {set, collector_number})

TDQS

A4.9/5.0
Behavior5/5

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.

Conciseness5/5

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.

Completeness5/5

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.

Parameters4/5

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.

Purpose5/5

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.

Usage Guidelines5/5

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
nameYesApproximate card name

TDQS

A4.5/5.0
Behavior4/5

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.

Conciseness5/5

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.

Completeness5/5

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.

Parameters4/5

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.

Purpose5/5

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.

Usage Guidelines4/5

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
setNoOptional 3-letter set code (e.g. 'mh2'); restricts to that printing
nameYesExact card name (case-insensitive)

TDQS

A4/5.0
Behavior3/5

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.

Conciseness5/5

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.

Completeness4/5

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.

Parameters3/5

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.

Purpose5/5

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.

Usage Guidelines4/5

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
qNoOptional Scryfall query to restrict the random pool

TDQS

A3.6/5.0
Behavior3/5

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.

Conciseness4/5

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.

Completeness4/5

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.

Parameters4/5

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.

Purpose4/5

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.

Usage Guidelines3/5

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
idNoScryfall card id (skips the name-resolution request)
nameNoExact card name

TDQS

A4.5/5.0
Behavior5/5

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.

Conciseness5/5

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.

Completeness5/5

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.

Parameters3/5

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.

Purpose5/5

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.

Usage Guidelines4/5

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.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 7 tool updatesv1.1.0
    • First observedbulk_default
    • First observedcard_collection
    • First observedcard_fuzzy
    • First observedcard_named
    • First observedcard_random
    • First observedcard_rulings
    • First observedcard_search

TDQS

A4.2/5.0

Scored across 7 tools

Disambiguation5/5

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.

Naming Consistency4/5

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.

Tool Count5/5

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.

Completeness4/5

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

ActivityActive
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    MCP 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 npm
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    Unified 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