Expose AnyWidget interfaces as interactive MCP Apps
Project description
anywidget-mcp
anywidget-mcp exposes an AnyWidget class or factory
as an interactive MCP App
tool. Each tool call creates a fresh widget, renders it in the host, and keeps
browser interaction synchronized with Python traitlets.
MCP Apps keep interactive interfaces inside the conversation. anywidget-mcp
packages the MCP tool, HTML resource, widget session, and model-context bridge
so the same widget can run in notebooks and MCP hosts.
Read the documentation for the quickstart, factory lifecycle, state projections, and deployment options.
Install the adapter:
pip install anywidget-mcp
Wigglystuff is an AnyWidget library. Install it to run its widgets:
pip install wigglystuff
Expose one of its widget classes:
anywidget-mcp serve wigglystuff:ColorPicker --port 8010
See it in Inspector Chat
Keep the widget server running. In another terminal, start the mcp-use Inspector:
npx --yes @mcp-use/inspector@12.0.3 \
--url http://127.0.0.1:8010/mcp \
--port 7878 \
--no-open
Open Inspector Chat, configure a
model provider, and ask: Use color_picker so I can choose a color. The model
calls the tool and the widget renders in the conversation. Color changes
synchronize with Python and update the model context available to later turns.
--no-open keeps the Inspector ready for browser-driven end-to-end checks.
Drive the same Chat flow, interact with the rendered widget, then ask about the
selected color. If port 7878 is busy, use the Inspector URL printed in the
terminal.
To expose several widget tools through one endpoint, restart the widget server with several targets:
anywidget-mcp serve \
wigglystuff:ManimWeb \
wigglystuff:ColorPicker \
--port 8010
The AnyWidget gallery lists widgets that can be registered the same way.
The target may also be a factory that returns one widget, returns a non-empty sequence of widgets, or yields either form from a synchronous or asynchronous context manager:
anywidget-mcp serve my_widgets:create_picker
The command serves a streamable HTTP endpoint at
http://127.0.0.1:8000/mcp. The class constructor or factory signature becomes
the MCP tool input schema, so the host can inspect and invoke the widget with
typed arguments.
Inspect that contract before starting the server:
anywidget-mcp inspect wigglystuff:ColorPicker
anywidget-mcp inspect my_widgets:create_picker --json
Inspection reports widget-class or factory as the target kind. FastMCP
Context parameters are injected at call time and stay outside the displayed
input schema.
Create widgets at runtime
Serve create_anywidget when the model should construct an interface for the
current question:
anywidget-mcp serve anywidget_mcp:create_anywidget --port 8010
The tool accepts Python source plus an ordered classnames list. Each name
must resolve to a zero-argument AnyWidget class after the code executes. One
selected class renders as the root widget. Several selected classes render
together in the requested order. If classnames is omitted, the tool selects
the last
final namespace binding to a source-defined top-level AnyWidget class. See the
generated widget
example
for a retry-budget explorer and its model-visible state.
create_anywidgetexecutes supplied Python in the server process and loads each widget's JavaScript in the app iframe. Run it in a disposable sandbox with scoped filesystem, network, credential, and process access.
Serve one widget
Use serve() when one class or factory defines the server:
from anywidget_mcp import serve
from wigglystuff import ColorPicker
serve(ColorPicker)
Set transport="stdio" when the MCP host launches the process:
serve(ColorPicker, transport="stdio")
The command-line form accepts the same transport and server options:
anywidget-mcp serve wigglystuff:SortableList --port 8010
Compose a widget tool server
AnyWidgetMCP extends FastMCP. Register each widget class or factory on
one server:
from anywidget_mcp import AnyWidgetMCP
from wigglystuff import ColorPicker, SortableList
mcp = AnyWidgetMCP("Widget tools")
mcp.widget(ColorPicker, state="color")
mcp.widget(SortableList, state="value")
mcp.run()
Class names become snake-case tool names. ColorPicker registers
color_picker, and Slider2D registers slider_2d. Use name=, title=,
description=, annotations=, and icons= to define the host-facing tool
contract.
Every invocation creates a fresh root widget and recursively enrolls widget references from synchronized dicts, lists, and tuples. AnyWidget subclasses and descriptor-backed protocol objects use the same session. Repeated references use one model, and container changes enroll new models before the parent update reaches the browser.
AnyWidgetMCP composes widget cleanup into the FastMCP lifespan. Server
shutdown closes every widget session and exits each managed factory. Inside an
active server lifespan, await mcp.aclose() closes all sessions early and
stops accepting widget calls until the next lifespan starts.
Attach to an existing FastMCP server
attach() adds widget tools to a server that already has other tools:
from mcp.server.fastmcp import FastMCP
from anywidget_mcp import attach
from wigglystuff import ColorPicker
mcp = FastMCP("Existing tools")
widgets = attach(mcp)
widgets.widget(ColorPicker, state="color")
attach() composes cleanup with the server's existing lifespan. Inside an
active lifespan, await widgets.aclose() closes all widget sessions early and
stops accepting widget calls until the next lifespan starts.
Prepare widgets with factories
A factory can validate arguments, load data, or configure the widget before it is rendered. Its explicit parameters define the tool schema:
from anywidget_mcp import AnyWidgetMCP
from wigglystuff import ColorPicker
mcp = AnyWidgetMCP("Color tools")
@mcp.widget(state="color")
def pick_color(color: str = "#315efb") -> ColorPicker:
"""Open a color picker at the requested hexadecimal value."""
return ColorPicker(color=color)
Async factories use the same decorator. A FastMCP Context parameter receives
the active request context and stays outside the MCP input schema:
from mcp.server.fastmcp import Context
@mcp.widget
async def prepared_picker(
color: str = "#315efb",
*,
ctx: Context,
) -> ColorPicker:
await ctx.info(f"Opening {color}")
return ColorPicker(color=color)
Use a context-managed factory when the widget owns a database, temporary file, model, or another resource that must remain open for the session:
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from anywidget_mcp import AnyWidgetMCP
from mcp.server.fastmcp import Context
from my_widgets import DatasetExplorer, open_dataset
mcp = AnyWidgetMCP("Dataset tools")
@mcp.widget
@asynccontextmanager
async def explore_dataset(
rows: list[str],
ctx: Context,
) -> AsyncIterator[DatasetExplorer]:
await ctx.report_progress(0, 2, "Preparing dataset")
dataset = await open_dataset(rows)
try:
await ctx.report_progress(1, 2, "Opening explorer")
yield DatasetExplorer(dataset=dataset)
finally:
await dataset.aclose()
Factories may return either context-manager type. Each manager must yield an
AnyWidget or a non-empty sequence of AnyWidgets. A sequence renders as one
vertical MCP App result. With state projection enabled, the aggregate projection
uses {"widgets": [state, ...]} within projection limits. Projection limits
encode a large list as a bounded sequence summary with type and length. The
summary may also include retained items, an omitted count, and jsonBytes.
Set state=None to disable the aggregate projection. The manager stays active
until app disposal, idle expiry, aclose(), or server shutdown. The widget
graph closes before the manager exits. The class or factory signature defines
the input contract after FastMCP removes its injected Context parameter. An
AnyWidget class whose constructor is (*args, **kwargs) produces an
empty input schema. Register a factory with named parameters when callers need
to configure that widget.
Cancellation interrupts factory acquisition. When a manager acquires a
resource before its final pre-yield await, shield that partial-acquisition
cleanup with anyio.CancelScope(shield=True). Cleanup after a successful yield
runs inside the protected session teardown path. An awaitable factory owns the
same rollback until it returns its widget or manager.
Describe tool behavior
annotations and icons use MCP tool metadata types:
from mcp.types import Icon, ToolAnnotations
mcp.widget(
ColorPicker,
state="color",
annotations=ToolAnnotations(readOnlyHint=True),
icons=[
Icon(
src="https://example.com/color-picker.svg",
mimeType="image/svg+xml",
)
],
)
WidgetTools.widget(), AnyWidgetMCP.widget(), and serve() accept both
options. serve() also applies icons to its MCP server.
Choose model-visible state
The state option controls the widget state available to the model:
| Value | Projection |
|---|---|
| Omitted | Public synchronized root traits except display metadata |
"color" |
One selected root trait |
("color", "show_label") |
Selected root traits |
None |
State projection and model-context updates are disabled |
lambda widget: {...} |
A custom mapping updated from the complete widget graph |
StateProjection(project, watch="value") |
A custom mapping updated by selected root traits |
The default omits the widget display traits layout, tabbable, and
tooltip.
A custom projection can derive a mapping from widget traits:
mcp.widget(
SortableList,
state=lambda widget: {
"items": widget.value,
"count": len(widget.value),
},
)
Use StateProjection to name the traits that can change that mapping:
from anywidget_mcp import StateProjection
def list_summary(widget: SortableList) -> dict[str, object]:
return {
"items": widget.value,
"count": len(widget.value),
}
mcp.widget(
SortableList,
state=StateProjection(list_summary, watch="value"),
)
watch=None invalidates when any trait in the enrolled widget graph changes. A
string or sequence invalidates on those root traits. watch=() computes the
projection once during launch. Every StateProjection computes an initial
value.
Projection callables are read-only. Mutating synchronized widget traits while building a projection returns a state-projection error.
When state projection is enabled, the tool result includes the initial
projection. Browser changes pass through each model's Python state handler.
When the host supports model-context updates, the app publishes the latest
complete projection through MCP Apps updateModelContext so later chat turns
can reason about the current widget state.
Tool text uses the registered title, such as Opened Color Picker. Structured
results and model context use the registered tool name through the tool
field.
Binary values become {"type": "binary", "bytes": N}. Large or recursive
values use deterministic bounded summaries. Runtime metadata carries the
widget graph, buffers, comm messages, protocol version, and content-addressed
references for ESM and CSS. The app verifies each source digest and caches the
source in a bounded memory cache and browser storage before rendering.
Versioned payloads use source references for _esm and _css. Browser storage
is best effort and never owns the render lifecycle.
Configure the MCP App resource
AnyWidgetMCP accepts FastMCP options plus the app resource policy:
mcp = AnyWidgetMCP(
"Media tools",
host="127.0.0.1",
port=8000,
csp={
"connectDomains": ["https://api.example.com"],
"resourceDomains": ["https://esm.sh"],
},
permissions={"camera": {}},
cors_origins=["https://host.example.com"],
session_idle_timeout=900,
)
The app resource uses ui://anywidget-mcp/widget.html. Add external ESM and
CSS origins to resourceDomains. Add API origins to connectDomains.
Author widgets in marimo
Use marimo to build and inspect the AnyWidget class:
import marimo as mo
from my_widgets import ColorPicker
picker = mo.ui.anywidget(ColorPicker())
picker
Expose the same class to an MCP host:
anywidget-mcp serve my_widgets:ColorPicker
The notebook and MCP App use the same widget implementation and trait contract.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file anywidget_mcp-0.0.1rc3.tar.gz.
File metadata
- Download URL: anywidget_mcp-0.0.1rc3.tar.gz
- Upload date:
- Size: 133.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5f5688bf1e1bb7c3dbd79ebfdf699d4e1c258830bedc5546bc1eae8b340ebcb7
|
|
| MD5 |
29629088eebf57fda8903d9c53c00f5d
|
|
| BLAKE2b-256 |
99cc0ac1ebc9296943ed67dc06f89c95f06e705c5f5d77c49c025a2957d70eef
|
Provenance
The following attestation bundles were made for anywidget_mcp-0.0.1rc3.tar.gz:
Publisher:
publish.yml on peter-gy/anywidget-mcp
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
anywidget_mcp-0.0.1rc3.tar.gz -
Subject digest:
5f5688bf1e1bb7c3dbd79ebfdf699d4e1c258830bedc5546bc1eae8b340ebcb7 - Sigstore transparency entry: 2185887590
- Sigstore integration time:
-
Permalink:
peter-gy/anywidget-mcp@d43cd296ff76f2e29b90e4be78245681135bdda5 -
Branch / Tag:
refs/tags/v0.0.1rc3 - Owner: https://github.com/peter-gy
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@d43cd296ff76f2e29b90e4be78245681135bdda5 -
Trigger Event:
push
-
Statement type:
File details
Details for the file anywidget_mcp-0.0.1rc3-py3-none-any.whl.
File metadata
- Download URL: anywidget_mcp-0.0.1rc3-py3-none-any.whl
- Upload date:
- Size: 137.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6f01f6e0ad76ee0ae1f90d6c55580ec899b907646cb46dc79ea799e3869444b6
|
|
| MD5 |
c0edf2ac31fdeee3e116e560cfc712ce
|
|
| BLAKE2b-256 |
f42cd9cd00223577950efc0a56ce68f210d269a1ca4991d9ad8fdbebf3c07a38
|
Provenance
The following attestation bundles were made for anywidget_mcp-0.0.1rc3-py3-none-any.whl:
Publisher:
publish.yml on peter-gy/anywidget-mcp
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
anywidget_mcp-0.0.1rc3-py3-none-any.whl -
Subject digest:
6f01f6e0ad76ee0ae1f90d6c55580ec899b907646cb46dc79ea799e3869444b6 - Sigstore transparency entry: 2185887779
- Sigstore integration time:
-
Permalink:
peter-gy/anywidget-mcp@d43cd296ff76f2e29b90e4be78245681135bdda5 -
Branch / Tag:
refs/tags/v0.0.1rc3 - Owner: https://github.com/peter-gy
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@d43cd296ff76f2e29b90e4be78245681135bdda5 -
Trigger Event:
push
-
Statement type: