ix-mcp serves a persistent Python session over MCP
nix run .#mcp speaks MCP over stdio and exposes a Python interpreter as a small set of tools. Sessions live across calls, so an agent can load a dataset once with python_exec and ask python_eval questions against the cached globals on later turns. No notebook server, no kernel manager, just stdin / stdout.
python_session_create python_session_list python_session_close
python_eval python_exec python_reset Each session boots its own writable venv under a temp dir on one pinned interpreter, so pip install works without mutating the read-only store python. The interpreter is fixed: there is no per-session interpreter choice. Pass cwd to root the session in a project directory:
{
"name": "python_session_create",
"arguments": {
"session_id": "build-analysis",
"cwd": "/Users/me/Projects/indexable-inc/index"
}
} This pairs naturally with nix run .#run. After recording a slow build, the agent can ask the MCP session to load .ix/run/latest/lines.jsonl into polars and rank the longest gaps, without paging tens of MB of log through its own context window:
python_exec(source="""
import polars as pl
nix_event_dtype = pl.Struct([
pl.Field("action", pl.String),
pl.Field("id", pl.Int64),
pl.Field("text", pl.String),
pl.Field("type", pl.Int64),
])
events = (
pl.read_ndjson(".ix/run/latest/lines.jsonl")
.filter(pl.col("text").str.starts_with("@nix "))
.select(
"ended_elapsed_ns",
pl.col("text").str.slice(5).str.json_decode(nix_event_dtype).alias("event"),
)
.unnest("event")
)
starts = events.filter(pl.col('action') == 'start').select('id', 'text', pl.col('ended_elapsed_ns').alias('t0'))
stops = events.filter(pl.col('action') == 'stop' ).select('id', pl.col('ended_elapsed_ns').alias('t1'))
slow = (starts.join(stops, on='id')
.with_columns(((pl.col('t1') - pl.col('t0')) / 1e9).alias('seconds'))
.sort('seconds', descending=True))
""")
python_eval(expression="slow.head(20)")
python_eval(expression="slow.filter(pl.col('text').str.contains('rust')).head(20)") The follow-up python_eval calls reuse slow, so the agent can ask several questions of the same dataframe instead of re-parsing the log each turn. python_reset clears the globals, python_session_close ends the worker.
The same binary is also a normal CLI: nix run .#mcp -- repl opens an interactive Python session, nix run .#mcp -- exec '<source>' and nix run .#mcp -- eval '<expression>' run a one-shot through the same worker. The MCP tools and the CLI subcommands share one implementation, so what works at the shell works the same when an agent drives it.