Skip to content

panel-live-server

panel_live_server

Panel Live Server - Execute and visualize Python code snippets.

__all__ = ['__version__'] module-attribute

__version__ = importlib.metadata.version('panel-live-server') module-attribute

app

Panel server for code visualization.

This module implements a Panel web server that executes Python code and displays the results through various endpoints.

config = get_config() module-attribute

logger = logging.getLogger(__name__) module-attribute

main(address='localhost', port=5077, show=True)

Start the Panel server.

cli

CLI for Panel Live Server.

app = typer.Typer(name='pls', help='Panel Live Server - Execute and visualize Python code snippets.', add_completion=False) module-attribute

list_app = typer.Typer(help='List resources (packages, etc.).') module-attribute

logger = logging.getLogger(__name__) module-attribute

install_browser()

Download the Chromium browser the screenshot MCP tool needs.

Playwright ships its browser binary separately from the Python package, so a pip or uv install does not fetch it automatically. Run this once after installing (pixi users get it via pixi run postinstall). It lands in the same environment that runs pls.

list_packages(filter=typer.Argument('', help='Optional substring to filter package names (case-insensitive).', show_default=False))

List all Python packages installed in the current environment.

Optionally filter by a substring, e.g. pls list packages panel to show only packages whose name contains "panel".

main()

Entry point for the pls command.

main_callback(ctx, version=False)

Panel Live Server - Execute and visualize Python code snippets.

mcp(transport=typer.Option('stdio', '--transport', '-t', help='MCP transport: stdio, http, or sse.', envvar='PANEL_LIVE_SERVER_TRANSPORT'), host=typer.Option('127.0.0.1', '--host', help='Host for HTTP/SSE transport.', envvar='PANEL_LIVE_SERVER_MCP_HOST'), port=typer.Option(8001, '--port', '-p', help='Port for HTTP/SSE transport.', envvar='PANEL_LIVE_SERVER_MCP_PORT'), verbose=typer.Option(False, '--verbose', '-v', help='Enable verbose logging.'))

Start as an MCP server for AI assistants.

The MCP server exposes the show tool for executing and displaying Python visualizations. A Panel visualization server starts automatically on a per-environment port (override with PANEL_LIVE_SERVER_PORT) — run pls status to see the address, then visit its /feed in a browser to watch visualizations appear in real time.

Note: the --port flag here controls the MCP HTTP/SSE listener, NOT the Panel visualization server port. For stdio transport, --port is unused.

serve(port=typer.Option(None, '--port', '-p', help='Port to run the Panel server on. Defaults to a per-environment port derived from the interpreter.', envvar='PANEL_LIVE_SERVER_PORT'), host=typer.Option('localhost', '--host', '-H', help='Host address to bind to.', envvar='PANEL_LIVE_SERVER_HOST', show_default=True), db_path=typer.Option(None, '--db-path', help='Path to the SQLite database file.', envvar='PANEL_LIVE_SERVER_DB_PATH'), show=typer.Option(False, '--show', help='Open the server in a browser after starting.'), verbose=typer.Option(False, '--verbose', '-v', help='Enable verbose logging.'))

Start the Panel Live Server directly.

The server provides a web interface for executing Python code snippets and visualizing the results. Visit http://:/feed to see visualizations as they are created.

Note: pls serve and pls mcp launched from the same environment resolve to the same per-environment default port, so a browser opened here shows the visualizations the MCP server renders. Set PANEL_LIVE_SERVER_PORT (or --port) to override.

status(port=typer.Option(None, '--port', '-p', help='Port to check. Defaults to the per-environment port derived from the interpreter.', envvar='PANEL_LIVE_SERVER_PORT'), host=typer.Option('localhost', '--host', '-H', help='Host to check.', envvar='PANEL_LIVE_SERVER_HOST', show_default=True))

Check whether the Panel server is running.

Queries the health endpoint and reports the server status.

version_callback(value)

Print version and exit.

client

HTTP client for Display Server REST API.

This module provides a client for interacting with the Panel Display Server via its REST API. The client can be used with either a locally-managed subprocess or a remote server instance.

logger = logging.getLogger(__name__) module-attribute

DisplayClient

HTTP client for Display Server REST API.

This client handles all HTTP communication with the Panel Display Server, including health checks and snippet creation. It uses a persistent session for connection pooling.

base_url = base_url.rstrip('/') instance-attribute
session = requests.Session() instance-attribute
timeout = timeout instance-attribute
close()

Close the HTTP session and cleanup resources.

create_snippet(code, name='', description='', method='inline', validated=False)

Create a visualization snippet on the Display Server.

Sends Python code to the server for execution and rendering.

Parameters:

Name Type Description Default
code str

