Skip to main content
Glama

moveit-mcp

Point your coding agent at a source and a target. It maps the schema, rehearses the move, and proves nothing was lost.

moveit-mcp is an MCP server and CLI that lets agents like Claude Code, Codex and Cursor migrate tables between databases without the usual "ran a script, hope it worked" step. Every migration is dry-run first, read from one consistent view of the source where the database supports it, loaded into hidden staging tables, read back and compared against the source, and only then swapped into place. If anything fails, your existing tables were never touched.

Supported: PostgreSQL · MySQL · Oracle · Snowflake · Databricks · SQLite · CSV, in any direction.

$ moveit migrate --source csv_in --target db --yes      # real output: a 2,500-row CSV into SQLite

Plan plan_95fde23a37 (hash 03a90805522d5a02): 1 table(s)
  customers -> customers  2500 rows, 9 columns
  [warn] customers.mrr: SQLite stores numbers as 64-bit floats, so values beyond ~15 significant digits cannot be stored exactly; verification will fail if any do. Use a text type override for exact values.
  dry run customers: 2500 rows, 0 failing

Run run_1fd56ce0: succeeded
  customers: promoted, 2500 rows, verified
Report: ./.moveit/runs/run_1fd56ce0/report.md

How it keeps data safe

  1. Plan (plan_migration) proposes column types, names, keys, indexes, defaults, identity columns and DDL, and flags conflicts. Writes nothing.

  2. Dry run (dry_run) reads every source row and checks it converts to the target types: integer ranges, decimal precision and scale, string lengths, NOT NULL, impossible dates, invalid JSON. Failures are reported per column with the row keys, not the values.

  3. Run (run_migration) needs the plan's hash, proving the exact plan was reviewed, and a complete passing dry run of that same plan. It reads every table from one point-in-time view of the source where the system supports it (PostgreSQL, MySQL/InnoDB, Oracle; opt-in elsewhere, see Consistency), and loads each table into a staging table.

  4. Verifies by reading the staged data back and comparing row counts, an order-independent whole-table digest and a per-column digest against what was sent. Inside a snapshot the source count and the rows read come from the same instant and must agree exactly; without one, the count is checked before and after the copy, so a source that changed mid-copy fails the run.

  5. Promotes all tables together with metadata-only renames (or, for append, one insert per table after a key check). A failure at any earlier point leaves existing tables untouched.

  6. Rolls back (rollback) on request, undoing exactly what the run did. It never touches the source.

Every step is written to a hash-chained evidence log, and export_report turns it into a Markdown report. The chain makes edits and deletions detectable; it is tamper-evident, not tamper-proof, so record the head hash elsewhere if that matters to you.

Related MCP server: migrate-mcp

Modes: create, replace, append

plan_migration takes a mode:

Mode

What it does

Safety net

create (default)

New tables only. Refuses if a target table exists.

Nothing existing is touched.

replace

Loads and verifies a new table, then swaps it in. The old table is renamed to _moveit_<run>_bak_<table> and kept.

rollback puts the old table back. finalize_run drops the backups when you are satisfied (after which rollback is no longer possible).

append

Adds the source rows to a table that already exists. Rows are staged and verified first; every key is checked against the existing table before anything is written; a collision stops the run with nothing appended.

rollback deletes exactly the appended rows by key. The staging tables are kept for that until finalize_run.

Things worth knowing:

  • Replace refuses when a table outside the plan has a foreign key into a table being replaced (the constraint would silently follow the old table into the backup). Grants, views and triggers that depended on the old table are not carried over.

  • Append checks that every planned column exists in the target and that no other NOT NULL column would be left unfilled. Without a key the rows cannot be told apart, so duplicates cannot be detected and the run cannot be rolled back; the plan says so. After an append, the existing table's row count is checked, and the whole table is digested when it started empty. On PostgreSQL the table's serial/identity counter is moved past the appended keys; on other systems the plan warns that you must reset it.

  • Append is not offered for CSV targets.

What is migrated besides columns

Carried over

Notes

Primary keys

yes

Named constraints; not enforced on Snowflake/Databricks, so duplicates are checked after staging.

Identity / auto-increment

yes, for a single integer primary key

PostgreSQL GENERATED BY DEFAULT AS IDENTITY (serial columns too), MySQL AUTO_INCREMENT, Oracle identity, Snowflake AUTOINCREMENT, Databricks identity (BIGINT), SQLite rowid alias. The counter continues after the highest value copied, computed at run time and checked after loading.

Defaults

plain literals, now() / CURRENT_TIMESTAMP, CURRENT_DATE

