mcp-diff — Ruby port (in progress)

Parallel port of mcp-diff to Ruby, on the ruby-port branch. The TypeScript packages under ../packages stay the shippable version until this reaches feature parity (24 rules + CLI + Action + green tests).

Decisions driving this port

  • Language: Ruby (user decision, supersedes the TS default for this branch).
  • MCP SDK: the official mcp gem (modelcontextprotocol/ruby-sdk) — honors the project's "anchor only to the official SDK" rule. Its client subsystem (MCP::Client + MCP::Client::Stdio / http) covers the full capture surface: connect/InitializeResult (capabilities, protocolVersion, serverInfo, instructions), #tools / #resources / #resource_templates / #prompts, all auto-paginated.
  • Distribution: RubyGems (gem install) + a Ruby-based GitHub Action — replaces the TS npx flow. (Verify the gem name mcp_diff / mcp-diff on RubyGems, mirroring the npm-naming saga.)

Module boundaries (mirror the TS split; keep core pure)

  • McpDiff::Core — normalize → diff → classify + rule catalog. Zero I/O.
  • McpDiff::Client — capture over the official mcp gem. The only server I/O.
  • McpDiff::Cli — init/snapshot/check/update/diff/explain; exit codes 0/1/2.

Spike result (verified — Ruby 3.3.12, mcp 1.0.0)

Capture works and is faithful. Gotcha found & handled: the gem's public #tools/#resources/… wrappers deserialize into objects that DROP annotations and title — both part of mcp-diff's contract (BEHAVIOR class). The capture layer reads the raw JSON one layer down via MCP::Client's own private request (full JSON-RPC envelope + id correlation), so nothing is lost. capture_test.rb locks this in (asserts add_doc's annotations survive).

Done on this branch (green — 35 tests / 94 assertions)

  • McpDiff::Core::Canonical + Normalize — deterministic snapshot + contractHash.
  • McpDiff::Client.capture — capability-gated, cursor-paginated capture over the official gem (stdio verified against two real servers; HTTP wired via MCP::Client::HTTP, not yet exercised by a test).
  • Core::Diff + Classify + SchemaDiff (D9) + the full 24-rule catalog — verified against the SAME fixtures the TS suite uses (test/fixtures/).
  • Cli — init/snapshot/check/update/diff/explain/rules, exit codes 0/1/2, pretty/JSON/MD. Output is byte-identical to the TS CLI (verified on the demo).
  • exe/mcp-diff, gemspec builds clean (mcp_diff-0.1.0.gem).

Done since (green — full suite: 45 tests / 157 assertions)

  • test/schema_diff_test.rb — the D9 variance primitives (port of the TS units).
  • test/catalog_test.rb — D8 no-drift: the catalog vs context/RULES.md.
  • action/action.yml — Ruby-native composite Action (setup-ruby → gem install mcp_diffmcp-diff --md check → PR comment + gate).
  • .github/workflows/ruby-ci.yml — runs the Ruby suite on CI.

Release artifact validated

Built + installed the gem and ran the installed mcp-diff binary end-to-end: init (live capture) wrote config + lockfile, check returned "✓ contract unchanged" (exit 0). So gem install mcp_diff yields a working CLI.

Standalone

test/fixtures/server.rb is a hand-rolled MCP stdio server mirroring the TS fixture — the whole suite runs with only Ruby (no Node), and ruby-ci.yml reflects that.

Remaining before a Ruby release

  • Publish to RubyGems — the name mcp_diff (and mcp-diff) is FREE on RubyGems (verified), so no npm-style workaround. The release is wired: .github/workflows/ruby-release.yml builds + gem push on a v* tag. It needs the repo secret RUBYGEMS_API_KEY (your RubyGems API key — the analogue of NPM_TOKEN). To ship: add the secret, then git tag v0.1.0 && git push --tags.
  • Nice-to-have: split rules into one-file-per-rule (contributor funnel); an HTTP-transport capture test.

Port stream tracking

Mirrors ../context/STREAMS.md S1–S5. Formal D-decisions land in context/ once the spike validates the foundation (not before — the rewrite isn't final until capture is proven in Ruby).