Python code to execute

required
name str

Name for the visualization

''
description str

Description of the visualization

''
method str

Execution method ("inline" or "server")

'inline'
validated bool

When True, signals that the code was already validated and executed by the MCP show tool, so the server can skip its redundant storage-time validation and execution.

False

Returns:

Type Description
dict

Server response containing either: - Success: {"url": str, "id": str, ...} - Error: {"error": str, "message": str, "traceback": str}

Raises:

Type Description
RuntimeError

If HTTP request fails

get_embed_html(snippet_id)

Fetch self-contained static HTML for an inline-method snippet.

Returns None on failure so the caller can degrade gracefully.

get_screenshot(snippet_id, width=None, height=None, full_page=False)

Fetch a PNG screenshot of a snippet's rendered /view page.

Returns:

Type Description
tuple[bytes | None, str | None]

(png_bytes, None) on success, or (None, error_message) on failure.

is_healthy()

Check if Display Server is healthy.

Returns:

Type Description
bool

True if server responds to health check, False otherwise

config

Configuration for Panel Live Server.

logger = logging.getLogger('panel_live_server') module-attribute

Config

Bases: BaseModel

Panel Live Server configuration.

db_path = Field(default_factory=(lambda: _default_user_dir() / 'snippets' / 'snippets.db'), description='Path to SQLite database for snippets') class-attribute instance-attribute
external_url = Field(default='', description='Externally reachable base URL for the Panel server (port-inclusive). Auto-detected from JUPYTERHUB_HOST + JUPYTERHUB_SERVICE_PREFIX (JupyterHub) or CODESPACE_NAME (GitHub Codespaces) if not set explicitly via PANEL_LIVE_SERVER_EXTERNAL_URL.') class-attribute instance-attribute
host = Field(default='localhost', description='Host address for the Panel server') class-attribute instance-attribute
max_restarts = Field(default=3, description='Maximum number of restart attempts') class-attribute instance-attribute
port = Field(default=5077, description='Port for the Panel server') class-attribute instance-attribute
screenshot_height = Field(default=800, description='Viewport height (px) for screenshot capture') class-attribute instance-attribute
screenshot_settle_ms = Field(default=1200, description='Delay (ms) after content mounts before capturing, to let Bokeh finish drawing') class-attribute instance-attribute
screenshot_timeout_ms = Field(default=30000, description='Max time (ms) to wait for the page to load before capturing') class-attribute instance-attribute
screenshot_width = Field(default=1200, description='Viewport width (px) for screenshot capture') class-attribute instance-attribute

default_panel_port()

Return the Panel server port for the active Python environment.

An explicit PANEL_LIVE_SERVER_PORT always wins. Otherwise the port is derived deterministically from the interpreter (sys.prefix) so that each environment gets its own server.

This is what keeps pls executing snippets against the packages the user expects: the Panel server subprocess runs in the same interpreter as pls itself, but a single fixed port would let an MCP client launched from one environment silently adopt a server already running in another. That server executes code against its installed packages, so an import the user just installed alongside pls shows up as missing. A per-environment port means different environments no longer collide on one server.

get_config()

Get or create the config instance.

reset_config()

Reset config (for testing).

database

Database models and operations for the display server.

This module handles SQLite database operations for storing and retrieving visualization requests.

logger = logging.getLogger(__name__) module-attribute

Snippet

Bases: BaseModel

Model for a code snippet stored in the database.

Represents a code snippet submitted to the Display System for visualization.

app = Field(..., description='Python code to execute') class-attribute instance-attribute
created_at = Field(default_factory=(lambda: datetime.now(timezone.utc))) class-attribute instance-attribute
description = Field(default='', description='Short description of the app') class-attribute instance-attribute
error_message = Field(default=None, description="Error details if status='error'") class-attribute instance-attribute
execution_time = Field(default=None, description='Execution time in seconds') class-attribute instance-attribute
extensions = Field(default_factory=list, description='Inferred Panel extensions') class-attribute instance-attribute
id = Field(default_factory=(lambda: str(uuid.uuid4()))) class-attribute instance-attribute
method = Field(..., description='Execution method') class-attribute instance-attribute
name = Field(default='', description='User-provided name') class-attribute instance-attribute
readme = Field(default='', description='Longer documentation describing the app') class-attribute instance-attribute
requirements = Field(default_factory=list, description='Inferred required packages') class-attribute instance-attribute
slug = Field(default='', description='URL-friendly slug for persistent links') class-attribute instance-attribute
status = Field(default='pending') class-attribute instance-attribute
tags = Field(default_factory=list, description='List of tags') class-attribute instance-attribute
updated_at = Field(default_factory=(lambda: datetime.now(timezone.utc))) class-attribute instance-attribute
user = Field(default='guest', description='User who created the snippet') class-attribute instance-attribute
validate_slug(v) classmethod

