fix: MCP apps rendering now
This commit is contained in:
@@ -3,14 +3,15 @@
|
|||||||
* from shared boilerplate + per-view configuration.
|
* from shared boilerplate + per-view configuration.
|
||||||
*
|
*
|
||||||
* Each generated page uses the `App` class from
|
* Each generated page uses the `App` class from
|
||||||
* `@modelcontextprotocol/ext-apps` (loaded via the `app-with-deps` bundle)
|
* `@modelcontextprotocol/ext-apps` (inlined as a self-contained script)
|
||||||
* and is served as a `ui://` resource for MCP hosts.
|
* and is served as a `ui://` resource for MCP hosts.
|
||||||
*
|
*
|
||||||
* This replaces the previous approach of 4 separate HTML files that
|
* The bundle is loaded from disk once and cached; it cannot use bare
|
||||||
* duplicated ~80% of their content (CSS, JS boilerplate, accept/discard
|
* specifiers in the sandboxed iframe, so it is inlined directly.
|
||||||
* handlers, status display, XSS escaping, app connection).
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import * as fs from 'fs';
|
||||||
|
|
||||||
/** Configuration for a single MCP review view. */
|
/** Configuration for a single MCP review view. */
|
||||||
export interface McpViewConfig {
|
export interface McpViewConfig {
|
||||||
/** Page <title>. */
|
/** Page <title>. */
|
||||||
@@ -57,10 +58,42 @@ const SHARED_CSS = `\
|
|||||||
.status-error { background: #f8d7da; color: #721c24; }
|
.status-error { background: #f8d7da; color: #721c24; }
|
||||||
.word-count { color: #888; font-size: 0.8rem; }`;
|
.word-count { color: #888; font-size: 0.8rem; }`;
|
||||||
|
|
||||||
|
/* ── Inline bundle loader ───────────────────────────────────────────── */
|
||||||
|
|
||||||
|
let _appBundle: string | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the `app-with-deps` ESM bundle from node_modules, strip its
|
||||||
|
* `export{...}` block, and add `globalThis.__bdsExtApp = App_internal_name;`
|
||||||
|
* so the App class is accessible as a global from a plain `<script>` tag.
|
||||||
|
* Result is cached after the first call.
|
||||||
|
*/
|
||||||
|
function getAppBundle(): string {
|
||||||
|
if (_appBundle !== null) return _appBundle;
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||||
|
const bundlePath: string = require.resolve('@modelcontextprotocol/ext-apps/app-with-deps');
|
||||||
|
let source = fs.readFileSync(bundlePath, 'utf-8');
|
||||||
|
|
||||||
|
// The bundle ends with export{...,X as App,...}.
|
||||||
|
// Extract the internal variable name for `App`.
|
||||||
|
const match = source.match(/export\{[^}]*\b(\w+)\s+as\s+App\b[^}]*\}/);
|
||||||
|
if (!match) throw new Error('Could not find App export in app-with-deps bundle');
|
||||||
|
const internalName = match[1];
|
||||||
|
|
||||||
|
// Strip ESM export block and expose App class on globalThis.
|
||||||
|
source = source.replace(/export\{[^}]+\}/, '');
|
||||||
|
source += `\nglobalThis.__bdsExtApp=${internalName};`;
|
||||||
|
|
||||||
|
_appBundle = source;
|
||||||
|
return _appBundle;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Shared JS ──────────────────────────────────────────────────────── */
|
/* ── Shared JS ──────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
const SHARED_JS = `\
|
const SHARED_JS = `\
|
||||||
import { App } from "@modelcontextprotocol/ext-apps/app-with-deps";
|
const App = globalThis.__bdsExtApp;
|
||||||
|
if (!App) { document.getElementById("status").textContent = "Error: App bundle not loaded"; document.getElementById("status").className = "status status-error"; document.getElementById("status").style.display = "block"; throw new Error("App bundle not loaded"); }
|
||||||
|
|
||||||
const app = new App({ name: "bDS Review", version: "1.0.0" });
|
const app = new App({ name: "bDS Review", version: "1.0.0" });
|
||||||
|
|
||||||
@@ -72,6 +105,8 @@ const SHARED_JS = `\
|
|||||||
if (textContent?.text) {
|
if (textContent?.text) {
|
||||||
currentData = JSON.parse(textContent.text);
|
currentData = JSON.parse(textContent.text);
|
||||||
renderReview(currentData);
|
renderReview(currentData);
|
||||||
|
} else {
|
||||||
|
showStatus("Tool result received but no text content found. Raw: " + JSON.stringify(result).slice(0, 200), "error");
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showStatus("Failed to parse tool result: " + e.message, "error");
|
showStatus("Failed to parse tool result: " + e.message, "error");
|
||||||
@@ -124,7 +159,18 @@ const SHARED_JS = `\
|
|||||||
}
|
}
|
||||||
|
|
||||||
window.showStatus = showStatus;
|
window.showStatus = showStatus;
|
||||||
function esc(s) { const d = document.createElement("div"); d.textContent = s; return d.innerHTML; }`;
|
function esc(s) { const d = document.createElement("div"); d.textContent = s; return d.innerHTML; }
|
||||||
|
|
||||||
|
window.__connectApp = () => {
|
||||||
|
app.connect()
|
||||||
|
.then(() => {
|
||||||
|
showStatus("Connected — waiting for tool result…", "success");
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
showStatus("Failed to connect to host: " + e.message, "error");
|
||||||
|
console.error("App connect failed:", e);
|
||||||
|
});
|
||||||
|
};`;
|
||||||
|
|
||||||
/* ── Builder ────────────────────────────────────────────────────────── */
|
/* ── Builder ────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
@@ -150,6 +196,7 @@ ${SHARED_CSS}${extraCss}
|
|||||||
<p class="meta">${config.waitingMessage}</p>
|
<p class="meta">${config.waitingMessage}</p>
|
||||||
</div>
|
</div>
|
||||||
<div id="status" class="status" style="display:none"></div>
|
<div id="status" class="status" style="display:none"></div>
|
||||||
|
<script>${getAppBundle()}</script>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
${SHARED_JS}${extraJs}
|
${SHARED_JS}${extraJs}
|
||||||
|
|
||||||
@@ -157,7 +204,7 @@ ${SHARED_JS}${extraJs}
|
|||||||
${config.renderBody}
|
${config.renderBody}
|
||||||
};
|
};
|
||||||
|
|
||||||
app.connect().catch(e => console.error("App connect failed:", e));
|
window.__connectApp();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ describe('mcp-views', () => {
|
|||||||
expect(html).toContain('</html>');
|
expect(html).toContain('</html>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('contains App import from ext-apps', () => {
|
it('inlines ext-apps bundle and uses App class', () => {
|
||||||
const html = reviewPostHtml();
|
const html = reviewPostHtml();
|
||||||
expect(html).toContain('@modelcontextprotocol/ext-apps/app-with-deps');
|
expect(html).toContain('__bdsExtApp'); // inlined bundle sets global
|
||||||
expect(html).toContain('new App(');
|
expect(html).toContain('new App('); // SHARED_JS uses the global
|
||||||
});
|
});
|
||||||
|
|
||||||
it('contains accept and discard buttons', () => {
|
it('contains accept and discard buttons', () => {
|
||||||
@@ -61,9 +61,10 @@ describe('mcp-views', () => {
|
|||||||
expect(html).toContain('</html>');
|
expect(html).toContain('</html>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('contains App import from ext-apps', () => {
|
it('inlines ext-apps bundle and uses App class', () => {
|
||||||
const html = reviewScriptHtml();
|
const html = reviewScriptHtml();
|
||||||
expect(html).toContain('@modelcontextprotocol/ext-apps/app-with-deps');
|
expect(html).toContain('__bdsExtApp');
|
||||||
|
expect(html).toContain('new App(');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('contains accept and discard buttons', () => {
|
it('contains accept and discard buttons', () => {
|
||||||
@@ -87,9 +88,10 @@ describe('mcp-views', () => {
|
|||||||
expect(html).toContain('</html>');
|
expect(html).toContain('</html>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('contains App import from ext-apps', () => {
|
it('inlines ext-apps bundle and uses App class', () => {
|
||||||
const html = reviewTemplateHtml();
|
const html = reviewTemplateHtml();
|
||||||
expect(html).toContain('@modelcontextprotocol/ext-apps/app-with-deps');
|
expect(html).toContain('__bdsExtApp');
|
||||||
|
expect(html).toContain('new App(');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('contains accept and discard buttons', () => {
|
it('contains accept and discard buttons', () => {
|
||||||
@@ -113,9 +115,10 @@ describe('mcp-views', () => {
|
|||||||
expect(html).toContain('</html>');
|
expect(html).toContain('</html>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('contains App import from ext-apps', () => {
|
it('inlines ext-apps bundle and uses App class', () => {
|
||||||
const html = reviewMetadataHtml();
|
const html = reviewMetadataHtml();
|
||||||
expect(html).toContain('@modelcontextprotocol/ext-apps/app-with-deps');
|
expect(html).toContain('__bdsExtApp');
|
||||||
|
expect(html).toContain('new App(');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('contains accept and discard buttons', () => {
|
it('contains accept and discard buttons', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user