I turned a CKEditor 5 v26→v47 migration into an MCP server (and saved 80–95% of my AI tokens)

At work, I’m currently running a CKEditor 5 migration from v26 to v47. On paper, just a “version bump”. In real life: around twenty in-house plugins, each stuffed with deep legacy imports like @ckeditor/ckeditor5-core/src/plugin, that all have to be swapped for the modern named exports of the flat ckeditor5 package. Multiply that by every plugin, add the config files that reference them, and you get a migration where the hard part isn’t technical but bookkeeping: knowing, at any moment, what’s done, half-done, or not started.

Tracking that by hand — or worse, asking an AI to re-read 200 files every session — is slow, expensive and error-prone. So I took the problem from the other end and turned it into an MCP server. It’s called ckeditor_audit and the code is public.

The trigger: what if I audited with an MCP?

An MCP (Model Context Protocol) server exposes tools that assistants like Claude can call directly. The idea: instead of the AI guessing my migration status by reading files one at a time, it queries a server that has already pre-indexed the whole project and answers in a single round-trip.

The server is read-only — it never writes to my code, it observes and reports. Two goals drove me: first, full visibility on migration progress; second, a drastic AI token saving. The two turned out to be linked: the fewer raw files the AI reads, the cheaper it gets, and the sharper its answers.

The core: migration audit

Two tools carry most of the value. audit_plugin returns a detailed report for a given plugin, and audit_all summarizes the whole project. Every plugin is sorted into one of three statuses:

The three CKEditor 5 audit statuses: migrated, partial and not_migrated, per plugin

  • migrated — no legacy import detected, the plugin is clean.
  • partial — the trap: legacy and modern imports coexist in the same plugin. Exactly the kind of thing a human eye lets slip.
  • not_migrated — legacy only, nothing has moved yet.

But the status isn’t enough: the report drills down to the technical detail. For every legacy pattern found, it gives the exact file and line, the matched legacy signature, and the suggested modern replacement. It also lists the config files that reference the plugin, distinguishing active references from those that only live in commented-out code — a classic blind spot of ad-hoc grep calls.

The most frequent pattern of this migration is this one:

Before and after of a CKEditor 5 import: deep legacy path to named export from the ckeditor5 package

// BEFORE (v26)
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';

// AFTER (v47)
import { Plugin } from 'ckeditor5';

The argument that changes everything: token saving

Here’s the calculation that convinced me to write this server. Without it, to understand the state of a single plugin, the AI typically reads 3 to 10 files: the plugin source, its package.json, the config files using it. Across twenty plugins, that’s 6000 to 15000 tokens just for discovery, before a single line of migration work. And in a long session, every re-read restarts the meter.

Token cost comparison: naive file reading versus an MCP audit report

The server, on the other hand, has already indexed everything. One audit_plugin call returns a report of about 200 tokens covering status, detected patterns and config references. The saving runs between 80 and 95% of discovery tokens per session. Each report even carries a token_savings field that quantifies what was just avoided. A nice bonus: coverage is better than a manual grep, because the server searches across all relevant file types (JS entry files, YAML configs, PHP constants, plugin registries) and even catches usages in commented-out code.

Not just an auditor: 33 search tools

Along the way, the server became a real Swiss-army knife for code navigation — 33 tools in total. Under the hood, two engines: ripgrep for blazing-fast text search (falling back to Python’s re if it isn’t installed), and ast-grep for structural search.

  • Text search: grep_code, grep_with_context, count_matches (to gauge scope before digging), multi_search (up to 10 queries in parallel).
  • Structural search: ast_search with a pattern syntax like class $NAME extends Plugin, plus find_class, find_method, who_calls, what_calls.
  • Git-aware navigation: git_changed_files, grep_changed to restrict a search to changed files only.

The server also exposes a guided MCP prompt, migrate_plugin, which assembles the current status and ready-to-use migration instructions. And three slash commands for Claude Code: /ckeditor-audit (the dashboard), /ckeditor-migrate <plugin> (the full audit → suggest → apply → validate workflow) and /ckeditor-report (the final Markdown or JSON report).

Built to be reusable

My favorite part: nothing is hardcoded for my migration. The legacy → modern mapping table lives in a plain lib/data/patterns.json file — you add an entry (with "is_regex": true for a regular expression) without touching code. And a generic fallback rule catches any deep @ckeditor/ckeditor5-* import not covered by a specific entry, so nothing slips through.

The version labels are configurable too: v26 as source, v47 as target in my case, but those are just two environment variables. In other words, this server can serve as an audit backbone for any A → B migration of CKEditor 5, not just mine.

The takeaway

Since I started using ckeditor_audit, my migration lost its anxiety-inducing side: I always know what’s migrated, partial or not_migrated, my AI sessions cost a fraction of what they used to, and every fix is traceable. A very concrete work problem turned into a reusable tool — exactly the kind of detour that ends up saving time.

The code is open and feedback is welcome: github.com/oumarkonate/ckeditor_audit.

Leave a Reply

Your email address will not be published. Required fields are marked *