Validate that slug is either empty or a valid URL slug.

SnippetDatabase

SQLite database manager for code snippets.

Manages storage and retrieval of Snippet records (code snippets) submitted to the Display System.

db_path = db_path instance-attribute
create_snippet(snippet)

Create a new snippet record.

Parameters:

Name Type Description Default
snippet Snippet

Snippet record to create

required

Returns:

Type Description
Snippet

Created snippet record with ID

create_visualization(app, name='', description='', readme='', method='inline', skip_validation=False)

Create a visualization request.

This is the core business logic for creating visualizations, shared by both the HTTP API endpoint and the UI form.

Parameters:

Name Type Description Default
app str

Python code to execute

required
name str

Display name for the visualization

''
description str

Short description of the visualization

''
readme str

Longer documentation describing the app

''
method str

Execution method: "inline", "server", or "pyodide"

'inline'
skip_validation bool

When True, skip the syntax/security/package/extension/format/runtime layers because the caller (the MCP show tool) has already validated and executed the code. Avoids running the snippet a second time on the render hot path. The web /add form leaves this False so untrusted input is still fully validated.

False

Returns:

Type Description
Snippet

The snippet created for the visualization request.

Raises:

Type Description
ValueError

If app is empty or contains unsupported operations

SyntaxError

If app has syntax errors

Exception

If database operation or other errors occur

delete_snippet(snippet_id)

Delete a snippet record.

Parameters:

Name Type Description Default
snippet_id str

Snippet ID

required

Returns:

Type Description
bool

True if deleted, False if not found

get_snippet(snippet_id)

Get a snippet record by ID.

Parameters:

Name Type Description Default
snippet_id str

Snippet ID

required

Returns:

Type Description
Optional[Snippet]

Snippet record if found, None otherwise

get_snippet_by_slug(slug)

Get the most recent snippet record by slug.

Parameters:

Name Type Description Default
slug str

Snippet slug

required

Returns:

Type Description
Optional[Snippet]

Most recent snippet record with this slug if found, None otherwise

list_snippets(limit=100, offset=0, start=None, end=None, status=None, method=None)

List snippet records with filters.

Parameters:

Name Type Description Default
limit int

Maximum number of snippets to return

100
offset int

Number of snippets to skip

0
start Optional[datetime]

Filter snippets created after this time

None
end Optional[datetime]

Filter snippets created before this time

None
status Optional[str]

Filter by status

None
method Optional[str]

Filter by method

None

Returns:

Type Description
list[Snippet]

List of snippet records

search_snippets(query, limit=100)

Search snippet records using full-text search.

Parameters:

Name Type Description Default
query str

Search query

required
limit int

Maximum number of results

100

Returns:

Type Description
list[Snippet]

Matching snippet records

update_snippet(snippet_id, status=None, error_message=None, execution_time=None, requirements=None, extensions=None)

Update a snippet record.

Parameters:

Name Type Description Default
snippet_id str

Snippet ID

required
status Optional[str]

New status

None
error_message Optional[str]

Error message

None
execution_time Optional[float]

Execution time

None
requirements Optional[list[str]]

Required packages

None
extensions Optional[list[str]]

Required extensions

None

Returns:

Type Description
bool

True if updated, False if not found

get_db(db_path=None)

Get or create the SnippetDatabase instance.

This function implements lazy initialization with a global cache. The database instance is created once and reused across the application.

Parameters:

Name Type Description Default
db_path Optional[Path]

Path to database file. If None, uses default from environment/config. Only used on first call; subsequent calls ignore this parameter.

None

Returns:

Type Description
SnippetDatabase

Shared database instance

reset_db()

Reset the database instance.

This is primarily for testing purposes to ensure a clean state.

endpoints

REST API endpoints for the Display System.

This module implements Tornado RequestHandler classes that provide HTTP endpoints for creating visualizations and checking server health.

logger = logging.getLogger(__name__) module-attribute

EmbedEndpoint

Bases: RequestHandler

Render a snippet to self-contained HTML via GET /api/embed?id=...

Used for method="inline" visualizations (hvplot, holoviews, bokeh, matplotlib, plotly). Returns a complete HTML document with CDN-loaded Bokeh/Panel resources suitable for embedding via iframe.srcdoc. JS-side interactivity (CustomJS, jslink, hover/zoom) is preserved; Python callbacks are not.

get()

Render the snippet identified by ?id= as static HTML.

HealthEndpoint

Bases: RequestHandler

Tornado RequestHandler for /api/health endpoint.

get()

Handle GET requests to check server health.