Anything else (sequence calls, UDFs, expressions) is reported, not guessed. Off by default on Databricks (needs a Delta table feature; set "defaults": true).

Indexes

yes, where the target has them

Created after the load. A failure is reported with its statement and never fails the run. Partial, expression, full-text, spatial and prefix indexes are listed as not migrated.

Foreign keys

yes, between tables in the same plan

Added after every table is promoted (SQLite: declared in CREATE TABLE). A failure is reported with its statement and never fails the run. Not created on Snowflake or Databricks, which do not enforce them.

Views, triggers, sequences, routines

no

Listed in the plan (inventory) and in the report so nothing is forgotten.

Grants, check constraints

no

Each of identity, defaults, indexes and foreign keys can be turned off with objects on plan_migration (CLI: --skip-objects indexes,foreign-keys,defaults,identity).

Consistency and resume

Snapshot reads. One transaction (or equivalent) is held for the whole staging phase, so every table, and every count, is read as of the same instant even while the source is being written to.

Source

How

PostgreSQL

REPEATABLE READ, READ ONLY transaction

automatic, tested live

MySQL

START TRANSACTION WITH CONSISTENT SNAPSHOT (InnoDB)

automatic, not tested live

Oracle

SET TRANSACTION READ ONLY

automatic, not tested live; a long run needs enough undo to avoid ORA-01555

SQLite

one read transaction

opt-in "snapshot": true; needs WAL mode or writers wait

Snowflake, Databricks

Time Travel AT(TIMESTAMP => ...) / TIMESTAMP AS OF

opt-in "snapshot": true; the table's retention must cover the run; not tested live

CSV

none

If the source changes after the snapshot, the run still succeeds and records a warning; reconcile later shows the drift. A run that is paused (maxBatches) and resumed takes a new snapshot, so the report marks it interrupted. A source that supports no snapshot keeps the strict rule: the row count must not change during the copy.

Resume after a crash. The checkpoint is written after each batch lands, so a crash between the two can leave rows in staging that the checkpoint does not know about. On resume, each staging table's row count is compared with the checkpoint. If they differ, that table is dropped and restarted from the beginning (with a warning); if they agree, loading continues where it stopped. Nothing is duplicated either way, and verification still compares everything at the end.

Speed. Reading the next batch from the source overlaps with writing the previous one (prefetch, default 2 batches; 0 turns it off). Snowflake and Databricks can also bulk load, see below.

Quick start

git clone <this repo> && cd moveit-mcp
npm install && npm run build       # Node 22.13+
npm link                           # puts `moveit` on your PATH (or run: node dist/src/index.js ...)
moveit setup                       # writes moveit.config.json and prints MCP client settings
moveit doctor                      # checks drivers and connections

Keep passwords in environment variables and reference them as "${NAME}". Mark sources "readOnly": true, ideally backed by a read-only database user.

{
  "connections": {
    "legacy_pg": { "type": "postgres", "url": "${LEGACY_PG_URL}", "readOnly": true },
    "warehouse": { "type": "snowflake", "account": "myorg-acct", "username": "MOVEIT", "password": "${SNOWFLAKE_PASSWORD}",
                   "warehouse": "LOAD_WH", "database": "ANALYTICS", "schema": "RAW", "role": "LOADER" }
  }
}

Add it to your MCP client:

{ "mcpServers": { "moveit": { "command": "moveit", "args": ["mcp"], "env": { "MOVEIT_CONFIG": "/path/to/moveit.config.json" } } } }

Then ask your agent: "Migrate the orders and customers tables from legacy_pg to warehouse."

Database drivers (pg, mysql2, oracledb, snowflake-sdk, @databricks/sql) are optional dependencies, loaded only when a connection of that type is used.

Connection options beyond the basics

Option

Systems

Effect

readOnly

all

Refuse every write through this connection.

snapshot

SQLite, Snowflake, Databricks (sources)

Read all tables as of one instant.

bulkLoad: true

Snowflake (targets)

Load each batch with PUT to the user stage + COPY INTO instead of multi-row INSERT. Batches default to 50,000 rows. Not verified against a live account.

bulkLoad: { "volume": "/Volumes/cat/schema/vol" }

Databricks (targets)

Upload each batch to a Unity Catalog volume and INSERT ... SELECT FROM read_files(...). Not verified against a live workspace.

identity: false

Snowflake, Databricks (targets)

Copy auto-generated keys as plain values.

defaults: true

Databricks (targets)

Create column defaults (enables the allowColumnDefaults table feature).

primaryKeys: false

Databricks (targets)

For hive_metastore, which has no key constraints.

Tools

Tool

