WRITING July 4, 2026 8 min read

Building an MCP Server for Database Schema: How I Built Tome

Designing MCP tools that give AI agents real-time database schema awareness, instead of making them read migration files and guess.

Every time an AI agent needs to know what a database table looks like, it reads migration files. It finds the create table statement, then hunts for every subsequent alter table, then tries to reconcile the current state from an append-only changelog that might be three hundred files deep. This is archaeology. The agent is reconstructing history when it could just look at what’s there right now.

That’s why I built Tome. It connects to the actual database, introspects the full schema once, caches it in BadgerDB, and exposes four MCP tools that answer structural questions in single-digit milliseconds. No migration reading. No guesswork. The database tells you what it is.

What agents get wrong about schemas

The default behavior for any AI coding agent when it encounters a database question is: find the migrations directory, read the relevant create table, scan for alters, then maybe cross-reference with the ORM models for good measure. This is bad for a few reasons.

First, migrations are append-only history, not current state. A table created in migration 001 and altered in migrations 014, 037, 089, and 142 requires the agent to mentally replay all five files to understand what the table looks like today. That’s expensive in tokens and error-prone.

Second, many frameworks don’t declare schema in one place. Laravel models don’t list their columns. Django models do, but the migration might have diverged from the model if someone ran a manual ALTER TABLE. Rails has a schema.rb that’s supposed to be canonical, but it’s generated and might be stale.

Third, the agent has to triangulate. It reads the migration, then the model, then maybe a factory or seeder to figure out which columns are required and what valid values look like. That’s three to six file reads to answer “what columns does the orders table have?”, a question the database itself can answer in one query.

The fix is to stop asking files and start asking the database.

The four tools and why four

Tome exposes four MCP tools, each designed for a specific class of agent question:

tome_describe: the common case. “What does this table look like?” Returns columns with types, nullability, defaults, indexes, and foreign keys. This is the tool agents call 80% of the time. It replaces the entire migration-reading ritual with a single call that returns ground truth.

tome_relations: FK navigation in both directions. “What does this table reference?” and “What references this table?” The bidirectional part matters. When an agent is planning a feature that touches the orders table, it needs to know both what orders depends on and what depends on orders.

tome_search: discovery. “Which tables mention ‘order’?” Substring search across table names and column names. This is the tool agents use when they don’t know which table to ask about yet. It replaces grepping the migrations directory for a keyword.

tome_enums: domain values. “What are the valid statuses for an order?” Returns the allowed values for enum and set columns. Without this, the agent has to find wherever those values are defined in application code, which might be a config file, a constant, a migration, or nowhere explicit at all.

Four tools instead of one mega-tool because each serves a different moment in the agent’s reasoning. Discovery first (search), then structure (describe), then relationships (relations), then domain values (enums). An agent building a feature typically hits all four in that order.

Why bidirectional FK lookup matters

This is a design choice that seems obvious in retrospect but wasn’t when I started. The natural way to think about foreign keys is forward: this table references that table. But agents ask the reverse question more often. “What references the users table?” is the impact question. If I’m changing the users table, I need to know everything that depends on it.

Building both directions at index time is trivial. When Tome introspects the schema, it gets a list of foreign key constraints. Each constraint has a source table/column and a target table/column. One pass through that list, two writes per constraint: one keyed by source table (forward lookup) and one keyed by target table (reverse lookup). The cost is negligible. The value is that tome_relations can answer both “what does this table reference” and “what references this table” from the same index without any additional queries.

Agents use the reverse direction roughly 3:1 over forward. They’re usually asking about impact, not provenance.

The init and refresh pattern

Tome starts with tome init. By default it reads the DSN from your .env file (--detect-env flag), connects to the database, introspects everything, and writes the full schema into a local BadgerDB store. From that point on, the MCP tools read from the local cache, not the live database.

The refresh question was interesting. Schemas change: someone adds a column, creates a table, drops an index. The cache needs to stay current. I considered three approaches:

  1. Poll on interval: connect to the database every N minutes and re-introspect.
  2. Watch for migrations: monitor the migrations directory for new files and re-introspect when one appears.
  3. Manual refresh: the user runs tome refresh when they know the schema changed.