The payload reports the interpreter running this server (sys.prefix and sys.executable) so a manager can tell whether a server already listening on the port belongs to its own environment before adopting it.

ScreenshotEndpoint

Bases: RequestHandler

Render a snippet's /view page to a PNG via GET /api/screenshot?id=...

Loads the live /view page in a headless browser (Playwright) and returns a PNG, giving LLMs a picture of the rendered output — layout, fonts, and margins as a user would see them. When no browser is installed/launchable this returns HTTP 503 with an install hint so the caller can surface a clear message instead of failing opaquely.

Query parameters

id : str Snippet id to render (required). width, height : int Viewport size in px (default from config). full_page : bool Capture the full scrollable page rather than just the viewport.

get() async

Capture and return the snippet identified by ?id= as a PNG.

SnippetEndpoint

Bases: RequestHandler

Tornado RequestHandler for /api/snippet endpoint.

post()

Handle POST requests to store snippets and create visualizations.

manager

Panel server subprocess management.

This module manages the Panel server as a subprocess, including startup, health checks, and shutdown.

logger = logging.getLogger(__name__) module-attribute

PanelServerManager

Manages the Panel server subprocess.

db_path = db_path instance-attribute
host = host instance-attribute
max_restarts = max_restarts instance-attribute
port = port instance-attribute
process = None instance-attribute
restart_count = 0 instance-attribute
get_base_url()

Get the base URL for the Panel server.

Returns:

Type Description
str

Base URL

is_healthy()

Check if Panel server is healthy.

Returns:

Type Description
bool

True if server is healthy, False otherwise

restart()

Restart the Panel server.

Returns:

Type Description
bool

True if restarted successfully, False otherwise

start()

Start the Panel server subprocess.

Returns:

Type Description
bool

True if started successfully, False otherwise

stop(timeout=5)

Stop the Panel server subprocess.

Parameters:

Name Type Description Default
timeout int

Maximum time to wait for graceful shutdown

5

pages

Panel page functions for Panel Live Server.

add_page

Add page for creating new visualizations.

This module implements the /add page endpoint that provides a form for manually creating visualizations via the UI.

ABOUT = '\n## Add Visualization\n\nThis page allows you to create new visualizations by writing Python code.\n\n### How to Use\n\n1. **Write Code**: Enter your Python visualization code in the editor\n2. **Configure**: Set a name, description, and execution method in the sidebar\n3. **Submit**: Click the Submit button to create the visualization\n\n### Execution Methods\n\n- **jupyter**: The last expression in the code is displayed (like a Jupyter cell)\n- **panel**: Objects marked with `.servable()` are displayed as a Panel app\n\n### Learn More\n\nFor more information about this project, visit:\n[Panel Live Server](https://github.com/panel-extensions/panel-live-server).\n' module-attribute
DEFAULT_SNIPPET = "import pandas as pd\nimport hvplot.pandas\n\ndf = pd.DataFrame({\n 'Product': ['A', 'B', 'C', 'D'],\n 'Sales': [120, 95, 180, 150]\n})\n\ndf.hvplot.bar(x='Product', y='Sales', title='Sales by Product')" module-attribute
logger = logging.getLogger(__name__) module-attribute
add_page()

Create the /add page for manually creating visualizations.

Provides a UI form for entering code, name, description, and execution method.

admin_page

Admin page for managing snippets.

This module implements the /admin page endpoint that allows viewing and deleting snippets from the database.

ABOUT = '\n## Snippet Manager\n\nThis page provides an administrative interface for managing all visualizations\nstored in the database.\n\n### Features\n\n- **View All Snippets**: See all visualizations with their name, description, method, status, and creation date\n- **View Code**: Expand any row to see the full Python code for that visualization\n- **Delete Snippets**: Remove visualizations you no longer need\n- **Direct Links**: Click the link icon to view any visualization\n\n### Learn More\n\nFor more information about this project, visit:\n[Panel Live Server](https://github.com/panel-extensions/panel-live-server).\n' module-attribute
admin_page()

Create the /admin page.

Provides an administrative interface for managing all snippets in the database.

feed_page

Feed page showing a scrollable list of visualizations.

This module implements the /feed page endpoint that displays recent visualizations in a feed-style layout with live updates.

ABOUT = '\n## Visualization Feed\n\nThis page displays a live feed of recent visualizations created through the Panel Live Server display tool.\n\n### Features\n\n- **Live Updates**: The feed automatically refreshes every second to show new visualizations\n- **View / Code Tabs**: Each visualization shows both an interactive preview and the source code\n- **Actions**: Open visualizations in full screen, copy code to clipboard, or delete entries\n- **Limit Control**: Use the sidebar to control how many visualizations are displayed\n\n### How It Works\n\nWhen an AI assistant uses the `show` tool to display a visualization, it appears here in the feed.\nEach entry includes the visualization name, creation time, description, and an iframe preview.\n\n### Learn More\n\nFor more information about this project, including setup instructions and advanced configuration options,\nvisit: [Panel Live Server](https://github.com/panel-extensions/panel-live-server).\n' module-attribute
feed_page()

