MCP server that condenses GraphQL introspection into query ready schema digests, ~30x fewer tokens.
AI agents that query subgraphs have to learn the schema first, and when the only way in is GraphQL introspection — a self-hosted graph-node, a Subgraph Studio dev endpoint, an unpublished deployment, any GraphQL API at all — that lesson is brutally expensive. graph-node auto-generates, for every entity, a _filter input type carrying a comparison variant for every single field, plus an _orderBy enum listing every field again. Uniswap V3's 23 entities expand into 83 introspection types and 161,647 tokens of JSON. Aave V2 on Ethereum hits 323,136.
Almost none of that carries information. A model already knows that a field called totalValueLockedUSD supports totalValueLockedUSD_gt, because those rules are mechanical. It only ever needed to know the field exists.
This MCP server fetches introspection, strips every generated type, and returns a compact digest of what actually matters — entities, fields, relationships, domain enums, referenced custom scalars — prefixed with a nine-line header that states the filter and ordering conventions once instead of enumerating their expansion thousands of times.
Measured across 30 live subgraphs (25 distinct schemas, 30 of 30 fetched successfully, nothing mocked): 5,337,846 tokens of raw introspection reduce to 177,728 tokens of digest. Median reduction 32x, range 20-78x, and still 6-21x against the fairer baseline of printed SDL rather than raw JSON.
Small is the easy half. Sufficient is the hard half. In Claude Desktop with only this server connected, a model was asked for the 10 most recently registered ENS domains. It read the digest, wrote a query using orderBy, orderDirection and first — none of which appear anywhere in the digest — traversed Registration to Domain to owner correctly, and returned real on-chain data on the first attempt. It then volunteered, unprompted, that Domain also exposes registrant and wrappedOwner and that these can differ from owner. That is a model reasoning about a data model instead of parsing filter permutations, which is the entire point.
Scope, stated honestly: The Graph's official Subgraph MCP reads the authored schema.graphql from the deployment manifest, which is a genuinely cheaper source. This tool does not compete with it on published subgraphs and the README says so outright. It exists for every case where that manifest isn't reachable and introspection is the only option.
TypeScript on Node. @modelcontextprotocol/sdk for the server over stdio, graphql for introspection types and schema utilities, graphql-request against The Graph's gateway, gpt-tokenizer (cl100k_base) so the token counts are real rather than estimated, Vitest for tests, tsx to run without a build step. No database, no frontend, no contracts, no deployed subgraph.
The core is a single pure function — condense(introspection) → Digest — with no I/O, no state and no network, which is what makes it exhaustively testable. It drops introspection meta-types, the operation roots, graph-node scaffolding (Meta, Block, Block_height, BlockChangedFilter, SubgraphErrorPolicy), the OrderDirection enum, and anything ending in _filter or _orderBy. Input objects are dropped structurally rather than by name, since every INPUT_OBJECT in a graph-node schema is generated machinery. Custom scalars survive only if a kept field actually references them. Domain enums are kept deliberately — dropping all enums would be simpler and would silently break real queries. Output is sorted alphabetically so digests are byte-stable and diffable in CI.
The thing that actually makes it work isn't the stripping, it's the replacement: a nine-line convention header stating the filter and ordering rules once. That's what lets you delete 98% of a schema and still have a model write a valid query on the first try.
Testing is three layers. A hand-built introspection fixture written inline in the test file that specifies each drop rule as its own assertion, including the NON_NULL(LIST(NON_NULL(Object))) type unwrapping that's the easiest thing in the codebase to silently corrupt. A named check against the real Uniswap V3 fixture. Then it.each invariants across all 30 saved fixtures: no boilerplate leaks (regex-based, so it catches generated types this codebase has never seen) and a rawSize/digestSize > 10 guard that fails the build if a future change starts leaking types back in and quietly invalidates the published numbers.
Two reproducible measurement scripts: npm run measure live-fetches all 30 subgraphs and saves fixtures plus two baselines, npm run condense runs fully offline and emits the reduction table as both JSON and markdown. The reduction range is wide (20x to 78x) and the cause is identifiable — per-entity digest cost runs from 82 tokens to 549 depending entirely on whether the subgraph author wrote GraphQL docstrings, which introspection preserves and the condenser keeps because they carry real semantic signal.
The notable part is a rescope. This was built on the assumption that The Graph's official MCP returns introspection. Mid-build I pulled the Rust source and grepped it: there is no introspection call anywhere in the repo. get_schema_by_subgraph_id queries the network subgraph for manifest { schema { schema } } — the authored source file. The premise was wrong. Rather than ship a 50-100x headline that dies at a judge's first question, I re-aimed the whole thing at introspection-only endpoints, deduplicated the sample (30 IDs, only 25 distinct schemas — several Aave and Compound deployments share the Messari standardized schema), and published the honest range instead of the flattering multiplier. The README states plainly where the tool doesn't help.