I went with manual. Schemas change a few times a day at most. Polling every 30 seconds is wasteful and creates unnecessary database connections. Watching migration files misses manual changes and adds filesystem complexity. Manual refresh is explicit, instant, and zero-cost when the schema hasn’t changed.

In practice, I run tome refresh after running migrations. It takes under a second. The agent always has current schema because I refresh when I change things, which is the only time staleness matters.

Cross-database normalization

Tome supports MySQL and PostgreSQL, and they store metadata very differently. The challenge is producing the same MCP response format regardless of which engine is behind the connection.

The biggest divergence is enums. MySQL stores enum values inline in the COLUMN_TYPE field as enum('active','inactive','suspended'). You parse the type string to extract values. PostgreSQL stores enum types separately in pg_enum and pg_type, which you join to get the values for a given column’s type.

Same story with indexes. MySQL’s information_schema.STATISTICS and PostgreSQL’s pg_indexes plus pg_index give you the same conceptual data in completely different shapes.

The approach is a driver interface. Each database engine implements a Driver that knows how to query its system catalogs and return normalized structs. The MCP tools operate on those normalized structs. The agent never sees engine-specific details: it gets the same JSON whether the backing database is MySQL 8 or PostgreSQL 16.

This matters because the agent shouldn’t need to know which database engine you’re running. “What does the orders table look like?” has the same answer format regardless of whether it’s stored in MySQL or Postgres. The normalization layer absorbs that complexity.

What I’d add next

Two things from the MCP spec that Tome doesn’t use yet but should.

Resources: MCP resources are static data the agent can pull into context. A full schema dump as a resource would let the agent load the entire database structure into its context window at the start of a session, rather than querying table by table. For a 50-table database, that’s maybe 4-5k tokens and gives the agent a birds-eye view before it starts asking specific questions.

Prompts: MCP prompts are reusable prompt templates. I’d add prompts for common analysis tasks: “analyze this table’s indexing strategy,” “identify missing foreign keys,” “suggest denormalization opportunities.” These would combine Tome’s schema data with a structured prompt that guides the agent through an analysis framework. The agent would call the prompt, get back a filled template with the relevant schema data already embedded, and reason from there.

Neither of these is urgent because the four tools cover the daily workflow well. But resources in particular would be a nice affordance for agents that want the full picture upfront.

Where it fits

Tome is one piece of a broader context layer I’ve been building. The MCP servers guide covers what the protocol is and why it matters. The Go implementation guide covers the nuts and bolts of building MCP servers in Go specifically. And The Context Layer covers how Tome, Scry, Lore, and Flume work together as a stack.

The thesis across all of them is the same: agents are fast when they already know things, and slow when they have to find things. Database schema is one of the things agents need to know constantly and currently find by reading files that were never meant to be a schema reference. Connecting to the actual database and caching the result is obvious in retrospect. It took building it to realize how much migration-reading time it was replacing.

Frequently Asked Questions

What is a database MCP server?

A database MCP server connects directly to a live SQL database, introspects its schema (tables, columns, types, indexes, foreign keys, enums), and exposes that information through MCP tools that AI agents can query in real time. Instead of reading migration files or ORM models, the agent gets the current schema from the actual database.

How do SQL schema MCP tools work?

SQL schema MCP tools query the database's information_schema (or equivalent system catalogs) to extract structural metadata, then cache that metadata locally for fast retrieval. When an AI agent needs to know what a table looks like, it calls the MCP tool and gets back structured JSON with columns, types, indexes, and relationships, without any file reading or guessing.

Can an MCP server work with both MySQL and PostgreSQL?

Yes. Tome supports both MySQL and PostgreSQL by normalizing their different system catalog formats into a single response schema. Each database engine stores metadata differently (MySQL uses COLUMN_TYPE inline strings, PostgreSQL uses pg_enum and pg_type), but the MCP tools return the same structured format regardless of which engine is behind the connection.

Why use an MCP server for database schema instead of reading migrations?

Migrations are append-only history: they tell you what changed, not what currently exists. An agent reading migrations has to replay the entire history mentally to figure out the current state, and it will miss manual changes, column renames buried in migration 47 of 200, or schema drift between environments. An MCP server connects to the live database and returns ground truth.

Back to all writing