Create the /feed page.

Displays a feed of recent visualizations with automatic updates.

view_page

View page for displaying individual visualizations.

This module implements the /view page endpoint that executes and displays a single visualization by ID.

logger = logging.getLogger(__name__) module-attribute
create_view(snippet_id)

Create a view for a single visualization snippet.

Parameters:

Name Type Description Default
snippet_id str

ID of the snippet to display

required

Returns:

Type Description
Viewable

Panel component displaying the visualization

view_page()

Create the /view page.

Renders a single visualization by ID or slug from the query string parameter. Supports ?id=... or ?slug=... query parameters. If both are provided, id takes precedence.

screenshot

Headless-browser screenshot capture for rendered Panel snippets.

Wraps Playwright (Chromium) to load a /view page and capture a PNG so the MCP screenshot tool can hand an LLM a picture of the rendered output — the actual layout, fonts, and margins as a user would see them, not just the source code.

Playwright is a required dependency (included in the base install). Import / launch failures are surfaced as :class:PlaywrightUnavailableError with an install hint so callers can degrade gracefully instead of crashing.

logger = logging.getLogger(__name__) module-attribute

PlaywrightUnavailableError

Bases: RuntimeError

Raised when Playwright or its browser is not installed/launchable.

capture_png(url, *, width=1200, height=800, full_page=False, settle_ms=1200, timeout_ms=30000) async

Capture a PNG screenshot of url using a shared headless browser.

Returns:

Type Description
bytes

PNG image data.

Raises:

Type Description
PlaywrightUnavailableError

If Playwright or a launchable browser is not available.

install_browser()

Download the headless Chromium browser the screenshot tool needs.

Playwright ships its browser binary separately from the Python package, so pip/uv installs do not fetch it automatically. This shells out to <this-interpreter> -m playwright install chromium so the browser always lands in the same environment that is running pls — avoiding the common trap where the binary is installed under a different interpreter.

Returns:

Type Description
int

The installer subprocess exit code (0 on success).

is_browser_installed()

Return True if the Chromium binary Playwright needs is present.

This is a cheap check — it does not launch a browser. It uses Playwright's sync API, so call it from a worker thread (e.g. asyncio.to_thread), not directly inside a running event loop.

server

Panel Live Server - MCP Server.

A standalone MCP server that provides the show tool for executing Python code and rendering visualizations via a Panel web server.

SHOW_RESOURCE_URI = 'ui://panel-live-server/show.html' module-attribute

SHOW_TEMPLATE_PATH = Path(__file__).parent / 'templates' / 'show.html' module-attribute

logger = logging.getLogger(__name__) module-attribute

mcp = FastMCP('Panel Live Server', instructions=("Panel Live Server executes Python code snippets and renders the resulting visualizations as live, interactive web pages.\n\nWORKFLOW:\nCall `show(code, name, method)` to render a visualization.\nStatic validation (syntax, security, packages) runs in ~50 ms.\nThe iframe loads immediately via Panel's WebSocket — no prior validate() needed.\n\nDO NOT write the visualization code to a file, script, notebook, or `examples/`\ndirectory, and do not run it in a separate shell/REPL. Pass the code string\nstraight to `show(code=...)` — it executes the code for you. Creating files\nclutters the user's repository and is not wanted.\n\nLIBRARY SELECTION:\nPRIMARILY write visualizations with HoloViz packages — they integrate best with\nthe live server and are ALWAYS installed:\n- hvPlot: quick interactive plots from DataFrames (.hvplot API)\n- HoloViews: advanced composable, interactive visualizations\n- Panel: dashboards, data apps, complex layouts (use method='server')\nAlso always available (no extra package needed):\n- Bokeh: HoloViz's default backend, for low-level interactive web plots\n- ECharts (pn.pane.ECharts): business-quality charts with transitions/animations, from a spec dict\n- deck.gl (pn.pane.DeckGL): large-scale geospatial and 3D visualization, from a spec dict\nOther well-known libraries (Matplotlib, Plotly, seaborn, Altair) are installed ONLY\nwhen the optional 'pydata' dependencies are present, so they may be MISSING — prefer\nHoloViz. You do NOT need to check availability up front: if an import is not installed,\n`show` returns the validation error — read it and rewrite using a HoloViz package.\n\n" + _CLAUDE_DESKTOP_INSTRUCTIONS + 'OUTPUT\nAfter calling `show`, ALWAYS present the returned URL to the user as a clickable Markdown link: [Show Visualization](url)\n\nERRORS\n`show` raises `SecurityError` for blocked imports or dangerous patterns — these require a substantive code rewrite, not a retry. `show` raises `ValidationError` for syntax errors, missing packages, or missing Panel extension declarations — fix the reported issue and try again.'), lifespan=app_lifespan) module-attribute

