Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@AGENTS.md

Copilot AI Feb 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CLAUDE.md file addition is not mentioned in the PR description or test plan, and appears unrelated to the "Enhancement/browser tools" changes. If this file is intentional, consider adding it in a separate PR with appropriate documentation explaining its purpose. If it's accidental, it should be removed from this PR.

Copilot uses AI. Check for mistakes.
36 changes: 27 additions & 9 deletions docs/explanation/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,18 +75,36 @@ Tools for discovering and working with Panel components.

**Demo**: [https://awesome-panel-holoviz-mcp-ui.hf.space/panel_get_component_parameters](https://awesome-panel-holoviz-mcp-ui.hf.space/panel_get_component_parameters)

### panel_take_screenshot
### panel_inspect_app

**Purpose**: Take a screenshot of your (panel) web app.
**Purpose**: Inspect your (Panel) web app by capturing a screenshot and/or browser console logs.

**Parameters**:
- `url` (string): The url to take the screenshot of. Default is 'http://localhost:5006/'

**Use Case**: Understand how the app looks

**Returns**: ImageContent.

**Example Query**: *"Take a screenshot of http://127.0.0.1:8000/"**
- `url` (string): The URL to inspect. Default is `http://localhost:5006/`
- `width` (int): The width of the browser viewport. Default is 1920
- `height` (int): The height of the browser viewport. Default is 1200
- `full_page` (bool): Whether to capture the full scrollable page. Default is False
- `delay` (int): Seconds to wait after page load before capturing. Default is 2
- `save_screenshot` (bool | string): Whether and where to save the screenshot to disk. Default is False
- `True`: Save to default screenshots directory (`~/.holoviz-mcp/screenshots/`) with auto-generated filename
- `False`: Don't save screenshot to disk (only return to AI)
- `string`: Save to specified absolute path (raises ValueError if path is not absolute)
- `screenshot` (bool): Whether to capture a screenshot. Default is True
- `console_logs` (bool): Whether to capture browser console logs. Default is True
- `log_level` (string, optional): Filter console logs by level (e.g., `"error"`, `"warning"`, `"log"`)

**Use Case**: Understand how the app looks, debug JavaScript errors, and inspect browser console output — all in a single call.

**Returns**: A list containing:
- `ImageContent` (screenshot) when `screenshot=True`
- `TextContent` (console logs as JSON) when `console_logs=True`

**Note**: At least one of `screenshot` or `console_logs` must be True. When `save_screenshot=True` or a path is provided, the screenshot is also saved to disk with a timestamp-based filename (e.g., `screenshot_2026-02-09_12-01-03_abc123.png`).

**Example Queries**:
- *"Take a screenshot of http://127.0.0.1:8000/"*
- *"Check my Panel app for JavaScript errors"*
- *"Show me the console errors from my app"*

## HoloViews Tool

Expand Down
18 changes: 18 additions & 0 deletions src/holoviz_mcp/config/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,24 @@ docs:
folders:
docs/bokeh/source/docs:
url_path: "/"
panel-live:
url: "https://github.com/panel-extensions/panel-live.git"
branch: main
url_transform: "plotly"
folders:
docs:
url_path: ""
base_url: "https://panel-extensions.github.io/panel-live"
reference_patterns: []
panel-reactflow:
url: "https://github.com/panel-extensions/panel-reactflow.git"
branch: main
url_transform: "plotly"
folders:
docs:
url_path: ""
base_url: "https://panel-extensions.github.io/panel-reactflow"
reference_patterns: []

index_patterns:
- "**/*.md"
Expand Down
3 changes: 3 additions & 0 deletions src/holoviz_mcp/config/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,9 @@ class ServerConfig(BaseModel):
vector_db_path: Path = Field(
default_factory=lambda: (_holoviz_mcp_user_dir() / "vector_db" / "chroma").expanduser(), description="Path to the Chroma vector database."
)
screenshots_dir: Path = Field(
default_factory=lambda: (_holoviz_mcp_user_dir() / "screenshots").expanduser(), description="Directory for saving screenshots from panel_inspect_app tool."
)


class DisplayConfig(BaseModel):
Expand Down
8 changes: 8 additions & 0 deletions src/holoviz_mcp/panel_mcp/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,11 @@ def to_base(self) -> ComponentSummary:
package=self.package,
description=self.description,
)


class ConsoleLogEntry(BaseModel):
"""A single browser console log entry captured during app inspection."""

level: str = Field(description="Console message level: 'log', 'info', 'warning', 'error', 'debug', etc.")
message: str = Field(description="The text content of the console message.")
timestamp: Optional[str] = Field(default=None, description="ISO 8601 timestamp when the message was captured.")
111 changes: 104 additions & 7 deletions src/holoviz_mcp/panel_mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,26 @@

import asyncio
import atexit
import json
import logging
from asyncio import sleep
from datetime import datetime
from importlib.metadata import distributions
from pathlib import Path
from uuid import uuid4

from fastmcp import Context
from fastmcp import FastMCP
from fastmcp.utilities.types import Image
from mcp.types import ImageContent
from mcp.types import TextContent

