-
Notifications
You must be signed in to change notification settings - Fork 279
feat: add skybridge integration #140
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a4136af
dbe9324
8d736a7
30a7ddb
cb2d62a
385b662
7fd79b9
514aca1
929f121
6adfb39
9e3e93f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -49,6 +49,7 @@ web_modules/ | |
|
|
||
| # PNPM | ||
| .pnpm-store/ | ||
| .pnpm-home/ | ||
|
|
||
| # TypeScript cache | ||
| *.tsbuildinfo | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| packages: | ||
| # all packages in subdirs of sdks/typescript/ | ||
| - 'sdks/typescript/*' | ||
| - 'examples/*' | ||
| - 'docs' | ||
| - sdks/typescript/* | ||
| - examples/* | ||
| - docs | ||
| onlyBuiltDependencies: | ||
| - esbuild |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| import { build } from 'esbuild'; | ||
| import { writeFileSync } from 'fs'; | ||
| import { fileURLToPath } from 'url'; | ||
| import { dirname, join } from 'path'; | ||
|
|
||
| const __filename = fileURLToPath(import.meta.url); | ||
| const __dirname = dirname(__filename); | ||
|
|
||
| try { | ||
| const result = await build({ | ||
| entryPoints: [join(__dirname, '../../client/src/adapters/appssdk/open-ai-runtime-script.ts')], | ||
| bundle: true, | ||
| write: false, | ||
| format: 'iife', | ||
| platform: 'browser', | ||
| target: 'es2020', | ||
| minify: false, | ||
| }); | ||
|
|
||
| const bundledCode = result.outputFiles[0].text; | ||
| const serializedCode = JSON.stringify(bundledCode); | ||
|
|
||
| const outputContent = `// This file is auto-generated by scripts/bundle-open-ai-script.js | ||
| // Do not edit directly - modify client/src/adapters/appssdk/open-ai-runtime-script.ts instead | ||
|
|
||
| export const API_RUNTIME_SCRIPT = ${serializedCode}; | ||
| `; | ||
|
|
||
| const outputTsPath = join(__dirname, '../../client/src/adapters/appssdk/open-ai-runtime-script.bundled.ts'); | ||
| writeFileSync(outputTsPath, outputContent); | ||
| console.log('✅ Successfully bundled API runtime script'); | ||
| } catch (error) { | ||
| console.error('❌ Failed to bundle API runtime script:', error); | ||
| process.exit(1); | ||
| } |
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @idosal you recently added the double proxy. what client was that for? I don't think this is a breaking change since it is the code clients need to add to their CDN anyway but curious if you tested the previous implementation
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not used in production yet, so we can change it. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,8 +2,6 @@ | |
| <html> | ||
| <head> | ||
| <meta charset="utf-8" /> | ||
| <!-- Permissive CSP so nested content is not constrained by host CSP --> | ||
| <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'wasm-unsafe-eval' 'unsafe-inline' 'unsafe-eval' blob: https://cdn.tailwindcss.com https://cdn.jsdelivr.net https://unpkg.com; style-src * 'unsafe-inline'; connect-src *; frame-src 'none'; base-uri 'self'; upgrade-insecure-requests;" /> | ||
| <title>MCP-UI Proxy</title> | ||
| <style> | ||
| html, | ||
|
|
@@ -47,27 +45,38 @@ | |
| if (contentType === 'rawhtml') { | ||
| // Double-iframe raw HTML mode (HTML sent via postMessage) | ||
| const inner = document.createElement('iframe'); | ||
|
|
||
| inner.id = "root"; | ||
| let pendingHtml = null; | ||
| const renderHtmlInIframe = (markup) => { | ||
| const doc = inner.contentDocument || inner.contentWindow?.document; | ||
| if (!doc) return false; | ||
| try { | ||
| doc.open(); | ||
| doc.write(markup); | ||
| doc.close(); | ||
| return true; | ||
| } catch (error) { | ||
| return false; | ||
| } | ||
| }; | ||
| inner.addEventListener('load', () => { | ||
| if (pendingHtml !== null && renderHtmlInIframe(pendingHtml)) { | ||
| pendingHtml = null; | ||
| } | ||
| }); | ||
| inner.style = 'width:100%; height:100%; border:none;'; | ||
| // sandbox will be set from postMessage payload; default minimal before html arrives | ||
| inner.setAttribute('sandbox', 'allow-scripts'); | ||
| inner.src = 'about:blank'; | ||
| document.body.appendChild(inner); | ||
|
|
||
| // Wait for HTML content from parent | ||
| window.addEventListener('message', (event) => { | ||
| if (event.source === window.parent) { | ||
| if (event.data && event.data.type === 'ui-html-content') { | ||
| const payload = event.data.payload || {}; | ||
| const html = payload.html; | ||
| const sandbox = payload.sandbox; | ||
| if (typeof sandbox === 'string') { | ||
| inner.setAttribute('sandbox', sandbox); | ||
| } | ||
| if (typeof html === 'string') { | ||
| inner.srcdoc = html; | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the key was to use srcdoc changes the root url and breaks client side navigation.
and document.write() do the trick
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great catch! |
||
| } | ||
| } else { | ||
| if (inner && inner.contentWindow) { | ||
| inner.contentWindow.postMessage(event.data, '*'); | ||
| if (event.source === window.parent && event.data && event.data.type === 'ui-html-content') { | ||
| const payload = event.data.payload || {}; | ||
| const html = payload.html; | ||
| if (typeof html === 'string') { | ||
| if (!renderHtmlInIframe(html)) { | ||
| pendingHtml = html; | ||
| } | ||
| } | ||
| } else if (event.source === inner.contentWindow) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| // This file is auto-generated by scripts/bundle-open-ai-script.js | ||
| // Do not edit directly - modify client/src/adapters/appssdk/open-ai-runtime-script.ts instead | ||
|
|
||
| export const API_RUNTIME_SCRIPT = "\"use strict\";\n(() => {\n // src/adapters/appssdk/open-ai-runtime-script.ts\n var DEFAULT_SAFE_AREA = {\n insets: {\n top: 0,\n bottom: 0,\n left: 0,\n right: 0\n }\n };\n var DEFAULT_CAPABILITIES = {\n hover: true,\n touch: false\n };\n var TOOL_CALL_TIMEOUT_MS = 3e4;\n (function initializeOpenAIWidget() {\n const globalWindow = window;\n const config = globalWindow.__MCP_WIDGET_CONFIG__;\n if (!config || typeof config !== \"object\" || config.widgetStateKey == null) {\n console.warn(\"[OpenAI Widget] Missing widget configuration. Skipping initialization.\");\n return;\n }\n const {\n widgetStateKey,\n toolInput = null,\n toolOutput = null,\n toolResponseMetadata = null,\n toolName = null,\n theme,\n locale,\n userAgent = null,\n model = null,\n displayMode,\n maxHeight,\n safeArea,\n capabilities\n } = config;\n const resolvedDisplayMode = typeof displayMode === \"string\" ? displayMode : \"inline\";\n const resolvedMaxHeight = typeof maxHeight === \"number\" ? maxHeight : 600;\n const resolvedTheme = typeof theme === \"string\" ? theme : \"light\";\n const resolvedLocale = typeof locale === \"string\" ? locale : \"en-US\";\n const resolvedSafeArea = safeArea && typeof safeArea === \"object\" ? safeArea : DEFAULT_SAFE_AREA;\n const mergedCapabilities = {\n ...DEFAULT_CAPABILITIES,\n ...typeof capabilities === \"object\" && capabilities !== null ? capabilities : {}\n };\n const openaiAPI = {\n toolInput,\n toolOutput,\n toolResponseMetadata,\n toolName,\n displayMode: resolvedDisplayMode,\n maxHeight: resolvedMaxHeight,\n theme: resolvedTheme,\n locale: resolvedLocale,\n safeArea: resolvedSafeArea,\n userAgent,\n capabilities: mergedCapabilities,\n model,\n widgetState: null,\n async setWidgetState(state) {\n this.widgetState = state;\n try {\n localStorage.setItem(widgetStateKey, JSON.stringify(state));\n } catch (err) {\n console.error(\"[OpenAI Widget] Failed to save widget state:\", err);\n }\n window.parent.postMessage(\n {\n type: \"openai:setWidgetState\",\n state\n },\n \"*\"\n );\n },\n async callTool(tool, params = {}) {\n return new Promise((resolve, reject) => {\n const requestId = `tool_${Date.now()}_${Math.random()}`;\n const handler = (event) => {\n const data = event.data;\n if (data?.type === \"openai:callTool:response\" && data?.requestId === requestId) {\n window.removeEventListener(\"message\", handler);\n if (data.error) {\n reject(new Error(data.error));\n } else {\n resolve(data.result);\n }\n }\n };\n window.addEventListener(\"message\", handler);\n window.parent.postMessage(\n {\n type: \"openai:callTool\",\n requestId,\n toolName: tool,\n params\n },\n \"*\"\n );\n setTimeout(() => {\n window.removeEventListener(\"message\", handler);\n reject(new Error(\"Tool call timeout\"));\n }, TOOL_CALL_TIMEOUT_MS);\n });\n },\n async sendFollowupTurn(message) {\n const payload = typeof message === \"string\" ? { prompt: message } : message;\n const value = payload?.prompt ?? payload;\n window.parent.postMessage(\n {\n type: \"openai:sendFollowup\",\n message: value\n },\n \"*\"\n );\n },\n async requestDisplayMode(options = {}) {\n const mode = typeof options.mode === \"string\" ? options.mode : this.displayMode || \"inline\";\n this.displayMode = mode;\n window.parent.postMessage(\n {\n type: \"openai:requestDisplayMode\",\n mode\n },\n \"*\"\n );\n return { mode };\n },\n async sendFollowUpMessage(args) {\n const prompt = typeof args === \"string\" ? args : args?.prompt ?? \"\";\n await this.sendFollowupTurn(prompt);\n },\n async openExternal(options) {\n const href = typeof options === \"string\" ? options : options?.href;\n if (!href) {\n throw new Error(\"href is required for openExternal\");\n }\n window.parent.postMessage(\n {\n type: \"openai:openExternal\",\n href\n },\n \"*\"\n );\n window.open(href, \"_blank\", \"noopener,noreferrer\");\n }\n };\n if (openaiAPI.userAgent == null && typeof navigator !== \"undefined\") {\n try {\n openaiAPI.userAgent = navigator.userAgent || \"\";\n } catch {\n openaiAPI.userAgent = \"\";\n }\n }\n Object.defineProperty(window, \"openai\", {\n value: openaiAPI,\n writable: false,\n configurable: false,\n enumerable: true\n });\n Object.defineProperty(window, \"webplus\", {\n value: openaiAPI,\n writable: false,\n configurable: false,\n enumerable: true\n });\n setTimeout(() => {\n try {\n const globalsEvent = new CustomEvent(\"webplus:set_globals\", {\n detail: {\n globals: {\n displayMode: openaiAPI.displayMode,\n maxHeight: openaiAPI.maxHeight,\n theme: openaiAPI.theme,\n locale: openaiAPI.locale,\n safeArea: openaiAPI.safeArea,\n userAgent: openaiAPI.userAgent,\n capabilities: openaiAPI.capabilities\n }\n }\n });\n window.dispatchEvent(globalsEvent);\n } catch {\n }\n }, 0);\n setTimeout(() => {\n try {\n const stored = localStorage.getItem(widgetStateKey);\n if (stored) {\n openaiAPI.widgetState = JSON.parse(stored);\n }\n } catch {\n }\n }, 0);\n try {\n delete globalWindow.__MCP_WIDGET_CONFIG__;\n } catch {\n globalWindow.__MCP_WIDGET_CONFIG__ = void 0;\n }\n })();\n})();\n"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@idosal @liady this is the main change to support the double iframe exactly the same way as chatGTP.
also I think line 6 the meta CSP is not required, I had to remove it since the CSP can be set on the actual remote html and needs to be super relaxed otherwise nothing load