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
Binary file added docs/assets/gifs/panel-live-claude-ai.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/gifs/panel-live-vs-code.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
52 changes: 47 additions & 5 deletions docs/how-to/mcp-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,59 @@ panel-live mcp
Used by VS Code Copilot Chat and Claude Desktop. The MCP client starts the
server as a subprocess and communicates via stdin/stdout.

### HTTP (SSE)
### Streamable HTTP (for Claude.ai and remote access)

```bash
panel-live mcp --transport streamable-http --port 5002
```

Starts a Streamable HTTP server. This is the transport Claude.ai uses for
remote MCP connectors. Expose it via ngrok or a public server to use from
Claude.ai. The server listens on `http://localhost:5002` by default.

### HTTP / SSE

```bash
panel-live mcp --transport http --port 5002
panel-live mcp --transport sse --port 5002
```

Starts an HTTP server with Server-Sent Events transport. Useful for testing
and remote setups. The server listens on `http://localhost:5002` by default.
Legacy SSE-based transports. Useful for testing and tools that don't yet
support Streamable HTTP. Note: SSE may be deprecated by MCP clients in the
future.

## Configuration examples

### Claude.ai (remote connector)

!!! warning "Extremely slow loading"

Claude.ai's MCP App webview does not provide cross-origin isolation headers
(COOP/COEP), so Pyodide falls back to a much slower initialization path.
**First load takes 2+ minutes.** We have reported it [here](https://github.com/modelcontextprotocol/ext-apps/issues/513) and at claude.ai.

Claude.ai requires a publicly accessible MCP server. We hope to host a public
panel-live MCP server in the future — for now, you can test with a local tunnel.

!!! note "ngrok is for testing only"

The [ngrok](https://ngrok.com/) example below exposes your local MCP server to the internet.
This is **not recommended for production use** — it is included here
only to demonstrate that panel-live can work with Claude.ai.

1. Start the server and expose it via ngrok:

```bash
panel-live mcp --transport streamable-http --port 5002
# In another terminal:
ngrok http 5002
```

2. In Claude.ai, go to **Settings > Connectors > Add custom connector**.
3. Paste the ngrok URL (e.g., `https://abc123.ngrok-free.app/mcp/`).

Available on free plans (1 connector) and Pro/Max/Team/Enterprise.

### VS Code Copilot Chat

Create `.vscode/mcp.json` in your project:
Expand Down Expand Up @@ -111,8 +153,8 @@ environment and the `Scripts`/`bin` directory is on your PATH.
### Slow loading in Claude.ai

Claude.ai does not provide COOP/COEP headers, so Pyodide falls back to a
slower initialization path (30–60 seconds). VS Code Copilot Chat provides
these headers and loads in 5–15 seconds. This is a known limitation of the
much slower initialization path (2+ minutes). VS Code Copilot Chat provides
these headers and loads in ~5 seconds. This is a known limitation of the
MCP Apps runtime environment.

### Network errors
Expand Down
44 changes: 41 additions & 3 deletions docs/tutorials/getting-started-mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ Restart VS Code. In Copilot Chat, ask:

Copilot will call the `show_panel_live` tool and render the app inline.

![VS Code Copilot Chat demo](../assets/gifs/panel-live-vs-code.gif)

## Claude Desktop

Add to your `claude_desktop_config.json`:
Expand Down Expand Up @@ -90,10 +92,46 @@ Alternatively, if panel-live is already installed:

Restart Claude Desktop and ask for an interactive visualization.

<!-- TODO: replace with actual GIF recording
![Claude Desktop demo](../assets/gifs/mcp-claude-desktop.gif)
-->

## Claude.ai (remote connector)

!!! warning "Extremely slow loading"

Claude.ai's MCP App webview does not provide cross-origin isolation headers
(COOP/COEP), so Pyodide falls back to a much slower initialization path.
**First load takes 2+ minutes.** We have reported it [here](https://github.com/modelcontextprotocol/ext-apps/issues/513) and at claude.ai.

Claude.ai supports remote MCP servers via Streamable HTTP. This requires
a publicly accessible MCP server. We hope to host a public panel-live MCP
server in the future — for now, you can test with a local tunnel.

!!! note "ngrok is for testing only"

The [ngrok](https://ngrok.com/) example below exposes your local MCP server to the internet.
This is **not recommended for production use** — it is included here
only to demonstrate that panel-live can work with Claude.ai.

```bash
panel-live mcp --transport streamable-http --port 5002
# In another terminal:
ngrok http 5002
```

Then in Claude.ai: **Settings > Connectors > Add custom connector**, paste
the ngrok URL (e.g., `https://abc123.ngrok-free.app/mcp/`).

See the [MCP Integration guide](../how-to/mcp-integration.md) for details.

![Claude.ai](../assets/gifs/panel-live-claude-ai.gif)

## Your first app

Ask the LLM to create any interactive Panel app. The code must call
`.servable()` on the final Panel object. For example:
Ask the LLM to create any interactive Panel app. You can use Panel code
(with `.servable()`) or regular Python where the last expression is rendered.
For example:

```python
import panel as pn
Expand All @@ -118,7 +156,7 @@ Matplotlib, Altair, NumPy, Pandas, SciPy, and most pure-Python packages.

## Known limitations

- **Claude.ai**: Loading takes 30–60 seconds without COOP/COEP headers.
- **Claude.ai**: Loading takes +2 mins without COOP/COEP headers.
VS Code Copilot Chat is faster because it provides these headers.
- **Server-side resources**: Code runs in the browser. Databases, filesystem,
and network APIs are not available.
Expand Down
2 changes: 1 addition & 1 deletion pixi.toml
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ fastmcp = ">=3.0,<4"

[feature.mcp.tasks]
mcp = { cmd = "panel-live mcp", depends-on = ["postinstall"] }
mcp-http = { cmd = "panel-live mcp --transport http --port 5002", depends-on = ["postinstall"] }
mcp-http = { cmd = "panel-live mcp --transport streamable-http --port 5002", depends-on = ["postinstall"] }
test-mcp = { cmd = "pytest tests/test_mcp.py -x", depends-on = ["postinstall"] }

[environments]
Expand Down
8 changes: 4 additions & 4 deletions src/panel_live/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
panel-live pre-render --file script.py
panel-live pre-render CODE --cache-dir .cache --setup-code "import panel as pn" --timeout 60
panel-live mcp
panel-live mcp --transport http --port 5002
panel-live mcp --transport streamable-http --port 5002
panel-live --version

The ``serve`` command starts a Panel server with the showcase example app,
Expand Down Expand Up @@ -52,9 +52,9 @@ def _build_parser() -> argparse.ArgumentParser:
mcp_parser = sub.add_parser("mcp", help="Start the MCP server")
mcp_parser.add_argument(
"--transport",
choices=["stdio", "http"],
choices=["stdio", "http", "streamable-http", "sse"],
default="stdio",
help="Transport protocol (default: stdio)",
help="Transport protocol (default: stdio). Use 'streamable-http' for Claude.ai remote connectors.",
)
mcp_parser.add_argument(
"--port",
Expand Down Expand Up @@ -184,7 +184,7 @@ def _cmd_mcp(args: argparse.Namespace) -> int:

server = create_mcp_server()
kwargs: dict = {"transport": args.transport}
if args.transport == "http":
if args.transport in ("http", "streamable-http", "sse"):
kwargs["port"] = args.port
server.run(**kwargs)
return 0
67 changes: 37 additions & 30 deletions src/panel_live/mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,46 +56,39 @@ def create_mcp_server() -> FastMCP:
version=version,
)

_cdn_domains = [
"https://unpkg.com", # @modelcontextprotocol/ext-apps SDK
"https://panel-extensions.github.io", # panel-live JS/CSS
"https://cdn.holoviz.org",
"https://cdn.jsdelivr.net",
"https://cdn.plot.ly",
"https://pyodide-cdn2.iodide.io",
"https://pypi.org",
"https://files.pythonhosted.org",
"https://cdn.bokeh.org",
"https://raw.githubusercontent.com",
]

csp = ResourceCSP(
resource_domains=[
resourceDomains=[
"'unsafe-inline'", # panel-live injects inline styles
"'unsafe-eval'", # Pyodide uses eval() for Python→JS interop
"'wasm-unsafe-eval'", # Pyodide loads WebAssembly modules
"blob:", # Pyodide creates blob URLs for workers
"data:", # Bokeh uses data: URIs for images
"https://unpkg.com", # @modelcontextprotocol/ext-apps SDK
"https://panel-extensions.github.io", # panel-live JS/CSS
"https://cdn.holoviz.org",
"https://cdn.jsdelivr.net",
"https://cdn.plot.ly",
"https://pyodide-cdn2.iodide.io",
"https://pypi.org",
"https://files.pythonhosted.org",
"https://cdn.bokeh.org",
"https://raw.githubusercontent.com",
],
connect_domains=[
"https://unpkg.com", # ext-apps SDK module fetch
"https://panel-extensions.github.io",
"https://cdn.holoviz.org",
"https://cdn.jsdelivr.net",
"https://cdn.plot.ly",
"https://pyodide-cdn2.iodide.io",
"https://pypi.org",
"https://files.pythonhosted.org",
"https://cdn.bokeh.org",
"https://raw.githubusercontent.com",
*_cdn_domains,
],
connectDomains=_cdn_domains,
)

@mcp.resource(RESOURCE_URI, app=AppConfig(csp=csp))
def show_view() -> str:
"""Return the MCP App HTML."""
return TEMPLATE_PATH.read_text(encoding="utf-8")

@mcp.tool(name="show_panel_live", app=AppConfig(resource_uri=RESOURCE_URI))
@mcp.tool(name="show_panel_live", app=AppConfig(resourceUri=RESOURCE_URI))
async def show_panel_live(code: str, ctx: Context | None = None) -> str:
"""Render interactive Python data apps in the chat using Panel + Pyodide.
"""Render interactive Python data apps in the chat using Panel + Pyodide (browser WASM).

## Use when
- User asks for an interactive visualization, dashboard, or data app
Expand All @@ -104,29 +97,43 @@ async def show_panel_live(code: str, ctx: Context | None = None) -> str:
- User wants to explore data interactively

## Don't use when
- User asks for a simple text/table answer
- User asks for a simple text/table answer with no interactivity
- Code needs server-side resources (databases, filesystem, network APIs)

## Code requirements
- Code runs in Pyodide (browser Python). Available: panel, bokeh, holoviews,
hvplot, plotly, matplotlib, altair, numpy, pandas, scipy, plus pure-Python pkgs.
- MUST call `.servable()` on the final Panel object.
- Two code styles are supported:
1. **Panel code**: build a layout and call `.servable()` on the final object.
2. **Regular Python**: the last expression is rendered automatically
(e.g., a matplotlib figure on the last line).
- Do NOT use `.show()`, `.plot()`, or Panel template classes (e.g., `FastListTemplate`).
- Use `pn.extension(...)` if loading JS extensions (e.g., `pn.extension("plotly")`).
- Heavy libs (scikit-learn, xarray, seaborn) work but add 10-30s to first load.
- Heavy libs (scikit-learn, xarray, seaborn) work but add extra time to first load.

## Example
## Examples
Panel with widgets:
```python
import panel as pn
import numpy as np
freq = pn.widgets.FloatSlider(name="Frequency", start=0.1, end=10, value=2)
pn.Column(freq, pn.bind(lambda f: f"Value: {f}", freq)).servable()
```

Plain matplotlib (no Panel needed):
```python
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0, 10, 100)
fig, ax = plt.subplots()
ax.plot(x, np.sin(x))
fig # last expression is rendered
```

Parameters
----------
code : str
Python code to run inside the panel-live Pyodide runtime.
Must call ``.servable()`` on the final Panel object.
ctx : Context | None, optional
FastMCP execution context (injected automatically).

Expand Down
Loading