app_lifespan(app) async

MCP server lifespan - eagerly start the Panel server.

screenshot(snippet_id, width=1200, height=800, full_page=False, ctx=None) async

See an EXISTING visualization — returns a PNG image of it to you (the LLM).

Pass the snippet_id that show returned; this tool screenshots that already-rendered /view page and hands you the picture so you can answer a follow-up question about how it LOOKS. It does NOT create or modify anything and is NOT a substitute for show — the user already has the interactive visualization in their browser.

════════════════════════════════════════════════════════════════════════ CRITICAL RULE — answering questions ABOUT a visualization's appearance: ════════════════════════════════════════════════════════════════════════ When the user asks where something is, which element is biggest/smallest, what color/position/shape something has, or anything about how the chart LOOKS, you MUST call this tool and answer from the returned image.

You MUST NOT answer such questions by reading the code, recomputing from the raw data, or re-running the snippet in a Python tool. THAT IS CHEATING AND IS USUALLY WRONG, because the rendered plot is NOT the same as the raw data: - heatmaps flip/reverse the row order (Row 0 often renders at the BOTTOM) - axes get inverted, categories get sorted, histograms bin/group values - color mapping, stacking, and layout change what is visually "highest" The raw-data answer and the on-screen answer frequently DISAGREE. The image is the only ground truth for a question about appearance — so look at it.

Do not add np.random.seed(...) or otherwise make data deterministic just so you can recompute it; read the answer off the actual picture.

════════════════════════════════════════════════════════════════════════ IMAGE QUALITY — when the picture is not enough: ════════════════════════════════════════════════════════════════════════ After receiving the screenshot, check whether it is clear enough to answer: - Is the chart blurry or pixelated? - Is the relevant detail (a label, a tick value, a legend entry) too small to read confidently? - Is the area of interest clipped or off-screen?

If YES — the image is not reliable enough — do NOT guess from it. Instead, answer the question directly from the code and data (compute the value, read the label, inspect the structure). A code-derived answer is better than a wrong guess from a bad image.

If the image is fine, always prefer it over recomputing (see CRITICAL RULE above — rendered output and raw data frequently disagree).

WHEN TO USE — a follow-up question about an already-shown visualization that can only be answered by seeing it (random/dynamic data, or visual position): · wave/line chart → "where does it peak?", "where is the lowest dip?" · bar chart → "which bar is the tallest?", "which category leads?" · scatter plot → "where are the outliers?", "how spread out are the points?" · heatmap → "which cell has the highest value?" · pie/donut chart → "which slice is the largest?" · histogram → "where is the distribution centered?" · any chart → "what color is X?", "what does the legend say?"

Typical loop: show (returns id) → screenshot(snippet_id=id) when the user asks something visual you can't read off the code.

Parameters:

Name Type Description Default
snippet_id str

Id of the visualization to screenshot, as returned by show.

required
width int

Browser viewport width in pixels.

1200
height int

Browser viewport height in pixels.

800
full_page bool

If True, capture the full scrollable page rather than just the viewport. Useful for tall dashboards.

False

Returns:

Type Description
Image

PNG screenshot of the rendered visualization.

show(code, name='', description='', method='inline', zoom=75, ctx=None) async

Display Python code as a live, interactive visualization.

Runs static validation (syntax, security, packages, extensions) in ~50 ms, stores the snippet, and returns the visualization URL. The iframe loads immediately via Panel's WebSocket — the user sees a loading indicator then the rendered visualization, with no prior validate() call needed.

Always call this tool when the user asks to show, display, plot, or visualize anything.

IMPORTANT — pass the Python code DIRECTLY as the code argument. Do NOT write it to a file in the user's project first, do NOT create scripts, notebooks, or examples/ files, and do NOT run it in a separate shell. This tool executes the code itself; creating files is unwanted side-effect clutter in the user's repository.

IMPORTANT — always provide a short name (e.g. "Temperature chart") so the visualization is easy to find in the feed.

IMPORTANT — after calling this tool, always present the returned url to the user as a clickable Markdown link: [Show Visualization](url)

Parameters:

Name Type Description Default
code str

Python code to execute. For "inline" method: the last expression is displayed. It must be fully dedented (no leading whitespace on top-level statements). For "server" method: call .servable() on objects to display.

