|
| 1 | +--- |
| 2 | +title: Iframe |
| 3 | +sidebar_position: 16 |
| 4 | +description: Embed an external web app (such as a Streamlit dashboard) as an iframe widget in OpenBB Workspace, with optional sub-widget export via the Iframe Widget Protocol and auto-connected MCP tools. |
| 5 | +keywords: |
| 6 | +- iframe widget |
| 7 | +- iframe widget protocol |
| 8 | +- streamlit |
| 9 | +- embed external app |
| 10 | +- postMessage |
| 11 | +- mcpUrl |
| 12 | +- destructiveHint |
| 13 | +- sub-widgets |
| 14 | +- OpenBB Workspace |
| 15 | +--- |
| 16 | + |
| 17 | +import HeadTitle from '@site/src/components/General/HeadTitle.tsx'; |
| 18 | + |
| 19 | +<HeadTitle title="Iframe | OpenBB Workspace Docs" /> |
| 20 | + |
| 21 | +The Iframe widget embeds an external web application — for example a [Streamlit](https://streamlit.io/) dashboard, an internal tool, or any URL that permits embedding — directly inside an OpenBB Workspace dashboard. |
| 22 | + |
| 23 | +On its own, an iframe just renders the URL. But by implementing the **Iframe Widget Protocol**, the embedded app can do much more: |
| 24 | + |
| 25 | +- **Export sub-widgets** — declare tables and markdown sections inside the iframe that Workspace can pull out as standalone dashboard widgets. |
| 26 | +- **Receive toolbar parameters** — react to Workspace parameters (dropdowns, dates, toggles) without a backend round-trip. |
| 27 | +- **Auto-connect an MCP server** — wire up Copilot tools the moment the widget mounts. |
| 28 | +- **Auto-refresh on mutating tool calls** — remount the iframe after a destructive MCP tool runs so the UI reflects new state. |
| 29 | + |
| 30 | +A complete working example (Streamlit app + MCP server + `widgets.json` + `apps.json`) lives in the [backends-for-openbb repository](https://github.com/OpenBB-finance/backends-for-openbb/tree/main/widget-examples/streamlit). |
| 31 | + |
| 32 | +<img className="pro-border-gradient" width="800" alt="Iframe widget embedding a Streamlit app in OpenBB Workspace" src="https://openbb-assets.s3.us-east-1.amazonaws.com/docs/pro/iframe.png" /> |
| 33 | + |
| 34 | +## Widget definition |
| 35 | + |
| 36 | +An iframe widget is declared in `widgets.json` with `type: "iframe"`. The `endpoint` is the initial `src` of the iframe (the user can still edit it via the widget's URL dialog). |
| 37 | + |
| 38 | +```json |
| 39 | +{ |
| 40 | + "portfolio_iframe": { |
| 41 | + "name": "Portfolio Dashboard (Streamlit)", |
| 42 | + "description": "Embedded Streamlit portfolio app with MCP tools", |
| 43 | + "category": "Portfolio", |
| 44 | + "type": "iframe", |
| 45 | + "endpoint": "http://localhost:8501", |
| 46 | + "storage": { |
| 47 | + "mcpUrl": "http://localhost:7769/mcp" |
| 48 | + }, |
| 49 | + "gridData": { "w": 40, "h": 16 }, |
| 50 | + "source": "Streamlit Demo" |
| 51 | + } |
| 52 | +} |
| 53 | +``` |
| 54 | + |
| 55 | +Two fields are specific to the iframe type: |
| 56 | + |
| 57 | +- **`endpoint`** — for iframe widgets this is a full URL (the iframe `src`), not a backend path. It is the initial address loaded into the iframe. |
| 58 | +- **`storage.mcpUrl`** — *(optional)* an MCP server to auto-connect when the widget mounts. The tools exposed by that server become available to Copilot immediately, with no manual URL entry. If omitted, the user can still attach an MCP server manually via the **MCP** icon in the widget navbar. |
| 59 | + |
| 60 | +:::note |
| 61 | +Some websites restrict being embedded in an iframe (via `X-Frame-Options` or `Content-Security-Policy`). OpenBB checks for this and only displays pages that permit embedding. Apps you run yourself (such as a local Streamlit app) typically allow it. |
| 62 | +::: |
| 63 | + |
| 64 | +## The Iframe Widget Protocol |
| 65 | + |
| 66 | +The protocol is a small set of `postMessage` events exchanged between the embedded app and Workspace. The embedded app announces what it offers, then responds to data requests and parameter updates. |
| 67 | + |
| 68 | +### Messages from the iframe → Workspace |
| 69 | + |
| 70 | +- **`openbb-connect`** — sent once on load to announce the available sub-widgets and parameters. |
| 71 | + |
| 72 | + ```js |
| 73 | + target.postMessage({ |
| 74 | + type: "openbb-connect", |
| 75 | + widgets: manifests, // sub-widget manifests |
| 76 | + params: paramDefs // toolbar parameter definitions |
| 77 | + }, "*"); |
| 78 | + ``` |
| 79 | + |
| 80 | +- **`openbb-data`** — sent in response to an `openbb-request`, carrying the data for a sub-widget. |
| 81 | + |
| 82 | + ```js |
| 83 | + target.postMessage({ |
| 84 | + type: "openbb-data", |
| 85 | + widgetId: "portfolio-holdings", |
| 86 | + dataType: "table", // "table" or "markdown" |
| 87 | + data: [ /* records */ ] // array of rows, or a markdown string |
| 88 | + }, "*"); |
| 89 | + ``` |
| 90 | + |
| 91 | +### Messages from Workspace → iframe |
| 92 | + |
| 93 | +- **`openbb-request`** — Workspace asks the iframe for a sub-widget's data. A `widgetId` of `null` means "send everything." |
| 94 | +- **`openbb-params-update`** — Workspace pushes new toolbar parameter values to the iframe (e.g. the user changed a dropdown). The app reads these and re-renders. |
| 95 | + |
| 96 | +### Sub-widget manifests |
| 97 | + |
| 98 | +Each entry in the `widgets` array of `openbb-connect` describes one exportable sub-widget: |
| 99 | + |
| 100 | +```js |
| 101 | +const WIDGET_MANIFESTS = [ |
| 102 | + { |
| 103 | + "widgetId": "portfolio-holdings", |
| 104 | + "name": "Portfolio Holdings", |
| 105 | + "description": "Current portfolio positions with PnL", |
| 106 | + "category": "Portfolio", |
| 107 | + "dataType": "table" // "table" or "markdown" |
| 108 | + }, |
| 109 | + { |
| 110 | + "widgetId": "market-summary", |
| 111 | + "name": "Market Summary", |
| 112 | + "description": "Weekly market analysis and outlook", |
| 113 | + "category": "Research", |
| 114 | + "dataType": "markdown" |
| 115 | + } |
| 116 | +]; |
| 117 | +``` |
| 118 | + |
| 119 | +When the app announces sub-widgets, a grid icon with a count badge appears in the iframe widget's navbar. Clicking it lets the user add any sub-widget to the dashboard as a standalone widget. |
| 120 | + |
| 121 | +### Parameter definitions |
| 122 | + |
| 123 | +The `params` array of `openbb-connect` declares toolbar parameters Workspace should render for the widget. These mirror the standard [widget parameter](../widget-parameters/text-input) types: |
| 124 | + |
| 125 | +```js |
| 126 | +const PARAM_DEFS = [ |
| 127 | + { |
| 128 | + "paramName": "sector", |
| 129 | + "label": "Sector", |
| 130 | + "type": "text", |
| 131 | + "description": "Filter holdings by sector", |
| 132 | + "value": "All", |
| 133 | + "options": [{ "label": "All", "value": "All" }, /* ... */] |
| 134 | + }, |
| 135 | + { |
| 136 | + "paramName": "min_shares", |
| 137 | + "label": "Min Shares", |
| 138 | + "type": "number", |
| 139 | + "value": "0", |
| 140 | + "min": 0, "max": 1000, "step": 10 |
| 141 | + }, |
| 142 | + { |
| 143 | + "paramName": "show_pnl_pct", |
| 144 | + "label": "Show PnL %", |
| 145 | + "type": "boolean", |
| 146 | + "value": "true" |
| 147 | + }, |
| 148 | + { |
| 149 | + "paramName": "as_of_date", |
| 150 | + "label": "As Of Date", |
| 151 | + "type": "date", |
| 152 | + "value": "2026-04-07" |
| 153 | + } |
| 154 | +]; |
| 155 | +``` |
| 156 | + |
| 157 | +When a user changes a parameter, Workspace sends an `openbb-params-update` message. A common pattern (used by the Streamlit example) is to mirror the values into the iframe's query string so the embedded app can read them on rerun. |
| 158 | + |
| 159 | +### Minimal bridge |
| 160 | + |
| 161 | +The bridge below is the complete client side of the protocol — announce on load, then answer requests. Inject it into your app's page (in Streamlit, via `st.components.v1.html(...)`). |
| 162 | + |
| 163 | +```js |
| 164 | +(function () { |
| 165 | + const manifests = WIDGET_MANIFESTS; |
| 166 | + const paramDefs = PARAM_DEFS; |
| 167 | + const widgetData = WIDGET_DATA; // { widgetId: { type: "openbb-data", widgetId, dataType, data } } |
| 168 | + |
| 169 | + const target = window.top || window.parent; |
| 170 | + |
| 171 | + // Announce available sub-widgets + params |
| 172 | + if (target !== window) { |
| 173 | + target.postMessage({ type: "openbb-connect", widgets: manifests, params: paramDefs }, "*"); |
| 174 | + } |
| 175 | + |
| 176 | + window.addEventListener("message", function (event) { |
| 177 | + if (!event.data || !event.data.type) return; |
| 178 | + |
| 179 | + if (event.data.type === "openbb-request") { |
| 180 | + const widgetId = event.data.widgetId; |
| 181 | + if (widgetId === null) { |
| 182 | + Object.values(widgetData).forEach((d) => target.postMessage(d, "*")); |
| 183 | + } else if (widgetData[widgetId]) { |
| 184 | + target.postMessage(widgetData[widgetId], "*"); |
| 185 | + } |
| 186 | + } |
| 187 | + }); |
| 188 | +})(); |
| 189 | +``` |
| 190 | + |
| 191 | +## Auto-connecting an MCP server |
| 192 | + |
| 193 | +Set `storage.mcpUrl` in the widget definition to attach an MCP server automatically when the iframe mounts. Its tools become available to Copilot with no manual setup: |
| 194 | + |
| 195 | +```json |
| 196 | +"portfolio_iframe": { |
| 197 | + "type": "iframe", |
| 198 | + "endpoint": "http://localhost:8501", |
| 199 | + "storage": { |
| 200 | + "mcpUrl": "http://localhost:7769/mcp" |
| 201 | + } |
| 202 | +} |
| 203 | +``` |
| 204 | + |
| 205 | +This is most useful when shipping a pre-built app via `apps.json`: the dashboard loads with the iframe URL pre-set and the MCP server already connected, giving a true one-click experience. |
| 206 | + |
| 207 | +## Auto-refresh on mutating MCP tools |
| 208 | + |
| 209 | +When an MCP tool is connected to an iframe widget, Workspace can automatically remount the iframe after a tool call so the UI reflects the new state. **The default is no refresh** — only tools that explicitly opt in trigger a remount. This avoids unwanted reloads during read-only operations. |
| 210 | + |
| 211 | +Opt in by marking the tool with `destructiveHint=True` in its annotations: |
| 212 | + |
| 213 | +```python |
| 214 | +from mcp.server.fastmcp import FastMCP |
| 215 | +from mcp.types import ToolAnnotations |
| 216 | + |
| 217 | +mcp = FastMCP("Portfolio Dashboard", host="0.0.0.0", port=7769) |
| 218 | + |
| 219 | +# Read-only tool — no annotation needed (no refresh) |
| 220 | +@mcp.tool() |
| 221 | +def get_portfolio_holdings(sector: str = "All") -> str: |
| 222 | + ... |
| 223 | + |
| 224 | +# Mutating tool — opts in to iframe refresh |
| 225 | +@mcp.tool(annotations=ToolAnnotations(destructiveHint=True)) |
| 226 | +def rebalance_portfolio() -> str: |
| 227 | + ... |
| 228 | +``` |
| 229 | + |
| 230 | +After Copilot calls `rebalance_portfolio`, the iframe remounts and the embedded app reloads with the new state. |
| 231 | + |
| 232 | +## Serving everything from one backend |
| 233 | + |
| 234 | +A convenient pattern is to serve the MCP transport and the Workspace backend routes from a single process, so adding one URL in Workspace wires up tools, widget definitions, and app layout together. The Streamlit example mounts the Workspace routes alongside a FastMCP `streamable_http_app`: |
| 235 | + |
| 236 | +| Route | Purpose | |
| 237 | +| ----- | ------- | |
| 238 | +| `/mcp` | MCP tools (FastMCP) | |
| 239 | +| `/widgets.json` | Widget definitions for OpenBB Workspace | |
| 240 | +| `/apps.json` | Pre-built app layout | |
| 241 | +| `/portfolio_note` | Markdown content for a companion note widget | |
| 242 | + |
| 243 | +## Additional Resources |
| 244 | + |
| 245 | +- Full working example: [Streamlit Iframe Widget Protocol demo](https://github.com/OpenBB-finance/backends-for-openbb/tree/main/widget-examples/streamlit) |
| 246 | +- [widgets.json Reference](../json-specs/widgets-json-reference) |
| 247 | +- [apps.json Reference](../json-specs/apps-json-reference) |
| 248 | +- [MCP Tools](../ai-features/mcp-tools) |
0 commit comments