Skip to content

Session & Viewer Controls

Functions for managing the napari viewer lifecycle, GUI state, and session information.

detect_viewers

MCP Tool Interface

Detect available viewers (local and external).

Returns:

Type Description
dict

Dictionary with information about available viewers

Source code in src/napari_mcp/server.py
async def detect_viewers() -> dict[str, Any]:
    """
    Detect available viewers (local and external).

    Returns
    -------
    dict
        Dictionary with information about available viewers
    """
    return await NapariMCPTools.detect_viewers()

Implementation

Detect available viewers (local and external).

Returns:

Type Description
dict

Dictionary with information about available viewers

Source code in src/napari_mcp/server.py
@staticmethod
async def detect_viewers() -> dict[str, Any]:
    """
    Detect available viewers (local and external).

    Returns
    -------
    dict
        Dictionary with information about available viewers
    """
    viewers: dict[str, Any] = {"local": None, "external": None}

    # Check for external viewer
    client, info = await _detect_external_viewer()
    if client and info is not None:
        viewers["external"] = {
            "available": True,
            "type": "napari_bridge",
            "port": info.get("bridge_port", _external_port),
            "viewer_info": info.get("viewer", {}),
        }
    else:
        viewers["external"] = {"available": False}

    # Check for local viewer
    global _viewer
    if _viewer is not None:
        viewers["local"] = {
            "available": True,
            "type": "singleton",
            "title": _viewer.title,
            "n_layers": len(_viewer.layers),
        }
    else:
        viewers["local"] = {
            "available": True,  # Can be created
            "type": "not_initialized",
        }

    return {
        "status": "ok",
        "viewers": viewers,
    }

init_viewer

MCP Tool Interface

Create or return the napari viewer (local or external).

Parameters:

Name Type Description Default
title str

Optional window title (only for local viewer).

None
width int

Optional initial canvas width (only for local viewer).

None
height int

Optional initial canvas height (only for local viewer).

None
port int

If provided, attempt to connect to an external napari-mcp bridge on this port (default is taken from NAPARI_MCP_BRIDGE_PORT or 9999).

None

Returns:

Type Description
dict

Dictionary containing status, viewer type, and layer info.

Source code in src/napari_mcp/server.py
async def init_viewer(
    title: str | None = None,
    width: int | str | None = None,
    height: int | str | None = None,
    port: int | str | None = None,
) -> dict[str, Any]:
    """
    Create or return the napari viewer (local or external).

    Parameters
    ----------
    title : str, optional
        Optional window title (only for local viewer).
    width : int, optional
        Optional initial canvas width (only for local viewer).
    height : int, optional
        Optional initial canvas height (only for local viewer).
    port : int, optional
        If provided, attempt to connect to an external napari-mcp bridge on
        this port (default is taken from NAPARI_MCP_BRIDGE_PORT or 9999).

    Returns
    -------
    dict
        Dictionary containing status, viewer type, and layer info.
    """
    return await NapariMCPTools.init_viewer(
        title=title, width=width, height=height, port=port
    )

Implementation

Create or return the napari viewer (local or external).

Parameters:

Name Type Description Default
title str

Optional window title (only for local viewer).

None
width int

Optional initial canvas width (only for local viewer).

None
height int

Optional initial canvas height (only for local viewer).

None
port int

If provided, attempt to connect to an external napari-mcp bridge on this port (default is taken from NAPARI_MCP_BRIDGE_PORT or 9999).

None

Returns:

Type Description
dict

Dictionary containing status, viewer type, and layer info.

Source code in src/napari_mcp/server.py
@staticmethod
async def init_viewer(
    title: str | None = None,
    width: int | str | None = None,
    height: int | str | None = None,
    port: int | str | None = None,
) -> dict[str, Any]:
    """
    Create or return the napari viewer (local or external).

    Parameters
    ----------
    title : str, optional
        Optional window title (only for local viewer).
    width : int, optional
        Optional initial canvas width (only for local viewer).
    height : int, optional
        Optional initial canvas height (only for local viewer).
    port : int, optional
        If provided, attempt to connect to an external napari-mcp bridge on
        this port (default is taken from NAPARI_MCP_BRIDGE_PORT or 9999).

    Returns
    -------
    dict
        Dictionary containing status, viewer type, and layer info.
    """
    # Allow overriding the external port per-call
    global _external_port
    if port is not None:
        try:
            _external_port = int(port)
        except Exception:
            logger.error("Invalid port: {port}")
            _external_port = _external_port

    async with _viewer_lock:
        # Try external viewer first; fall back to local
        try:
            return await NapariMCPTools._external_session_information(
                _external_port
            )
        except Exception:
            # No external viewer; continue to local viewer
            pass

        # Use local viewer
        v = _ensure_viewer()
        if title:
            v.title = title
        if width or height:
            w = (
                int(width)
                if width is not None
                else v.window.qt_viewer.canvas.size().width()
            )
            h = (
                int(height)
                if height is not None
                else v.window.qt_viewer.canvas.size().height()
            )
            v.window.qt_viewer.canvas.native.resize(w, h)
        # Always ensure GUI pump is running for local viewer (backwards-incompatible change)
        global _qt_pump_task
        app = _ensure_qt_app()
        with contextlib.suppress(Exception):
            app.setQuitOnLastWindowClosed(False)
        _connect_window_destroyed_signal(v)

        # Best-effort to show window without forcing focus (safer for tests/headless)
        try:
            qt_win = v.window._qt_window  # type: ignore[attr-defined]
            qt_win.show()
        except Exception:
            pass

        if _qt_pump_task is None or _qt_pump_task.done():
            loop = asyncio.get_running_loop()
            _qt_pump_task = loop.create_task(_qt_event_pump())

        _process_events()
        return {
            "status": "ok",
            "viewer_type": "local",
            "title": v.title,
            "layers": [lyr.name for lyr in v.layers],
        }

close_viewer

MCP Tool Interface

Close the viewer window and clear all layers.

Returns:

Type Description
dict

Dictionary with status: 'closed' if viewer existed, 'no_viewer' if none.

Source code in src/napari_mcp/server.py
async def close_viewer() -> dict[str, Any]:
    """
    Close the viewer window and clear all layers.

    Returns
    -------
    dict
        Dictionary with status: 'closed' if viewer existed, 'no_viewer' if none.
    """
    return await NapariMCPTools.close_viewer()

Implementation

Close the viewer window and clear all layers.

Returns:

Type Description
dict

Dictionary with status: 'closed' if viewer existed, 'no_viewer' if none.

Source code in src/napari_mcp/server.py
@staticmethod
async def close_viewer() -> dict[str, Any]:
    """
    Close the viewer window and clear all layers.

    Returns
    -------
    dict
        Dictionary with status: 'closed' if viewer existed, 'no_viewer' if none.
    """
    async with _viewer_lock:
        global _viewer, _qt_pump_task
        if _viewer is not None:
            _viewer.close()
            _viewer = None
            # Stop GUI pump when closing viewer
            if _qt_pump_task is not None and not _qt_pump_task.done():
                _qt_pump_task.cancel()
                with contextlib.suppress(asyncio.CancelledError):
                    await _qt_pump_task
            _qt_pump_task = None
            _process_events()
            return {"status": "closed"}
        return {"status": "no_viewer"}

session_information

MCP Tool Interface

Get comprehensive information about the current napari session.

Returns:

Type Description
dict

Comprehensive session information including viewer state, system info, and environment details.

Source code in src/napari_mcp/server.py
async def session_information() -> dict[str, Any]:
    """
    Get comprehensive information about the current napari session.

    Returns
    -------
    dict
        Comprehensive session information including viewer state, system info,
        and environment details.
    """
    return await NapariMCPTools.session_information()

