Getting started
Run ColorPicker and LiveEdit in Inspector Chat. Choose a color, then ask for an interactive explanation based on your selection.
Requires Python 3.11 or newer.
Run both widgets
Serve Wigglystuff's ColorPicker and LiveEdit together:
uvx --with wigglystuff anywidget-mcp serve \
wigglystuff:ColorPicker \
wigglystuff:LiveEdit \
--port 8010The MCP endpoint is http://127.0.0.1:8010/mcp. The server exposes both widgets as tools.
Browse the AnyWidget gallery for more widgets and links to their packages.
See it in Inspector Chat
Keep the widget server running. In another terminal, start the mcp-use Inspector, a local MCP Apps host:
npx --yes @mcp-use/inspector@12.0.3 \
--url http://127.0.0.1:8010/mcp \
--port 7878Open Inspector Chat, configure a model provider, and ask:
let me pick a color
Choose a color, then ask:
given my current selection help explain the HEX to RGB algo interactively
The model reads the picker's current color from its model-visible state and can open LiveEdit for an interactive explanation.
If port 7878 is busy, use the Inspector URL printed in the terminal.
Use another MCP client
Codex can connect to MCP servers, but inline widget rendering requires MCP Apps support. ChatGPT on the web and Claude Desktop support MCP Apps. Check the current client support list before configuring another client.
Install in a project
Install anywidget-mcp in the Python environment that owns your widget code:
uv pip install anywidget-mcpBring your own AnyWidget
Define a counter with the same Python state and frontend module you would use in a notebook:
# counter.py
import anywidget
import traitlets
class Counter(anywidget.AnyWidget):
"""Adjust a counter and inspect its current value."""
_esm = """
function render({ model, el, signal }) {
const button = document.createElement("button");
const draw = () => {
button.textContent = `Count: ${model.get("value")}`;
};
button.addEventListener("click", () => {
model.set("value", model.get("value") + 1);
model.save_changes();
}, { signal });
model.on("change:value", draw);
signal.addEventListener("abort", () => {
model.off("change:value", draw);
}, { once: true });
draw();
el.append(button);
}
export default { render };
"""
value = traitlets.Int(0, help="Current counter value.").tag(sync=True)Point the CLI at the widget's import path:
anywidget-mcp serve counter:CounterA button click updates Counter.value in Python. The default model-visible state includes value.
Use the same widget in marimo
Render Counter while developing it in a marimo notebook:
import marimo as mo
from counter import Counter
counter = mo.ui.anywidget(Counter())
counterThe notebook and MCP App use the same widget code and synchronized value.
Start the server from Python
Call serve() when the widget module should start the MCP server:
from anywidget_mcp import serve
from counter import Counter
serve(Counter, state="value")serve() listens on streamable HTTP by default and blocks until the transport exits. Set transport="stdio" when the MCP host launches the process.
How it works explains how the widget becomes an MCP tool, how browser changes reach Python, and how the model reads the current value.
Next steps
- Model-visible state chooses which widget values the model can use.
- Pass input to widgets accepts values from the model, loads data, returns several widgets, and adds widgets to an existing MCP server.
- Create AnyWidgets from source at runtime covers generated widget code and its sandbox requirements.
- Deployment covers tool inspection, HTTP and standard-input transports, browser policy, and session lifetime.