What it does

Writes?

list_connections

Connection names, types, read-only flag. Never credentials.

no

test_connection

Connect and ping.

no

inspect_schema

Tables, columns, native and canonical types, keys, row counts.

no

sample_rows

A few rows, with emails, card numbers, SSNs, names and addresses redacted by default.

no

profile_data

Per-column nulls, distinct counts, lengths, min/max, candidate keys.

no

plan_migration

Mapping, DDL, identity, defaults, indexes, foreign keys and conflicts. Takes mode and objects. Returns planId and planHash.

no

get_plan

Show a plan and its dry-run status.

no

dry_run

Validate every row against the plan.

no

run_migration

Snapshot, stage, verify, promote. Supports maxBatches, resumeRunId, prefetch.

yes

run_status

Per-table progress.

no

reconcile

Re-read both sides now and compare digests. Detects drift later. (Not applicable to append plans, where the target also holds older rows.)

no

rollback

Undo a run. Requires confirm equal to the run id.

yes

finalize_run

Drop the backups (replace) or staging tables (append) a run kept. Requires confirm. After this, no rollback.

yes

export_report

Markdown report plus evidence-chain integrity check.

report file

Overrides on plan_migration let an agent fix what the dry run finds: target (table name), exclude, renames, typeOverrides (to any canonical type, with length, precision and scale) and primaryKey.

CLI equivalents: moveit migrate --source A --target B [--mode create|replace|append] [--skip-objects ...] --yes, moveit finalize <runId> --yes, moveit rollback <runId> --yes.

Support matrix: read this before pointing it at production

System

Source

Target

How it was tested

SQLite

yes

yes

Automated end-to-end tests, including every feature above

CSV

yes

yes (create, replace)

Automated end-to-end tests; tested in SQLite → CSV → SQLite round trips

PostgreSQL

yes

yes

Automated tests against a live PostgreSQL 18: PostgreSQL → PostgreSQL, SQLite → PostgreSQL, and PostgreSQL → CSV → PostgreSQL paths; independent in-database hash comparison, identity, defaults, indexes, foreign keys, snapshots under a concurrent writer, crash-resume, replace, append, rollback, finalize

MySQL

yes

yes

Unit tests of the SQL it generates and of how it reads catalog rows (identity, defaults, indexes, foreign keys). Never run against a live server.

Oracle

yes

yes

Same: unit tests only. Never run against a live server.

Snowflake

yes

yes

Same, plus tests of the bulk-load statements against a fake driver. Never run against a live account.

Databricks

yes

yes

Same, plus tests of the bulk-load statements against a fake driver. Never run against a live workspace.

The MySQL, Oracle, Snowflake and Databricks adapters follow each system's documented behaviour, and the staged-load-plus-verification design means a wrong guess about data should surface as a failed run rather than bad data. Index, foreign-key and identity DDL is different: a statement the server rejects is reported with its text instead of failing the run (identity and defaults, which are part of CREATE TABLE, fail the staging step instead, before anything is promoted).

Tested migration paths are not a full source/target matrix. The regular end-to-end suite exercises SQLite → SQLite and SQLite → CSV → SQLite. The live PostgreSQL suite also exercises PostgreSQL → PostgreSQL, SQLite → PostgreSQL, and PostgreSQL → CSV → PostgreSQL. These tests do not establish that every supported source can migrate directly to every supported target.

The optional live conformance suite, test/conformance.live.test.ts, runs SQLite → configured system → SQLite for each system you provide: digests are compared at every hop, schema objects are read back, and where supported it checks a snapshot read under a concurrent writer, identity and defaults on new rows, replace + finalize, and append + rollback. SQLite always runs; PostgreSQL runs when MOVEIT_TEST_PG is set; MySQL, Oracle, Snowflake, and Databricks run only when their corresponding MOVEIT_TEST_* variables are set. The README's current live-test status is listed in the table above; having a conformance test available does not mean that system has been tested against a live server/account/workspace. Give the suite a scratch database or schema (it only creates and drops tables named conf_*):

MOVEIT_TEST_MYSQL='{"type":"mysql","host":"127.0.0.1","user":"root","password":"${MYSQL_PW}","database":"scratch"}' \
MOVEIT_TEST_ORACLE='{"type":"oracle","user":"scratch","password":"${ORA_PW}","connectString":"localhost/FREEPDB1"}' \
npm run test:live

(MOVEIT_TEST_SNOWFLAKE and MOVEIT_TEST_DATABRICKS work the same way.) Please open an issue with anything it finds.