required
name str

Short display name shown in the feed (e.g. "Sales chart 2024"). Always provide this — unnamed visualizations are hard to track.

''
description str

One-sentence description of what the visualization shows.

''
method (inline, server)

Execution mode:

  • "inline": displays the last expression's result. Use for standard plots, DataFrames, and objects that do NOT import Panel directly.
  • "server": displays objects marked .servable(). Use when the code imports and uses Panel to build dashboards or complex layouts.
"inline"
zoom (100, 75, 50, 25)

Initial zoom level for the preview pane. 75 fits most charts and dashboards. Use 50 for full-page templates, 25 for very wide apps.

100

Returns:

Type Description
str

JSON payload for MCP App rendering, including the visualization URL.

show_view()

Return the HTML resource used by the show MCP App.

ui

Shared UI components for the Display Server.

banner()

Create a banner indicating alpha/experimental software.

utils

Utilities for inferring required packages and Panel extensions from code.

logger = logging.getLogger(__name__) module-attribute

ExtensionError

Bases: Exception

Custom exception for missing Panel extensions.

execute_in_module(code, module_name, *, cleanup=True)

Execute Python code in a proper module namespace.

Creates a types.ModuleType following Bokeh's pattern, registers it in sys.modules, executes code, and optionally cleans up. This ensures Panel decorators (@pn.cache, @pn.depends) and function references work properly by using module.dict as a single namespace for both globals and locals.

Parameters:

Name Type Description Default
code str

Python code to execute

required
module_name str

Unique name for the module (should be a valid Python identifier)

required
cleanup bool

Whether to remove module from sys.modules after execution. Set to False if you need to keep the module registered (e.g., for later eval calls), but remember to clean up manually.

True

Returns:

Type Description
dict[str, Any]

The module's namespace (module.dict) after execution

Raises:

Type Description
Exception

Any exception raised during code execution

Notes

This pattern is critical for Panel decorators and code that uses function introspection or cross-references. It follows Bokeh's CodeRunner pattern.

Examples:

>>> namespace = execute_in_module(
...     "x = 1\ny = 2\nz = x + y",
...     "my_module"
... )
>>> namespace['z']
3

extract_last_expression(code)

Extract the last expression from code for jupyter method.

Parameters:

Name Type Description Default
code str

Python code

required

Returns:

Type Description
tuple[str, str]

(statements_code, last_expression_code)

find_extensions(code, namespace=None)

Infer Panel extensions required for code execution.

Maps common packages/types to their Panel extensions: - pandas DataFrame/Series -> "tabulator" - plotly figures -> "plotly" - bokeh models -> (none, default) - matplotlib figures -> (none, uses pngpane) - altair charts -> "vega" - deck.gl -> "deckgl"

Parameters:

Name Type Description Default
code str

Python code to analyze

required
namespace dict[str, Any] | None

Namespace from code execution (optional)

None

Returns:

Type Description
list[str]

List of required Panel extension names

find_requirements(code)

Find package requirements from code.

Uses Panel's built-in find_requirements function to detect package dependencies.

Parameters:

Name Type Description Default
code str

Python code to analyze

required

Returns:

Type Description
list[str]

List of required package names

get_relative_view_url(id)

Generate a relative URL for viewing a visualization by ID.

Parameters:

Name Type Description Default
id str

Visualization ID

required

Returns:

Type Description
str

Relative URL to view the visualization

prepend_env_dll_paths(env)

Prepend conda/pixi environment DLL directories to env["PATH"].

On Windows, compiled extensions such as numpy require <env>/Library/bin and <env>/DLLs to be on PATH so Windows can locate the native DLLs at import time. These entries are added automatically when a shell activates the environment (e.g. via conda activate or pixi run), but external launchers such as MCP clients typically do not activate the environment.

This function is idempotent: if the entries are already present in env["PATH"] they are not duplicated.

It operates on any str → str mapping so it can patch both os.environ (for the current process) and a freshly copied dict (for a subprocess env= argument).

Parameters:

Name Type Description Default
env dict[str, str]

Environment mapping to update in-place. Normally os.environ or a copy of it.

required

Returns:

Type Description
dict[str, str]

The same env mapping, updated in-place (returned for convenience).

validate_code(code)

Execute code in a thread with a timeout to catch runtime errors.

Runs code in a separate thread so the caller is not blocked indefinitely by long-running or hanging code. The thread is a daemon — on timeout it continues running until the process exits (threads cannot be forcibly killed).

Parameters:

Name Type Description Default
code str

Python code to validate as a string.

required

Returns:

Type Description
str

Empty string if the code runs without error, otherwise the traceback or a timeout message.

validate_extension_availability(code)

