Skip to content

Commit 6124ef0

Browse files
mcp: add view module + sexy global polars HTML render (#777)
## What A bundled `view` module + a global, gorgeous polars HTML render, so agents stop hand-rolling `Result(user_html=...)` and stop shelling to `ls`/`grep`/`cat`. - **`view.ls/tree/grep/find` return plain `polars.DataFrame`s** → they compose with the full polars API (`.filter()/.sort()/.join()`) *and* render as a styled table. - **`view.cat/read/head/tail/json/diff` return a `Code` view**: pygments-highlighted HTML for the human, raw text as the agent's value (no token cost for the HTML). - **`01-ix-polars.py` installs `view.df_html` as the global `pl.DataFrame._repr_html_`** so every frame (view's, the agent's, the human's) gets the same dark, dtype-aware table and stays composable. The agent's text/plain repr is untouched. - Built on the bundled `fff`/`polars`/`pygments` (pygments newly added). No bash. - Advertised in the MCP server instructions so sessions discover it. ## Look Flat dark Tokyo-night palette: dtype subheaders, zebra rows, orange right-aligned tabular-num numerics, green strings, purple bools, muted italic `null`. (Prototyped + screenshotted before building.) ## Validated ✅ `nix build .#mcp` · ✅ `.#mcp.tests.viewSmoke` (helpers + composability + df_html + Code views, cross-platform) · ✅ `nix run .#lint` Empirically checked the high-risk areas: HTML **escaping** (a `</td><script>` filename renders as `&lt;script&gt;`, XSS-safe), **empty frames** (grep no-hits → "0 rows", sort guarded), **fff is sync** (`grep`/`find`; async are `agrep`/`afind`), **no re-entrancy** (df_html never re-renders; override lives in startup, not import), and a **safe `df_html` fallback** (render failure → `<pre>`, never breaks display). _Authored with Claude (Opus 4.8)._
1 parent edb799c commit 6124ef0

5 files changed

Lines changed: 561 additions & 2 deletions

File tree

packages/mcp/default.nix

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,26 @@ let
226226
# ctypes for the Accessibility (TCC) permission check. macOS-only: the module
227227
# itself raises on a non-Darwin platform, and `Quartz` is not available off
228228
# Darwin, so the dependency is gated below.
229+
# Pretty, composable views of files and search results (view.ls/tree/grep/find
230+
# return polars DataFrames; view.cat/json/diff return highlighted Code). Pure
231+
# Python over the bundled fff/polars/pygments; cross-platform, so every session
232+
# can `import view`.
233+
viewPythonSource = builtins.path {
234+
name = "ix-mcp-view-python-source";
235+
path = ./src/view;
236+
};
237+
viewModule = pkgs.python3.pkgs.toPythonModule (
238+
pkgs.runCommand "ix-mcp-view-python-module"
239+
{
240+
strictDeps = true;
241+
meta.description = "Pretty composable file/search views bundled into the ix-mcp interpreter";
242+
}
243+
''
244+
site="$out/${pkgs.python3.sitePackages}/view"
245+
mkdir -p "$site"
246+
cp -r ${viewPythonSource}/view/. "$site/"
247+
''
248+
);
229249
screenPythonSource = builtins.path {
230250
name = "ix-mcp-screen-python-source";
231251
path = ./src/screen;
@@ -335,6 +355,8 @@ let
335355
# capturable out of the box: the worker renders any open figure / object
336356
# with a `_repr_png_` back as an MCP image block.
337357
ps.matplotlib
358+
# pygments: syntax highlighting for `view`'s Code views (cat/json/diff).
359+
ps.pygments
338360
# playwright for browser automation out of the box. The Nix python package
339361
# is patched to use `playwright-driver` as its node driver, and the wrapper
340362
# below points PLAYWRIGHT_BROWSERS_PATH at the matching browser bundle, so
@@ -362,6 +384,7 @@ let
362384
googleAuthModule
363385
ixGoogleModule
364386
ixNotebookMcpModule
387+
viewModule
365388
]
366389
++ darwinExtraPackages ps
367390
);
@@ -897,6 +920,70 @@ let
897920
mkdir -p "$out"
898921
'';
899922