from holoviz_mcp.config.loader import get_config
from holoviz_mcp.panel_mcp.data import get_components as _get_components_org
from holoviz_mcp.panel_mcp.models import ComponentDetails
from holoviz_mcp.panel_mcp.models import ComponentSummary
from holoviz_mcp.panel_mcp.models import ComponentSummarySearchResult
from holoviz_mcp.panel_mcp.models import ConsoleLogEntry
from holoviz_mcp.panel_mcp.models import ParameterInfo

# Create the FastMCP server
Expand Down Expand Up @@ -499,38 +505,129 @@ def _get_playwright_manager() -> PlaywrightManager:


@mcp.tool()

Copilot AI Feb 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tool decorator should use @mcp.tool without parentheses for consistency with other tools in the codebase (list_packages, search_components, list_components, get_component, get_component_parameters). Only use @mcp.tool() with parentheses when passing arguments like enabled=False.

Suggested change
@mcp.tool()
@mcp.tool

Copilot uses AI. Check for mistakes.
async def take_screenshot(url: str = "http://localhost:5006/", width: int = 1920, height: int = 1200, full_page: bool = False, delay: int = 2) -> ImageContent:
async def inspect_app(
url: str = "http://localhost:5006/",
width: int = 1920,
height: int = 1200,
full_page: bool = False,
delay: int = 2,
save_screenshot: bool | str = False,
screenshot: bool = True,
console_logs: bool = True,
log_level: str | None = None,
) -> list[TextContent | ImageContent]:
"""
Take a screenshot of the specified url.
Inspect a running Panel app by capturing a screenshot and/or browser console logs.

Panel apps (especially custom components) often have JavaScript errors that are
invisible to users and LLMs. This tool captures both a visual screenshot and the
browser console output in a single call, making it easy to diagnose rendering
issues, JS errors, and runtime warnings.

Arguments
----------
url : str, default="http://localhost:5006/"
The URL of the page to take a screenshot of.
The URL of the page to inspect.
width : int, default=1920
The width of the browser viewport.
height : int, default=1200
The height of the browser viewport.
full_page : bool, default=False
Whether to capture the full scrollable page.
delay : int, default=2
Seconds to wait after page load before taking the screenshot, to allow dynamic content to render.
Seconds to wait after page load before capturing, to allow dynamic content to render.
save_screenshot : bool | str, default=False
Whether and where to save the screenshot to disk:
- True: Save to default screenshots directory (~/.holoviz-mcp/screenshots/) with auto-generated filename
- False: Don't save screenshot to disk (only return to AI)
- str: Save to specified absolute path (raises ValueError if path is not absolute)
screenshot : bool, default=True
Whether to capture a screenshot of the page.
console_logs : bool, default=True
Whether to capture browser console log messages.
log_level : str | None, default=None
Filter console logs by level. If None, all levels are captured.
Common levels: 'log', 'info', 'warning', 'error', 'debug'.
"""
if not screenshot and not console_logs:
raise ValueError("At least one of 'screenshot' or 'console_logs' must be True.")

manager = _get_playwright_manager()
browser = await manager.get_browser()
page = await browser.new_page(
ignore_https_errors=True,
viewport={"width": width, "height": height},
)

# Collect console log entries
collected_logs: list[ConsoleLogEntry] = []

if console_logs:

def _on_console(msg):
collected_logs.append(
ConsoleLogEntry(
level=msg.type,
message=msg.text,
timestamp=datetime.now().isoformat(),
)
)

page.on("console", _on_console)

try:
await page.goto(url, wait_until="networkidle")
await sleep(delay=delay)
buffer = await page.screenshot(type="png", full_page=full_page)
buffer = await page.screenshot(type="png", full_page=full_page) if screenshot else None
finally:
await page.close()

image = Image(data=buffer, format="png")
return image.to_image_content()
result: list[TextContent | ImageContent] = []

# Handle screenshot saving and result
if screenshot and buffer is not None:
# Coerce string "false"/"true" to bool (MCP clients may serialize bools as strings)
save = save_screenshot
if isinstance(save, str) and save.lower() in ("true", "false"):
save = save.lower() == "true"

if save:
if isinstance(save, str):
# Custom path specified
save_path = Path(save)
if not save_path.is_absolute():
raise ValueError(f"save_screenshot path must be absolute, got: {save_screenshot}")
else:
# Default path - use screenshots_dir from config
screenshots_dir = _config.server.screenshots_dir
screenshots_dir.mkdir(parents=True, exist_ok=True)

# Generate filename with timestamp and UUID
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
unique_id = str(uuid4())[:8]
filename = f"screenshot_{timestamp}_{unique_id}.png"
save_path = screenshots_dir / filename

# Ensure parent directory exists
save_path.parent.mkdir(parents=True, exist_ok=True)

# Write the screenshot to disk
save_path.write_bytes(buffer)
logger.info(f"Screenshot saved to: {save_path}")

image = Image(data=buffer, format="png")
result.append(image.to_image_content())

# Handle console logs result
if console_logs:
filtered_logs = collected_logs
if log_level is not None:
filtered_logs = [entry for entry in collected_logs if entry.level == log_level]

logs_json = json.dumps([entry.model_dump() for entry in filtered_logs], indent=2)
result.append(TextContent(type="text", text=logs_json))

return result


if __name__ == "__main__":
Expand Down
Loading
Loading