DEV Community

Michael Kaminski
Michael Kaminski

Posted on Originally published at michael-kaminski.io

One Knowledge Base, Four Surfaces: Pages, Graph, Search Index, and MCP

Originally published on michael-kaminski.io.

The Genome of Games publishes the same 1,180 records four different ways, and one command writes all four: node build.js, 0.39 seconds, zero npm dependencies.

Out come 1,245 static HTML pages for crawlers, an interactive canvas graph for humans, a 129,037-byte search index for the site's own search box, and a Model Context Protocol server exposing 8 tools to agents.

The decision worth copying is the one that sounds like a downgrade. The MCP server does not query the site and does not read the source data. It statically imports a 1.9 MB index that the build wrote. There is exactly one place where slugs, lineage, and adoption edges get joined, so an agent and a crawler cannot come back with different answers.

The dataset is an ontology of video game mechanics — 168 mechanics, 618 games, 394 companies, 4,366 recorded links, 1962 to 2025. What the records are about does not matter here. The shape of the problem shows up anywhere a structured knowledge base has to serve both a search engine and a model.

Four surfaces, one build, a twelve-fold expansion

Six hand-edited JSON files under data/ are the source of truth: the feature ontology, the graph, the prose, the company registry, the site copy, and the verified outbound links. Together they are 1,312,577 bytes.

The build turns that into 16,644,215 bytes of generated read surface. A 12.7× expansion, and every byte of it is disposable.

Surface Consumer Bytes Per entity
1,245 static HTML pages Crawlers, humans 14,613,203 11,728 / page
mcp-index.json → MCP server Agents 1,901,975 1,612
search-index.json The site's own search box 129,037 109
/graph/ canvas Humans exploring lineage data injected at build

The build also emits sitemap.xml with 1,245 entries, llms.txt, robots.txt, and a 404 page. The same run reports 96,843 internal links across those pages.

Nothing in that list is authored. Delete the whole output directory and the next build restores it in under half a second.

The agent surface is a build artifact, not a query path

The obvious way to serve an agent is to put an API in front of the data and let the MCP server call it. That is the version that rots.

An API layer has to re-derive the same things the page renderer derives — how a name becomes a slug, which parents count as ancestors, which adoption edges are shown. Two implementations of one join is two implementations that will disagree, and the disagreement surfaces as an agent confidently citing a URL that renders something else.

So build.js writes data/mcp-index.json as a build step, and api/mcp.mjs opens with a static import of it. The serverless function holds no derivation logic at all. Its first line of real work is const { meta, families, eras, entities } = INDEX.

The protocol itself is spoken by hand — JSON-RPC over POST, no MCP SDK, for the same reason there is no Stripe or Supabase SDK anywhere in the repo. initialize, tools/list, tools/call, ping, and two notifications is the entire surface area. The whole server is 21,508 bytes.

You can check it from a terminal:

curl -s -X POST https://genome-of-games.vercel.app/api/mcp/ \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
Enter fullscreen mode Exit fullscreen mode

Eight tools come back: overview, search, get_mechanic, get_game, get_studio, trace_lineage, list_family, by_year.

The agent pays 14.7× the search index, per entity, and should

Same 1,180 entities, two indexes, wildly different budgets. The search index spends 109 bytes per entity. The MCP index spends 1,612. That ratio is 14.7×, and it is the most useful number in the build.

The search box only needs enough to rank a substring match and hand over a URL: name, type, year, path. Everything else is one navigation away, and the human doing the navigating is the retrieval system.

An agent has no second hop it can afford. If genome_get_mechanic returns a stub, the model either guesses or makes four more tool calls, and both outcomes are worse than a fat payload. So the MCP record carries the credited origin game and its developer, the full prose essay, parents, children, everything downstream, and the later adopters — pre-joined.

The generic version of this: size a machine-readable surface by how many round trips the consumer can tolerate, not by what looks tidy. Humans tolerate many. Agents tolerate roughly one.

The same logic drives the tool descriptions. genome_get_overview spends its budget telling the model that "origin" means the first notable shipped implementation rather than invention, that over-the-shoulder aim is credited to kill.switch in 2003 rather than Resident Evil 4 in 2005, and that 949 of 1,180 entities carry a verified Wikipedia permalink while the remaining 231 carry none. Coverage gaps are a tool output, not a footnote.

Determinism is what makes any of this checkable

Because mcp-index.json is committed rather than gitignored, a rebuild is a falsifiable claim. I ran the build three times: 0.40s, 0.39s, 0.39s. The index md5 was ba9928a35651fb201b4e0c3e0cab61d7 before and after, and git status reported zero changed files.

That is the whole verification story. If a build ever produces a diff on a run where the source did not change, the pipeline has picked up a clock, a hash seed, or a network call, and the "one join" guarantee is already gone.

Committing generated output is usually bad practice. Here it buys a cheap integrity test on every pull request, and it keeps the build offline and reproducible — the Wikipedia verification pass runs separately, by hand, and writes its results into the committed data.

The cost: the surface the build does not own has already drifted

Single source of truth holds only for surfaces inside the build step. The README is outside it, and it is already wrong.

The README says 1,243 pages and 89,351 internal links. The build says 1,245 and 96,843. Off by two pages, and off by 7,492 links — an 8.4% understatement, sitting in the first file anyone reads.

Nobody edited a number badly. Content was added, every generated surface absorbed it silently, and the one hand-written surface stayed where it was. That is the failure mode this architecture produces: it does not create disagreement between surfaces it owns, and it hides disagreement with surfaces it does not.

The fix is not discipline. It is either generating the README's numbers too, or asserting them in the build and failing loudly. I have done neither yet, which is why this paragraph exists.

There is a second bill. The MCP function statically imports 1.9 MB, so every cold start pays for the whole dataset whether the caller wanted one mechanic or the overview. At this size that is a fine trade. At 20 MB it would not be, and the answer then is a real index with range reads — which reintroduces exactly the derivation layer this design removed.

What I would copy, and what I would not

Copy the direction of the dependency. Generated artifact in, no query out. The agent surface should be downstream of the same build that produces the pages, never a sibling of it.

Copy the honesty budget in the tool descriptions. A model that is told where a dataset is thin cites it more carefully than one that is handed clean-looking records.

Do not copy the static import past a few megabytes, and do not copy hand-rolled JSON-RPC into a codebase that already has dependencies — the only reason it is defensible here is that the alternative was the repo's first one.

If you are shipping a knowledge base to both crawlers and agents, I want to know what your per-entity byte ratio is between the two surfaces. Mine is 14.7×. I have not seen anyone else publish theirs, and it is the number that decides whether the agent has to make a second call.

Top comments (6)

Collapse
 
bulti_global profile image
Bulti

The one-build guarantee solves consistency after a source is selected, but I would keep that separate from whether an external AI system selects or cites the source at all. In a scan we ran across 284 Korean DTC brands and 50 AI shopping questions per brand, 65.5% had zero appearances and the mean was only 0.648 out of 50. A site can have perfectly aligned HTML, llms.txt, and MCP outputs and still be absent at the selection stage. For this architecture, I would add a fifth observed-outcome surface that the build does not own: frozen prompts, repeated runs, mention/citation/route-to-owned-domain states, all versioned against the build hash. Have you tested whether the MCP tools and rendered pages return the same answer for identical entity questions, then whether that consistency changes external citation behavior?

Collapse
 
makaminski1337 profile image
Michael Kaminski

Fair split, and the second half is the one I can't answer.

Parity here is structural, not measured. The MCP tools and the pages read the same committed index, so there is one join and one place a slug can be wrong. What the post verifies is determinism — three builds, 0.39s, index md5 unchanged, zero changed files. That is the build not lying to itself, not a differential test across the two surfaces.

Your test is cheap and I don't have it: freeze a set of entity questions, hit genome_get_mechanic and the rendered page, diff, stamp the result with the build hash.

Where I'd push back: 65.5% is a selection-stage number, and the scan doesn't hold parity as a variable. It shows aligned surfaces aren't sufficient. It doesn't yet price what they buy.

Collapse
 
mark2phillips9 profile image
Mark2Phillips9

The integration of multiple surfaces in knowledge base architecture enhances user experience by providing diverse access points to information. This can lead to increased engagement and potentially improved SEO outcomes as user behavior signals are captured across various interfaces.

Collapse
 
makaminski1337 profile image
Michael Kaminski

The access points aren't interchangeable, and that's the part worth pulling on.

The search index spends 109 bytes per entity. The MCP index spends 1,612 for the same 1,180 records — 14.7x. A human takes a second hop; an agent can't afford one, so the payload has to be pre-joined.

Engagement signal isn't what carries it. The single join is. Four surfaces off one build means a crawler and a model can't return different answers for the same slug.

Where it does break is the surface the build doesn't own. My README still says 1,243 pages and 89,351 internal links against the build's 1,245 and 96,843.

Collapse
 
salparvez profile image
Sal Parvez | ML Systems • Edited

The direction of the dependency is the part I'd copy too. ML Systems runs the same shape on a house: seven agents and a homeowner write claims into one record (the Master Ledger), typed against one ontology — the Collective Ontology, which is where the mechanics-to-games kind of edge lives for us (rafter sits on plate, sheathing is fastened through rafter). Every surface — the app, the lender's view, the crew's cut plan — reads a derived artifact, the HomeGenome, that the compression step emits per property and per cycle. Nothing reads the ledger live and nothing writes to a surface. An agent and a human can't come back with different answers because there is exactly one join.

The thing I'd add to your honesty budget: put it on the edge of the ontology, not only in the tool description. Each of your 4,366 links has a source; ours carries an evidence grade — MEASURED > STATED > RECORD > MODELED — so a rafter-to-plate edge is MODELED from a photo before the roof comes off and MEASURED after. Same build, same surfaces, but the model knows which edges to cite carefully without a prose warning.

Per-entity byte ratio between the crawler surface and the agent surface: I haven't measured ours. Now I will.

Some comments may only be visible to logged-in visitors. Sign in to view all comments.