923+
# The view module: tabular helpers return plain polars DataFrames (so they stay
924+
# composable), the file helpers return a Code view whose repr is the raw text,
925+
# and df_html renders the styled table the kernel installs globally. Pure local
926+
# FS over the bundled fff/polars/pygments, so the sandbox runs it.
927+
viewTestPy = pkgs.writeText "ix-mcp-view-test.py" ''
928+
import polars as pl
929+
930+
import view
931+
932+
base = "${./.}"
933+
934+
lsdf = view.ls(base)
935+
assert isinstance(lsdf, pl.DataFrame) and "kind" in lsdf.columns, lsdf.columns
936+
# A DataFrame stays a DataFrame through polars ops (composable).
937+
assert isinstance(lsdf.filter(pl.col("kind") == "dir"), pl.DataFrame)
938+
939+
g = view.grep("viewTestPy", base)
940+
assert isinstance(g, pl.DataFrame) and set(g.columns) == {"path", "line", "text"}, g.columns
941+
assert g.height > 0, "expected a grep hit for the marker"
942+
943+
f = view.find("default.nix", base)
944+
assert isinstance(f, pl.DataFrame) and "path" in f.columns
945+
946+
tr = view.tree(base, depth=1)
947+
assert isinstance(tr, pl.DataFrame) and "depth" in tr.columns
948+
949+
out = view.df_html(lsdf)
950+
assert "<table" in out and "rows" in out and "tabular-nums" in out, out[:120]
951+
952+
c = view.cat(base + "/default.nix", lines=(1, 3))
953+
assert isinstance(c, view.Code)
954+
assert repr(c).count("\n") <= 3
955+
assert "span" in c._repr_html_().lower()
956+
957+
j = view.json({"a": [1, 2], "b": None})
958+
assert '"a"' in repr(j) and "span" in j._repr_html_().lower()
959+
960+
d = view.diff("x\ny\n", "x\nz\n")
961+
assert "-y" in repr(d) and "+z" in repr(d)
962+
963+
print("view-ok")
964+
'';
965+
viewSmoke =
966+
pkgs.runCommand "ix-mcp-view-smoke"
967+
{
968+
nativeBuildInputs = [ mcpPython ];
969+
strictDeps = true;
970+
}
971+
''
972+
export HOME=$TMPDIR/home
973+
mkdir -p "$HOME"
974+
${lib.getExe mcpPython} ${viewTestPy} >stdout 2>stderr || {
975+
echo "ix-mcp view smoke failed:" >&2
976+
cat stdout stderr >&2
977+
exit 1
978+
}
979+
grep -qx 'view-ok' stdout || {
980+
echo "ix-mcp view smoke did not confirm the view module:" >&2
981+
cat stdout stderr >&2
982+
exit 1
983+
}
984+
mkdir -p "$out"
985+
'';
986+
900987
# macOS-only modules (`screen`, `vmkit`) are only bundled on Darwin; their
901988
# import tests only exist there.
902989
screenBundled = importTest "screen" "import screen; print('screen-ok', callable(screen.capture), callable(screen.click), callable(screen.accessibility_trusted))";
@@ -920,6 +1007,7 @@ package.overrideAttrs (old: {
9201007
runtimeSmoke
9211008
richSmoke
9221009
bindDefaultSmoke
1010+
viewSmoke
9231011
;
9241012
}
9251013
// lib.optionalAttrs pkgs.stdenv.hostPlatform.isDarwin {

packages/mcp/ix_notebook_mcp/ipython/01-ix-polars.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,17 @@
1414
pl.Config.set_tbl_cols(40)
1515
pl.Config.set_fmt_str_lengths(80)
1616
pl.Config.set_tbl_width_chars(160)
17+
18+
# The dashboard renders a DataFrame from its ``text/html`` repr; install the
19+
# `view` module's styled renderer globally so every DataFrame (a ``view`` result,
20+
# the agent's own, the human's) shows the same dark, dtype-aware table while
21+
# staying a plain DataFrame that composes with the polars API. The text/plain
22+
# repr above is untouched, so this never changes what the agent reads.
23+
try:
24+
import view
25+
26+
pl.DataFrame._repr_html_ = lambda self: view.df_html(self)
27+
except Exception:
28+
# `view` should always be bundled; if it is not, fall back to polars' default
29+
# HTML repr rather than breaking startup.
30+
pass

packages/mcp/ix_notebook_mcp/tools.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,13 @@
3737
"once and none blocks the others; for a blocking call (fff, a heavy numpy "
3838
"op, a subprocess) wrap it in `await asyncio.to_thread(...)` so it stays "
3939
"off the event loop. Bundled modules import with no install step: `fff` "
40-
"(async file search/grep), `tui`, `search`, `exa_py`, `google_auth`, "
41-
"numpy, polars, duckdb, httpx, matplotlib, playwright. Use polars (`pl`) "
40+
"(async file search/grep), `view`, `tui`, `search`, `exa_py`, "
41+
"`google_auth`, numpy, polars, duckdb, httpx, matplotlib, playwright. "
42+
"Prefer these over shelling out: `view.ls/tree/grep/find` return "
43+
"polars DataFrames (composable + a styled HTML table) and "
44+
"`view.cat/read/head/json/diff` return a syntax-highlighted view, so "
45+
"you never hand-roll display HTML or run `ls`/`grep`/`cat` in bash. "
46+
"Use polars (`pl`) "
4247
"for dataframes; pandas is not bundled. End a cell with a bare expression "
4348
"to display its result richly: a polars DataFrame renders as an HTML table "
4449
"and a matplotlib figure as an image, both in this reply and on the "

0 commit comments

Comments
 (0)