Implementation

Get comprehensive information about the current napari session.

Returns:

Type Description
dict

Comprehensive session information including viewer state, system info, and environment details.

Source code in src/napari_mcp/server.py
@staticmethod
async def session_information() -> dict[str, Any]:
    """
    Get comprehensive information about the current napari session.

    Returns
    -------
    dict
        Comprehensive session information including viewer state, system info,
        and environment details.
    """
    import os
    import platform

    async with _viewer_lock:
        global _viewer, _qt_pump_task, _exec_globals

        try:
            return await NapariMCPTools._external_session_information(
                _external_port
            )
        except Exception:
            # No external viewer; continue to local viewer
            pass

        # Use local viewer

        # Check if viewer exists
        viewer_exists = _viewer is not None
        if not viewer_exists:
            return {
                "status": "ok",
                "session_type": "napari_mcp_standalone_session",
                "timestamp": str(np.datetime64("now")),
                "viewer": None,
                "message": "No viewer currently initialized. Call init_viewer() first.",
            }

        v = _viewer
        assert v is not None  # We already checked this above

        # Viewer information
        viewer_info = {
            "title": v.title,
            "viewer_id": id(v),
            "n_layers": len(v.layers),
            "layer_names": [layer.name for layer in v.layers],
            "selected_layers": [layer.name for layer in v.layers.selection],
            "current_step": dict(enumerate(v.dims.current_step))
            if hasattr(v.dims, "current_step")
            else {},
            "ndisplay": v.dims.ndisplay,
            "camera_center": list(v.camera.center),
            "camera_zoom": float(v.camera.zoom),
            "camera_angles": list(v.camera.angles) if v.camera.angles else [],
            "grid_enabled": v.grid.enabled,
        }

        # System information
        system_info = {
            "python_version": sys.version,
            "platform": platform.platform(),
            "napari_version": getattr(napari, "__version__", "unknown"),
            "process_id": os.getpid(),
            "working_directory": os.getcwd(),
        }

        # Session status
        gui_running = _qt_pump_task is not None and not _qt_pump_task.done()
        session_info = {
            "server_type": "napari_mcp_standalone",
            "viewer_instance": f"<napari.Viewer at {hex(id(v))}>",
            "gui_pump_running": gui_running,
            "execution_namespace_vars": list(_exec_globals.keys()),
            "qt_app_available": _qt_app is not None,
        }

        # Layer details
        layer_details = []
        for layer in v.layers:
            layer_detail = {
                "name": layer.name,
                "type": layer.__class__.__name__,
                "visible": _parse_bool(getattr(layer, "visible", True)),
                "opacity": float(getattr(layer, "opacity", 1.0)),
                "blending": getattr(layer, "blending", None),
                "data_shape": list(layer.data.shape)
                if hasattr(layer, "data") and hasattr(layer.data, "shape")
                else None,
                "data_dtype": str(layer.data.dtype)
                if hasattr(layer, "data") and hasattr(layer.data, "dtype")
                else None,
                "layer_id": id(layer),
            }

            # Add layer-specific properties
            if hasattr(layer, "colormap"):
                layer_detail["colormap"] = getattr(
                    layer.colormap, "name", str(layer.colormap)
                )
            if hasattr(layer, "contrast_limits"):
                try:
                    cl = layer.contrast_limits
                    layer_detail["contrast_limits"] = [float(cl[0]), float(cl[1])]
                except Exception:
                    pass
            if hasattr(layer, "gamma"):
                layer_detail["gamma"] = float(getattr(layer, "gamma", 1.0))

            layer_details.append(layer_detail)

        return {
            "status": "ok",
            "session_type": "napari_mcp_standalone_session",
            "timestamp": str(np.datetime64("now")),
            "viewer": viewer_info,
            "system": system_info,
            "session": session_info,
            "layers": layer_details,
        }