Skip to content
Merged
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
70 changes: 58 additions & 12 deletions internal/mcpserver/widget/app.html
Original file line number Diff line number Diff line change
Expand Up @@ -520,7 +520,7 @@
}
if (view.sourceURL) {
const link = el("button", "linkbtn", domainOf(view.sourceURL) || "source");
link.onclick = () => bridge.request("ui/open-link", { url: view.sourceURL });
link.onclick = () => openLink(view.sourceURL);
actions.appendChild(link);
}
headings.appendChild(actions);
Expand Down Expand Up @@ -578,6 +578,12 @@
* Tool calls initiated from the widget
* ------------------------------------------------------------------ */
async function callTool(name, args) {
const oa = openAIHost();
if (oa && typeof oa.callTool === "function") {
// ChatGPT Apps runtime: returns { structuredContent, content }.
const next = await oa.callTool(name, args);
return (next && next.structuredContent) || {};
}
const result = await bridge.request("tools/call", { name, arguments: args });
if (result && result.isError) {
const text = (result.content || []).map((c) => c.text).filter(Boolean).join(" ");
Expand All @@ -586,6 +592,17 @@
return (result && result.structuredContent) || {};
}

// openLink sends the user to an external URL via whichever host bridge is active.
function openLink(url) {
if (!url) return;
const oa = openAIHost();
if (oa && typeof oa.openExternal === "function") {
try { oa.openExternal({ href: url }); return; } catch (e) { /* fall through */ }
try { oa.openExternal(url); return; } catch (e) { /* fall through */ }
}
bridge.request("ui/open-link", { url }).catch(() => {});
}

async function openPreview(url) {
if (!url) return;
skeleton();
Expand Down Expand Up @@ -625,6 +642,26 @@
}
}

// openAIHost returns ChatGPT's Apps runtime object when the widget is embedded in
// ChatGPT, else null. ChatGPT delivers the tool's structured output via
// window.openai (globals + the "openai:set_globals" event), NOT the MCP Apps
// postMessage bridge — so without this the widget would sit on its loading note
// in ChatGPT forever.
function openAIHost() {
return (typeof window !== "undefined" && window.openai) ? window.openai : null;
}

// applyOpenAIGlobals renders from the ChatGPT runtime globals: theme + the tool's
// structured output, which is exactly the shape render() expects.
function applyOpenAIGlobals(globals) {
if (!globals) return;
if (globals.theme === "dark" || globals.theme === "light") {
document.documentElement.setAttribute("data-theme", globals.theme);
}
if (globals.toolOutput) render(globals.toolOutput);
}

// MCP Apps postMessage host (Claude and other ext-apps hosts).
bridge.on("ui/notifications/tool-result", (params) => {
if (params && params.structuredContent) render(params.structuredContent);
});
Expand All @@ -633,26 +670,35 @@
applyHostContext(params && params.hostContext);
});

// ChatGPT Apps runtime host: tool output + theme arrive via window.openai globals.
window.addEventListener("openai:set_globals", (event) => {
applyOpenAIGlobals(event && event.detail && event.detail.globals);
});

const resizeObserver = new ResizeObserver(() => {
bridge.notify("ui/notifications/size-changed", {
width: document.documentElement.scrollWidth,
height: document.documentElement.scrollHeight,
});
});

(async function main() {
try {
const init = await bridge.request("ui/initialize", {
protocolVersion: "2026-01-26",
clientInfo: { name: "saltybytes-widget", version: "1.0.0" },
capabilities: {},
});
(function main() {
// ChatGPT: window.openai is present at mount — render immediately from whatever
// is already set, then let "openai:set_globals" drive updates.
const oa = openAIHost();
if (oa) applyOpenAIGlobals(oa);
// MCP Apps host (Claude, etc.): run the postMessage handshake, but NON-BLOCKING
// so a host that never answers ui/initialize (ChatGPT) can't leave the widget
// stuck on its loading note.
bridge.request("ui/initialize", {
protocolVersion: "2026-01-26",
clientInfo: { name: "saltybytes-widget", version: "1.0.0" },
capabilities: {},
}).then((init) => {
applyHostContext(init && init.hostContext);
bridge.notify("ui/notifications/initialized");
resizeObserver.observe(document.body);
} catch {
/* Host without MCP Apps support: leave the static note visible. */
}
}).catch(() => { /* not an MCP Apps postMessage host */ });
resizeObserver.observe(document.body);
})();
</script>
</body>
Expand Down
Loading