Skip to main content

Expose AnyWidget interfaces as interactive MCP Apps

Project description

anywidget-mcp

PyPI License: MIT

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

anywidget_mcp-0.0.1rc1.tar.gz (126.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

anywidget_mcp-0.0.1rc1-py3-none-any.whl (129.9 kB view details)

Uploaded Python 3

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

Hashes for anywidget_mcp-0.0.1rc1.tar.gz
Algorithm Hash digest
SHA256 b070748d0f6530682933e445cf42c56bfe2f726dfbdc68ff9d61a2b4b7a8f26b
MD5 7f15402ebcafd683db78f83dee780aa8
BLAKE2b-256 259c18dc35526f0093a9fde78752731c66b1391339c45e73ed6163e969ab3dc0

See more details on using hashes here.

Provenance

The following attestation bundles were made for anywidget_mcp-0.0.1rc1.tar.gz:

Publisher: publish.yml on peter-gy/anywidget-mcp

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file anywidget_mcp-0.0.1rc1-py3-none-any.whl.

File metadata

File hashes

Hashes for anywidget_mcp-0.0.1rc1-py3-none-any.whl
Algorithm Hash digest
SHA256 1e9d0ee463e4019426ab5d2ae118e7fb3509cbad977467c570fac5f60344e17a
MD5 a3a8312cd6f417620c4158d70cd60ee5
BLAKE2b-256 413740c5b20aad6d8d06a0070168d5ce38d2b8ae1873d84e9c8e33aa12021707

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page