Skip to content

Commit b38aa0d

Browse files
docs for iframe / mcp
1 parent 9c8225f commit b38aa0d

2 files changed

Lines changed: 259 additions & 1 deletion

File tree

content/workspace/developers/json-specs/widgets-json-reference.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,19 @@ A `Widgets.json` table is a configuration structure with any of the named attrib
8181
- **type**
8282
_Type:_ `string`
8383
Sets the default visualization type for the widget.
84-
_Possible values:_ `"chart"`, `"table"`, `"table_ssrm"`, `"markdown"`, `"metric"`, `"note"`, `"multi_file_viewer"`, `"live_grid"`, `"newsfeed"`, `"advanced-chart"`, `"chart-highcharts"`, `"chart-vegalite"`, `"youtube"`
84+
_Possible values:_ `"chart"`, `"table"`, `"table_ssrm"`, `"markdown"`, `"metric"`, `"note"`, `"multi_file_viewer"`, `"live_grid"`, `"newsfeed"`, `"advanced-chart"`, `"chart-highcharts"`, `"chart-vegalite"`, `"youtube"`, `"iframe"`
8585
_Default:_ `"table"`
8686

87+
- **storage**
88+
_Type:_ object
89+
Persisted, widget-specific configuration. Currently used by the [Iframe](../widget-types/iframe) widget to auto-connect an MCP server when the widget mounts.
90+
Contains the following keys:
91+
92+
- **mcpUrl**
93+
_Type:_ `string`
94+
The URL of an MCP server to auto-connect when an Iframe widget mounts. Tools exposed by that server become available to Copilot immediately, with no manual URL entry.
95+
_Example:_ `"http://localhost:7769/mcp"`
96+
8797
- **raw**
8898
_Type:_ `boolean`
8999
ONLY used for Plotly configuration. If true will create a button on the widget to switch between the chart and raw data.
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
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

Comments
 (0)