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.
Read the documentation for the quickstart, factory lifecycle, state projections, and deployment options.
Install the adapter and the widget package you want to serve:
pip install anywidget-mcp wigglystuff
Expose an installed widget class:
anywidget-mcp serve wigglystuff:ColorPicker
The target may also be a factory that returns a widget directly or yields one 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. Constructor parameters become 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.
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. 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. A minimal 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 the standard 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 concise 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 expose an intent-focused view of a larger widget model:
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.1rc1.tar.gz.
File metadata
- Download URL: anywidget_mcp-0.0.1rc1.tar.gz
- Upload date:
- Size: 126.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b070748d0f6530682933e445cf42c56bfe2f726dfbdc68ff9d61a2b4b7a8f26b
|
|
| MD5 |
7f15402ebcafd683db78f83dee780aa8
|
|
| BLAKE2b-256 |
259c18dc35526f0093a9fde78752731c66b1391339c45e73ed6163e969ab3dc0
|
Provenance
The following attestation bundles were made for anywidget_mcp-0.0.1rc1.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.1rc1.tar.gz -
Subject digest:
b070748d0f6530682933e445cf42c56bfe2f726dfbdc68ff9d61a2b4b7a8f26b - Sigstore transparency entry: 2180941415
- Sigstore integration time:
-
Permalink:
peter-gy/anywidget-mcp@005ef7f7d38c74194c68eb5b168c9abe0027e467 -
Branch / Tag:
refs/tags/v0.0.1rc1 - Owner: https://github.com/peter-gy
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@005ef7f7d38c74194c68eb5b168c9abe0027e467 -
Trigger Event:
push
-
Statement type:
File details
Details for the file anywidget_mcp-0.0.1rc1-py3-none-any.whl.
File metadata
- Download URL: anywidget_mcp-0.0.1rc1-py3-none-any.whl
- Upload date:
- Size: 129.9 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 |
1e9d0ee463e4019426ab5d2ae118e7fb3509cbad977467c570fac5f60344e17a
|
|
| MD5 |
a3a8312cd6f417620c4158d70cd60ee5
|
|
| BLAKE2b-256 |
413740c5b20aad6d8d06a0070168d5ce38d2b8ae1873d84e9c8e33aa12021707
|
Provenance
The following attestation bundles were made for anywidget_mcp-0.0.1rc1-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.1rc1-py3-none-any.whl -
Subject digest:
1e9d0ee463e4019426ab5d2ae118e7fb3509cbad977467c570fac5f60344e17a - Sigstore transparency entry: 2180941612
- Sigstore integration time:
-
Permalink:
peter-gy/anywidget-mcp@005ef7f7d38c74194c68eb5b168c9abe0027e467 -
Branch / Tag:
refs/tags/v0.0.1rc1 - Owner: https://github.com/peter-gy
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@005ef7f7d38c74194c68eb5b168c9abe0027e467 -
Trigger Event:
push
-
Statement type: