Skip to content

Config drift: mcp_settings.json silently goes stale after the first DB migration #820

@salmon-21

Description

@salmon-21

Summary

Once MCPHub boots in Database mode (DB_URL set), mcp_settings.json is read once to seed Postgres and is never consulted again. Subsequent file edits silently have no effect, and dashboard / API edits never get written back to the file. There's also no on-disk indicator of whether the current state diverges from the file. This produces a few related operator failure modes that took us a while to triangulate during initial setup of a claude.ai custom-connector deployment.

Observable symptoms

  1. File edits don't apply. Operator edits mcp_settings.json (e.g. to add "args": [] to a stdio server that needs it), restarts the container, sees no change. The only way out is docker compose down -v to wipe the DB and re-seed, which also wipes admin / OAuth / token state.
  2. File and DB diverge. Dashboard edits diverge the DB from the file. There's no in-product cue telling the operator the file they're staring at is no longer authoritative.
  3. Origin is invisible. Once in the DB, a server has no origin / managedBy / source field. Whether it came from the file, the dashboard, or the API is only inferable from owner = NULL heuristically — which conflicts with the permission model rework in Social-login users default to isAdmin=false with no documented path to admin #817 if we redefine NULL there.

Possible directions

Each row works on its own; they can also be combined:

A. Source of truth

Option Behaviour
A1. mcp_settings.json is purely an init seed (current behaviour, documented). Startup logs a warning when mcp_settings.json mtime is newer than the DB's migration timestamp. No automatic re-import.
A2. Reconcile on startup. mcp_settings.json is re-read every boot and upserted into the DB. UI edits get overwritten unless the server is marked managedBy: 'ui'.
A3. Two-way sync. UI edits write back to the file; file edits are detected via fs watcher and applied to the DB. Highest complexity.
A4. Rename file post-migration. Move mcp_settings.jsonmcp_settings.imported.json (or move it inside the container's data volume) on successful migration. Physically prevents the "I edited the file and nothing happened" failure.
A5. Explicit re-import CLI. DB-only by default, mcphub config import (or a dashboard button) explicitly re-applies the file.
A6. Grafana-style allowUiUpdates. Per-server managedBy: 'file' | 'ui'. File-managed servers are read-only in the UI; UI-managed servers are immutable from the file.

B. Origin tracking

Option Behaviour
B1. origin enum column 'file' | 'dashboard' | 'api' (or similar). Read-only metadata, useful for filtering and audit.
B2. source string Free-text like "mcp_settings.json:toggl" for full traceability.
B3. managedBy column (composes with A6) 'file' | 'ui'. Drives whether the server is editable in the UI.
B4. No-op, lean on owner = NULL as a heuristic Status quo. Breaks if NULL semantics change for permission reasons (#817).
B5. Separate server_labels table Flexible key/value tags. Heaviest but most future-proof.

Relation to #817

The permission rework in #817 (visibility column) sits in the same migration. Bundling B1 or B3 with that migration is much cheaper than landing them separately. They're also semantically adjacent: a 'file' origin server is naturally a 'public' visibility server in most single-user / small-team deployments.

Recommendation

Lowest-cost combination that closes the operator-pain reports: A1 (warning on stale file) + B1 (origin column). Lands in one small migration, doesn't change existing behaviour, makes the divergence diagnosable.

A4 (rename mcp_settings.json after a successful migration) is also worth considering alongside — it's effectively free to implement and physically prevents the "I edited the file and nothing happened" failure mode that A1's warning only flags after the fact. Combining A1 + A4 + B1 covers detection, prevention, and origin tracking together.

A6 + B3 is the right long-term answer if MCPHub wants to support GitOps-style declarative configuration.

Happy to open a PR for the minimal combination once #817's direction is settled (so the schema migration only happens once).


🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions