Skip to content

Commit 92bdf48

Browse files
browser tools
1 parent 89f0f48 commit 92bdf48

7 files changed

Lines changed: 257 additions & 170 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@AGENTS.md

docs/explanation/tools.md

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -75,28 +75,36 @@ Tools for discovering and working with Panel components.
7575

7676
**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)
7777

78-
### panel_take_screenshot
78+
### panel_inspect_app
7979

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

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

93-
**Use Case**: Understand how the app looks and save screenshots for later review
96+
**Use Case**: Understand how the app looks, debug JavaScript errors, and inspect browser console output — all in a single call.
9497

95-
**Returns**: ImageContent (screenshot image returned to AI).
98+
**Returns**: A list containing:
99+
- `ImageContent` (screenshot) when `screenshot=True`
100+
- `TextContent` (console logs as JSON) when `console_logs=True`
96101

97-
**Note**: 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`).
102+
**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`).
98103

99-
**Example Query**: *"Take a screenshot of http://127.0.0.1:8000/"**
104+
**Example Queries**:
105+
- *"Take a screenshot of http://127.0.0.1:8000/"*
106+
- *"Check my Panel app for JavaScript errors"*
107+
- *"Show me the console errors from my app"*
100108

101109
## HoloViews Tool
102110

src/holoviz_mcp/config/config.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,24 @@ docs:
167167
folders:
168168
docs/bokeh/source/docs:
169169
url_path: "/"
170+
panel-live:
171+
url: "https://github.com/panel-extensions/panel-live.git"
172+
branch: main
173+
url_transform: "plotly"
174+
folders:
175+
docs:
176+
url_path: ""
177+
base_url: "https://panel-extensions.github.io/panel-live"
178+
reference_patterns: []
179+
panel-reactflow:
180+
url: "https://github.com/panel-extensions/panel-reactflow.git"
181+
branch: main
182+
url_transform: "plotly"
183+
folders:
184+
docs:
185+
url_path: ""
186+
base_url: "https://panel-extensions.github.io/panel-reactflow"
187+
reference_patterns: []
170188

171189
index_patterns:
172190
- "**/*.md"

src/holoviz_mcp/config/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ class ServerConfig(BaseModel):
150150
default_factory=lambda: (_holoviz_mcp_user_dir() / "vector_db" / "chroma").expanduser(), description="Path to the Chroma vector database."
151151
)
152152
screenshots_dir: Path = Field(
153-
default_factory=lambda: (_holoviz_mcp_user_dir() / "screenshots").expanduser(), description="Directory for saving screenshots from panel_take_screenshot tool."
153+
default_factory=lambda: (_holoviz_mcp_user_dir() / "screenshots").expanduser(), description="Directory for saving screenshots from panel_inspect_app tool."
154154
)
155155

156156

src/holoviz_mcp/panel_mcp/models.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,11 @@ def to_base(self) -> ComponentSummary:
122122
package=self.package,
123123
description=self.description,
124124
)
125+
126+
127+
class ConsoleLogEntry(BaseModel):
128+
"""A single browser console log entry captured during app inspection."""
129+
130+
level: str = Field(description="Console message level: 'log', 'info', 'warning', 'error', 'debug', etc.")
131+
message: str = Field(description="The text content of the console message.")
132+
timestamp: Optional[str] = Field(default=None, description="ISO 8601 timestamp when the message was captured.")

src/holoviz_mcp/panel_mcp/server.py

Lines changed: 90 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import asyncio
1212
import atexit
13+
import json
1314
import logging
1415
from asyncio import sleep
1516
from datetime import datetime
@@ -21,12 +22,14 @@
2122
from fastmcp import FastMCP
2223
from fastmcp.utilities.types import Image
2324
from mcp.types import ImageContent
25+
from mcp.types import TextContent
2426

2527
from holoviz_mcp.config.loader import get_config
2628
from holoviz_mcp.panel_mcp.data import get_components as _get_components_org
2729
from holoviz_mcp.panel_mcp.models import ComponentDetails
2830
from holoviz_mcp.panel_mcp.models import ComponentSummary
2931
from holoviz_mcp.panel_mcp.models import ComponentSummarySearchResult
32+
from holoviz_mcp.panel_mcp.models import ConsoleLogEntry
3033
from holoviz_mcp.panel_mcp.models import ParameterInfo
3134

3235
# Create the FastMCP server
@@ -502,79 +505,129 @@ def _get_playwright_manager() -> PlaywrightManager:
502505

503506

