diff --git a/docs/assets/gifs/panel-live-claude-ai.gif b/docs/assets/gifs/panel-live-claude-ai.gif new file mode 100644 index 0000000..480ce15 Binary files /dev/null and b/docs/assets/gifs/panel-live-claude-ai.gif differ diff --git a/docs/assets/gifs/panel-live-vs-code.gif b/docs/assets/gifs/panel-live-vs-code.gif new file mode 100644 index 0000000..132fe52 Binary files /dev/null and b/docs/assets/gifs/panel-live-vs-code.gif differ diff --git a/docs/how-to/mcp-integration.md b/docs/how-to/mcp-integration.md index 1cd1636..7cd0be0 100644 --- a/docs/how-to/mcp-integration.md +++ b/docs/how-to/mcp-integration.md @@ -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: @@ -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 diff --git a/docs/tutorials/getting-started-mcp.md b/docs/tutorials/getting-started-mcp.md index fbef9f1..706464e 100644 --- a/docs/tutorials/getting-started-mcp.md +++ b/docs/tutorials/getting-started-mcp.md @@ -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`: @@ -90,10 +92,46 @@ Alternatively, if panel-live is already installed: Restart Claude Desktop and ask for an interactive visualization. + + +## 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 @@ -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. diff --git a/pixi.toml b/pixi.toml index 5351d53..23745f6 100644 --- a/pixi.toml +++ b/pixi.toml @@ -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] diff --git a/src/panel_live/cli.py b/src/panel_live/cli.py index 402578c..810c7cc 100644 --- a/src/panel_live/cli.py +++ b/src/panel_live/cli.py @@ -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, @@ -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", @@ -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 diff --git a/src/panel_live/mcp.py b/src/panel_live/mcp.py index ff71824..01fd7ee 100644 --- a/src/panel_live/mcp.py +++ b/src/panel_live/mcp.py @@ -56,36 +56,29 @@ 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)) @@ -93,9 +86,9 @@ 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 @@ -104,17 +97,22 @@ 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 @@ -122,11 +120,20 @@ async def show_panel_live(code: str, ctx: Context | None = None) -> str: 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).