[RFC]: `mlir-opt-repl`: interactive MLIR pass pipeline explorer and MCP server

TL;DR:

Inspired by a conversation with a “fatherly figure” I made something fun:

I present mlir-opt-repl: a pip-installable Python package that provides a repl experience on top of mlir-opt. It’s small but already has lots of “powers”, like reading IR and running passes:

mlir-opt-repl> load -
func.func @matmul(%A: memref<4x8xf32>, %B: memref<8x4xf32>, %C: memref<4x4xf32>) {
  linalg.matmul ins(%A, %B : memref<4x8xf32>, memref<8x4xf32>)
  outs(%C : memref<4x4xf32>)
  return
}

mlir-opt-repl> run canonicalize
module {
  func.func @matmul(%arg0: memref<4x8xf32>, %arg1: memref<8x4xf32>, %arg2: memref<4x4xf32>) {
    linalg.matmul ins(%arg0, %arg1 : memref<4x8xf32>, memref<8x4xf32>) outs(%arg2 : memref<4x4xf32>)
    return
  }
}
mlir-opt-repl> run convert-linalg-to-loops
module {
  func.func @matmul(%arg0: memref<4x8xf32>, %arg1: memref<8x4xf32>, %arg2: memref<4x4xf32>) {
    %c0 = arith.constant 0 : index
    %c4 = arith.constant 4 : index
    %c1 = arith.constant 1 : index
    %c8 = arith.constant 8 : index
    scf.for %arg3 = %c0 to %c4 step %c1 {
      scf.for %arg4 = %c0 to %c4 step %c1 {
        scf.for %arg5 = %c0 to %c8 step %c1 {
          %0 = memref.load %arg0[%arg3, %arg5] : memref<4x8xf32>
          %1 = memref.load %arg1[%arg5, %arg4] : memref<8x4xf32>
          %2 = memref.load %arg2[%arg3, %arg4] : memref<4x4xf32>
          %3 = arith.mulf %0, %1 : f32
          %4 = arith.addf %2, %3 : f32
          memref.store %4, %arg2[%arg3, %arg4] : memref<4x4xf32>
        }
      }
    }
    return
  }
}

and printing diffs between passes:

mlir-opt-repl> diff
--- --canonicalize
+++ --convert-linalg-to-loops
@@ -1,6 +1,21 @@
 module {
   func.func @matmul(%arg0: memref<4x8xf32>, %arg1: memref<8x4xf32>, %arg2: memref<4x4xf32>) {
-    linalg.matmul ins(%arg0, %arg1 : memref<4x8xf32>, memref<8x4xf32>) outs(%arg2 : memref<4x4xf32>)
+    %c0 = arith.constant 0 : index
+    %c4 = arith.constant 4 : index
+    %c1 = arith.constant 1 : index
+    %c8 = arith.constant 8 : index
+    scf.for %arg3 = %c0 to %c4 step %c1 {
+      scf.for %arg4 = %c0 to %c4 step %c1 {
+        scf.for %arg5 = %c0 to %c8 step %c1 {
+          %0 = memref.load %arg0[%arg3, %arg5] : memref<4x8xf32>
+          %1 = memref.load %arg1[%arg5, %arg4] : memref<8x4xf32>
+          %2 = memref.load %arg2[%arg3, %arg4] : memref<4x4xf32>
+          %3 = arith.mulf %0, %1 : f32
+          %4 = arith.addf %2, %3 : f32
+          memref.store %4, %arg2[%arg3, %arg4] : memref<4x4xf32>
+        }
+      }
+    }
     return
   }
 }

(which is actually colorized in a compatible terminal).

It also supports a rewind [N] for reverting passes:

mlir-opt-repl> rewind
Rewound 1 step(s). Now at: --canonicalize

module {
  func.func @matmul(%arg0: memref<4x8xf32>, %arg1: memref<8x4xf32>, %arg2: memref<4x4xf32>) {
    linalg.matmul ins(%arg0, %arg1 : memref<4x8xf32>, memref<8x4xf32>) outs(%arg2 : memref<4x4xf32>)
    return
  }
}
mlir-opt-repl> 

Importantly (thanks @MaheshRavishankar for the idea :wink:) everything available via repl is also available via mcp i.e., Claude can drive :smiley:; just add this to your .mcp.json:

{
  "mcpServers": {
    "mlir-opt-repl": {
      "command": "mlir-opt-repl",
      "args": ["mcp"],
      "env": {"MLIR_OPT": "/path/to/build/bin/mlir-opt"}
    }
  }
}

Motivation

Understanding how MLIR passes transform IR typically involves running mlir-opt repeatedly with different flag combinations, manually diffing outputs, and losing track of intermediate states. This tool makes that workflow interactive and stateful since you can build up a lowering pipeline incrementally, inspect each step, and backtrack without starting over.

The MCP server mode allows AI coding assistants (Claude Code) to programmatically drive mlir-opt pipelines during development sessions, with the same history/rewind/diff capabilities.

Features

  1. Terminal REPL (default): load MLIR from files or stdin, apply passes one at a time, view colored diffs of what changed, rewind to try different lowering paths.
  2. MCP server: exposes the same functionality as a Model Context Protocol server over JSON-RPC stdio, for use as a Claude Code tool.

Stateful pass pipeline execution

  • run_pipeline / run: feed MLIR through passes, stores the result
  • chain_pipeline / run (subsequent): apply more passes to the current IR
  • State persists across calls; each pass application is recorded in history

History and rewind

  • Full history of every pass application with the IR at each step
  • rewind N: undo the last N steps, restoring IR to that point
  • reset: clear everything and start fresh

Diff visualization

  • Unified diff: standard ---/+++ format showing what each pass changed
  • Side-by-side: two-column comparison with changed lines aligned
  • Both support ANSI-colored “pretty” mode (red/green/dim) for terminal display
  • Compare any two history steps by index, not just consecutive ones

Pass discovery

  • list_passes / passes [filter]: query available mlir-opt passes with substring filtering

Distribution

I plan to develop it in-tree (assuming the community accepts) but also ship it to PyPI (regardless).

PR

PR over here. Feel free to suggest more features here or there.

This is cool!

Though I have to say, I have gotten pretty good mileage from simply adding a skill that tells the agent how to add flags like --mlir-print-ir-before-all and then it doesn’t need to be stateful because it has all the information in one place. Combine that with a skill that can convert a lit test target to a matching CLI command, and it can isolate large pipeline failures reliably. The only thing I need now is an improved mlir-reduce!

I don’t use MCP servers a lot though, so I’m not sure what benefit they add over a skill

I’m not an LLM expert but the explicit thing I wanted here was real state (the history) instead of just “context” so that Claude could solve the phase ordering problem for me (lol…). The rest just came for free.