DEV Community

Douglas D
Douglas D

Posted on

My MCP server's worst first-run error was ugly. Auto-fixing it was the wrong move.

The first thing a new user does with Founders OS is usually something like "add Acme as a customer." If their database is blank, that command hits a raw PostgREST error, table not found, and stops there. No pointer to what went wrong or what to do about it. Not a great first five minutes.

The obvious fix is to have the server just create its own tables on a blank database. I spent a session this week working through whether to do that, and I didn't.

Here's the reasoning, in the order it actually convinced me.

The deciding fact: I connect through a PostgREST client, not a raw Postgres connection. PostgREST has no DDL path, there's no bootstrap function sitting in the setup script either, I checked. To auto-provision at all I'd need a strictly more powerful credential I don't currently collect: either a direct Postgres connection string plus a pg dependency in the bundle, or a Supabase Management API token and project ref. That's a much bigger ask of a self-hoster than the API key they already hand over.

Even granting that credential, the case against held up:

"Blank" is an inference, not a fact. A transient network error or a typo'd URL looks identical to an empty database from where the server sits. Running DDL on that assumption is not something you can take back if the assumption was wrong.

The setup SQL isn't safe to run blindly either. It has a vector dimension placeholder that has to match whatever embedding provider is configured, so an automatic run risks a schema that doesn't match the deployment it's sitting in.

Concurrent boots are a real scenario for me, not a hypothetical. Claude Desktop, Cursor, and a scheduled task runner can all start against the same database around the same time. Auto-DDL with no locking is a race condition I'd be shipping on purpose.

The failure mode is invisible exactly where it matters. A stdio server that errors on startup just shows the user "server failed to start." So the riskiest step would run precisely where nobody can watch it happen or stop it.

And it only solves the one-time case. The problem that actually recurs is migrations, not first boot. My own dev database is sitting at schema version 42 against a server build that expects 37 right now. That mismatch is the daily reality, not the blank-database case.

Last one, and honestly the one that mattered most: this server is self-hosted so people can own their business data. If it starts writing DDL against your database without you watching, that promise stops meaning anything.

What I'm building instead, no DDL anywhere in it: the schema-classification logic already existed and already knew how to tell current from behind from ahead from blank, it just wasn't reachable unless someone specifically asked for a version check. So the fix is to pull that classification into one place so it can't drift between call sites, catch the specific error codes centrally where every tool already funnels through and turn them into a structured "setup required" response instead of a raw database error, and surface the same hint proactively at session start instead of waiting to be asked.

One rule I'm being strict about: match on error codes only, never on message text. A real bug, a constraint violation, a network failure, has to pass through unchanged. Get that wrong and actual bugs get silently mislabeled as a setup problem.

None of this needed a smarter model or a cleverer prompt. It needed sitting with an ugly failure mode long enough to find the boundary between "convenient" and "safe with someone else's data."

Founders OS is open source: github.com/OurThinkTank/founders-os.

Site: foundersmcp.com.

Top comments (0)