504507
@mcp.tool()
505-
async def take_screenshot(
508+
async def inspect_app(
506509
url: str = "http://localhost:5006/",
507510
width: int = 1920,
508511
height: int = 1200,
509512
full_page: bool = False,
510513
delay: int = 2,
511514
save_screenshot: bool | str = False,
512-
) -> ImageContent:
515+
screenshot: bool = True,
516+
console_logs: bool = True,
517+
log_level: str | None = None,
518+
) -> list[TextContent | ImageContent]:
513519
"""
514-
Take a screenshot of the specified url.
520+
Inspect a running Panel app by capturing a screenshot and/or browser console logs.
521+
522+
Panel apps (especially custom components) often have JavaScript errors that are
523+
invisible to users and LLMs. This tool captures both a visual screenshot and the
524+
browser console output in a single call, making it easy to diagnose rendering
525+
issues, JS errors, and runtime warnings.
515526
516527
Arguments
517528
----------
518529
url : str, default="http://localhost:5006/"
519-
The URL of the page to take a screenshot of.
530+
The URL of the page to inspect.
520531
width : int, default=1920
521532
The width of the browser viewport.
522533
height : int, default=1200
523534
The height of the browser viewport.
524535
full_page : bool, default=False
525536
Whether to capture the full scrollable page.
526537
delay : int, default=2
527-
Seconds to wait after page load before taking the screenshot, to allow dynamic content to render.
538+
Seconds to wait after page load before capturing, to allow dynamic content to render.
528539
save_screenshot : bool | str, default=False
529540
Whether and where to save the screenshot to disk:
530541
- True: Save to default screenshots directory (~/.holoviz-mcp/screenshots/) with auto-generated filename
531542
- False: Don't save screenshot to disk (only return to AI)
532543
- str: Save to specified absolute path (raises ValueError if path is not absolute)
544+
screenshot : bool, default=True
545+
Whether to capture a screenshot of the page.
546+
console_logs : bool, default=True
547+
Whether to capture browser console log messages.
548+
log_level : str | None, default=None
549+
Filter console logs by level. If None, all levels are captured.
550+
Common levels: 'log', 'info', 'warning', 'error', 'debug'.
533551
"""
552+
if not screenshot and not console_logs:
553+
raise ValueError("At least one of 'screenshot' or 'console_logs' must be True.")
554+
534555
manager = _get_playwright_manager()
535556
browser = await manager.get_browser()
536557
page = await browser.new_page(
537558
ignore_https_errors=True,
538559
viewport={"width": width, "height": height},
539560
)
561+
562+
# Collect console log entries
563+
collected_logs: list[ConsoleLogEntry] = []
564+
565+
if console_logs:
566+
567+
def _on_console(msg):
568+
collected_logs.append(
569+
ConsoleLogEntry(
570+
level=msg.type,
571+
message=msg.text,
572+
timestamp=datetime.now().isoformat(),
573+
)
574+
)
575+
576+
page.on("console", _on_console)
577+
540578
try:
541579
await page.goto(url, wait_until="networkidle")
542580
await sleep(delay=delay)
543-
buffer = await page.screenshot(type="png", full_page=full_page)
581+
buffer = await page.screenshot(type="png", full_page=full_page) if screenshot else None
544582
finally:
545583
await page.close()
546584

547-
# Coerce string "false"/"true" to bool (MCP clients may serialize bools as strings)
548-
if isinstance(save_screenshot, str) and save_screenshot.lower() in ("true", "false"):
549-
save_screenshot = save_screenshot.lower() == "true"
550-
551-
# Handle screenshot saving
552-
if save_screenshot:
553-
if isinstance(save_screenshot, str):
554-
# Custom path specified
555-
save_path = Path(save_screenshot)
556-
if not save_path.is_absolute():
557-
raise ValueError(f"save_screenshot path must be absolute, got: {save_screenshot}")
558-
else:
559-
# Default path - use screenshots_dir from config
560-
screenshots_dir = _config.server.screenshots_dir
561-
screenshots_dir.mkdir(parents=True, exist_ok=True)
562-
563-
# Generate filename with timestamp and UUID
564-
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
565-
unique_id = str(uuid4())[:8]
566-
filename = f"screenshot_{timestamp}_{unique_id}.png"
567-
save_path = screenshots_dir / filename
568-
569-
# Ensure parent directory exists
570-
save_path.parent.mkdir(parents=True, exist_ok=True)
571-
572-
# Write the screenshot to disk
573-
save_path.write_bytes(buffer)
574-
logger.info(f"Screenshot saved to: {save_path}")
575-
576-
image = Image(data=buffer, format="png")
577-
return image.to_image_content()
585+
result: list[TextContent | ImageContent] = []
586+
587+
# Handle screenshot saving and result
588+
if screenshot and buffer is not None:
589+
# Coerce string "false"/"true" to bool (MCP clients may serialize bools as strings)
590+
save = save_screenshot
591+
if isinstance(save, str) and save.lower() in ("true", "false"):
592+
save = save.lower() == "true"
593+
594+
if save:
595+
if isinstance(save, str):
596+
# Custom path specified
597+
save_path = Path(save)
598+
if not save_path.is_absolute():
599+
raise ValueError(f"save_screenshot path must be absolute, got: {save_screenshot}")
600+
else:
601+
# Default path - use screenshots_dir from config
602+
screenshots_dir = _config.server.screenshots_dir
603+
screenshots_dir.mkdir(parents=True, exist_ok=True)
604+
605+
# Generate filename with timestamp and UUID
606+
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
607+
unique_id = str(uuid4())[:8]
608+
filename = f"screenshot_{timestamp}_{unique_id}.png"
609+
save_path = screenshots_dir / filename
610+
611+
# Ensure parent directory exists
612+
save_path.parent.mkdir(parents=True, exist_ok=True)
613+
614+
# Write the screenshot to disk
615+
save_path.write_bytes(buffer)
616+
logger.info(f"Screenshot saved to: {save_path}")
617+
618+
image = Image(data=buffer, format="png")
619+
result.append(image.to_image_content())
620+
621+
# Handle console logs result
622+
if console_logs:
623+
filtered_logs = collected_logs
624+
if log_level is not None:
625+
filtered_logs = [entry for entry in collected_logs if entry.level == log_level]
626+
627+
logs_json = json.dumps([entry.model_dump() for entry in filtered_logs], indent=2)
628+
result.append(TextContent(type="text", text=logs_json))
629+
630+
return result
578631

579632

580633
if __name__ == "__main__":

0 commit comments

Comments
 (0)