Notable per-system behaviour the planner already handles or warns about:

  • Oracle cannot store '': it becomes NULL. The dry run counts these and refuses them for NOT NULL columns. VARCHAR2 is limited to 4,000 bytes, so longer values map to CLOB. Oracle has no TIME or (before 23ai) BOOLEAN; they are stored as VARCHAR2(15) and NUMBER(1,0). Oracle foreign keys have no ON UPDATE.

  • Snowflake and Databricks accept PRIMARY KEY but do not enforce it, so duplicates are checked explicitly after staging. Databricks has no TIME and does not enforce VARCHAR length; its identity columns must be BIGINT.

  • SQLite stores numbers as 64-bit floats, so decimals beyond ~15 significant digits cannot round-trip. The plan warns and verification fails safely. A lone INTEGER PRIMARY KEY is treated as an identity column (it is the rowid alias, which hands out the next value itself).

  • MySQL has no timestamp-with-zone; values are normalised to UTC. Text, JSON and blob columns cannot carry a literal default or be indexed without a prefix length, so those are skipped with a note.

  • Decimals wider than the target's maximum (38 on Oracle, Snowflake and Databricks) are clamped with a warning, and the dry run checks every value fits.

  • A CSV has no types, so they are inferred from the first 10,000 rows (override with typeOverrides). Unquoted empty fields are NULL; quoted "" is an empty string, and the writer preserves the difference.

What it does not do

  • No upsert and no change-data-capture. append only adds rows that do not already exist; it never updates or deletes existing ones. A snapshot is one instant, not a stream; to catch up afterwards, migrate again with replace.

  • Views, triggers, sequences, routines, grants, check constraints, and partial / expression / full-text indexes are not migrated. They are listed in the plan and report.

  • Append rollback needs a key, and append verification is a row count (plus a full digest when the table started empty), not a digest of the merged table.

  • Snapshots are per run. A paused-and-resumed run reads different moments (marked interrupted). Snowflake, Databricks and SQLite snapshots are opt-in; Snowflake and Databricks use Time Travel and need retention that covers the run.

  • Bulk loading, Time Travel snapshots and the MySQL, Oracle, Snowflake and Databricks adapters as a whole have not been run against live servers (see the support matrix and the conformance suite).

  • Identity counters on append are only moved automatically on PostgreSQL.

Security notes

  • Credentials come from environment variables, are never returned by any tool, and are scrubbed from error messages and evidence.

  • sample_rows, profile_data and dry-run examples redact sensitive-looking columns and values by default. Redaction is by column-name and value patterns, so treat it as a safety net, not a guarantee.

  • Dry-run failures identify rows by primary key, which may itself be sensitive in some schemas.

  • The server never writes to a connection marked readOnly, and it never writes to the source at all.

  • run_migration, rollback and finalize_run are marked as writing (the last two as destructive) in MCP annotations, so clients can require human approval.

  • Bulk loading writes temporary newline-delimited JSON files (mode 0600) to the OS temp directory and removes them after each batch; the same data is staged in your Snowflake user stage or Databricks volume and removed after the load.

Development

npm install
npm test                    # unit + SQLite/CSV end-to-end + MCP protocol tests (no services needed)

# live tests: PostgreSQL (create two empty databases first) and, optionally, the conformance suite for the others
createdb moveit_src && createdb moveit_dst
MOVEIT_TEST_PG=postgres://postgres@127.0.0.1:5432 npm run test:live

State lives in ./.moveit (MOVEIT_HOME to change): plans, run state and evidence logs. They contain connection names, never credentials.

License

MIT

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      A
      quality
      B
      maintenance
      Enables safe database migration workflows by detecting frameworks, reviewing migration risk and drift, generating migration files with explicit approval, and providing rollback guidance—without ever applying migrations or executing rollbacks.
      7
      12 npm
      1
      MIT
    • A
      license
      Not graded
      quality
      C
      maintenance
      Enables developers and coding agents to submit structured database schema changes to a shared coordinator, compare them against accepted migration history and other active work, and receive reports on safe changes, stale bases, dependencies, and conflicts before migrations are finalized. It supports CI-controlled deployment coordination without exposing application database credentials.
      Apache 2.0
    • A
      license
      B
      quality
      C
      maintenance
      Enables AI coding agents to perform deterministic database architecture audits and performance engineering for PostgreSQL and MySQL, covering migration analysis with schema health scoring, ESR-based composite index recommendations, N+1 query detection, EXPLAIN plan diagnosis, transaction and deadlock analysis, partitioning and archival design, and capacity or configuration tuning. It also generates phased Expand & Contract runbooks that turn blocking DDL into zero-downtime production migrations.
      8
      MIT