Validate that required Panel Javascript extensions are loaded in the code.

Parameters:

Name Type Description Default
code str

Python code to analyze

required

Raises:

Type Description
ExtensionError

If a required extension is not loaded.

Example
This code will raise an ExtensionError because 'tabulator' extension is not available:

code = ''' import pandas as pd import panel as pn pn.extension()

df = pd.DataFrame({'A': [1, 2], 'B': [3, 4]}) pn.widgets.Tabulator(df).servable() '''

validate_extension_availability(code)

This code will pass as 'tabulator' extension is included:

code = ''' import pandas as pd import panel as pn pn.extension('tabulator') df = pd.DataFrame({'A': [1, 2], 'B': [3, 4]}) pn.widgets.Tabulator(df).servable() '''

validate_extension_availability(code)

Note

pn.extension("tabulator", "plotly")

and

pn.extension("tabulator") pn.extension("plotly")

will both work correctly.

validation

Code validation pipeline for panel-live-server.

Provides four static validation layers that run before code is stored:

  • Layer 1 ast_check — syntax via ast.parse()
  • Layer 2 ruff_check — security rules via ruff (raises SecurityError)
  • Layer 3 check_packages — all imports are installed
  • Formatting ruff_format — autoformat via ruff format

Runtime execution (Layer 5) lives in utils.validate_code.

BLOCKED_IMPORTS = frozenset({'pickle', 'marshal', 'shelve', 'subprocess', 'multiprocessing', 'threading', 'socket', 'ctypes', 'importlib', 'ftplib', 'smtplib', 'telnetlib', 'webbrowser', 'xmlrpc'}) module-attribute

IMPORT_TO_PACKAGE = {'PIL': 'Pillow', 'sklearn': 'scikit-learn', 'cv2': 'opencv-python', 'skimage': 'scikit-image', 'bs4': 'beautifulsoup4', 'yaml': 'PyYAML', 'dateutil': 'python-dateutil', 'dotenv': 'python-dotenv', 'gi': 'PyGObject', 'wx': 'wxPython', 'Crypto': 'pycryptodome', 'OpenSSL': 'pyOpenSSL', 'usb': 'pyusb', 'serial': 'pyserial', 'magic': 'python-magic', 'attr': 'attrs'} module-attribute

logger = logging.getLogger(__name__) module-attribute

SecurityError

Bases: ToolError

Raised by show() when code contains a security violation.

Given a special class (separate from ValidationError) to signal seriousness — security violations are never auto-fixable and should not be retried without a substantive code rewrite. Particularly relevant in enterprise contexts where security policy enforcement is audited.

ValidationError

Bases: ToolError

Raised by show() when code fails a non-security validation check.

Covers syntax errors, missing packages, and missing Panel extension declarations. The message always begins with the layer name in brackets, e.g. [syntax] invalid syntax so the LLM can identify the failing check at a glance.

ast_check(code)

Return an error string if code has a syntax error, else None.

Parameters:

Name Type Description Default
code str

Python source to check.

required

Returns:

Type Description
str | None

Human-readable error with line/col info, or None if syntax is valid.

Examples:

>>> ast_check("x = 1")
>>> ast_check("if True")
'expected \':\' (line 1, col 8)'

check_packages(code)

Check that all packages imported by code are installed.

Parses imports via AST and calls importlib.util.find_spec() for each top-level module name. Stdlib modules are skipped. Returns an error string for the first missing package, or None if everything is available.

Parameters:

Name Type Description Default
code str

Python source to analyse.

required

Returns:

Type Description
str | None

Error string with install hint, or None if all packages are available.

Examples:

>>> check_packages("import os\\nimport json") is None
True
>>> check_packages("import numpy") is None
True
>>> "HoloViz" in (check_packages("import _totally_fake_pkg_xyz") or "")
True

ruff_check(code)

Run import blocklist and ruff security checks on code.

First performs a fast AST-based blocked-import scan (does not depend on ruff being installed), then runs ruff for deeper static analysis.

Raises SecurityError if any violations are found. Returns None silently if the code is clean or ruff is not installed.

Parameters:

Name Type Description Default
code str

Python source to check.

required

Raises:

Type Description
SecurityError

If a blocked import is found or ruff reports any violations.

Examples:

>>> ruff_check("x = 1")
>>> ruff_check("import pickle")
SecurityError: ...

ruff_format(code)

Autoformat code via ruff format and return the result.

Returns code unchanged if ruff is not installed or formatting fails.

Parameters:

Name Type Description Default
code str

Python source to format.

required

Returns:

Type Description
str

Formatted source, or original source on failure.

Examples:

>>> ruff_format("x=1+2") == "x = 1 + 2\\n"
True