@fusedashlabs/ui9000-workspace
Version:
UI9000 workspace MCP. One tool, show_workspace, draws a chart from your table. Set TYPESAFE_API_KEY to let Jev choose the chart.
138 lines (137 loc) • 4.96 kB
JavaScript
/**
* MCP Apps surface for Cursor / Claude — same `ui://ui9000/chart` as mcp-ui.
* Prefer the sibling checkout's generated App View so local iframe/CSS
* changes show up without a deploy. Fall back to
* `https://mcp.ui9000.com/mcp-app/chart` when that file is missing
* (published bin, other machines).
* Do not put a bare data-link URL in tool text: hosts open it as a page (JSON).
*/
import { existsSync, readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { DEFAULT_HOSTED_MCP_BASE_URL } from '../migrate/datalink/hosted.js';
export const WORKSPACE_CHART_RESOURCE_URI = 'ui://ui9000/chart';
export const MCP_APP_RESOURCE_MIME = 'text/html;profile=mcp-app';
/** Same prefix mcp-ui chart-app parses from tool text (Cursor often strips `_meta`). */
export const UI9000_META_PREFIX = 'ui9000-meta:';
/** chart-app reads `src/inspector/meta-contract.ts`. It does not invent `_meta` keys. */
export const HOSTED_MCP_APP_CHART_PATH = '/mcp-app/chart';
const MAP_CONNECT_ORIGINS = [
'https://api.mapbox.com',
'https://events.mapbox.com',
'https://a.tiles.mapbox.com',
'https://b.tiles.mapbox.com',
'https://dash.fusedash.ai',
'https://mcp.ui9000.com',
];
const MAP_RESOURCE_ORIGINS = ['https://api.mapbox.com', 'https://*.tiles.mapbox.com'];
export function originFromBaseUrl(raw, fallback = DEFAULT_HOSTED_MCP_BASE_URL) {
const candidate = (raw || fallback).trim() || fallback;
try {
return new URL(candidate).origin;
}
catch {
return new URL(fallback).origin;
}
}
export function buildWorkspaceChartCsp(baseUrl) {
const origin = originFromBaseUrl(baseUrl);
return {
connectDomains: [...new Set([origin, ...MAP_CONNECT_ORIGINS])],
resourceDomains: [...MAP_RESOURCE_ORIGINS],
frameDomains: [],
};
}
export function hostedChartAppUrl(baseUrl) {
return `${originFromBaseUrl(baseUrl)}${HOSTED_MCP_APP_CHART_PATH}`;
}
let htmlCache;
let localHtmlCache;
export function localChartAppHtmlPath() {
let dir = dirname(fileURLToPath(import.meta.url));
for (let depth = 0; depth < 6; depth += 1) {
const candidates = [
join(dir, 'mcp-ui/src/mcp-app/generated/chart-app.html'),
join(dir, 'mcp-ui/dist/generated/chart-app.html'),
];
const hit = candidates.find((path) => existsSync(path));
if (hit)
return hit;
const parent = dirname(dir);
if (parent === dir)
break;
dir = parent;
}
return undefined;
}
export function loadLocalChartAppHtml() {
if (localHtmlCache)
return localHtmlCache;
const path = localChartAppHtmlPath();
if (!path)
return undefined;
const text = readFileSync(path, 'utf8');
if (!text.trim())
return undefined;
localHtmlCache = text;
return text;
}
export async function loadChartAppHtml(baseUrl = DEFAULT_HOSTED_MCP_BASE_URL, fetchImpl = fetch) {
const local = loadLocalChartAppHtml();
if (local)
return local;
return loadHostedChartAppHtml(baseUrl, fetchImpl);
}
export async function loadHostedChartAppHtml(baseUrl = DEFAULT_HOSTED_MCP_BASE_URL, fetchImpl = fetch) {
const url = hostedChartAppUrl(baseUrl);
if (htmlCache?.url === url)
return htmlCache.text;
const response = await fetchImpl(url, {
headers: { Accept: 'text/html' },
signal: AbortSignal.timeout(15_000),
});
if (!response.ok) {
throw new Error(`MCP App HTML failed (${response.status}) at ${url}`);
}
const text = await response.text();
if (!text.trim()) {
throw new Error(`MCP App HTML was empty at ${url}`);
}
htmlCache = { url, text };
return text;
}
export function resetHostedChartAppHtmlCacheForTests() {
htmlCache = undefined;
localHtmlCache = undefined;
}
export function chartIdFromDataUrl(dataUrl) {
try {
const parts = new URL(dataUrl).pathname.split('/').filter(Boolean);
return parts[parts.length - 1] ?? 'workspace';
}
catch {
return 'workspace';
}
}
export function formatWorkspaceAppText(summary, meta) {
const safe = summary
.split('\n')
.filter((line) => !/^\s*ui9000-meta:/i.test(line))
.join('\n');
return `${safe}\n${UI9000_META_PREFIX}${JSON.stringify({
chartType: meta.chartType,
dataUrl: meta.dataUrl,
chartId: meta.chartId,
...(meta.traceId ? { traceId: meta.traceId } : {}),
})}`;
}
export function workspaceChartAppResource(baseUrl, loadHtml) {
return {
uri: WORKSPACE_CHART_RESOURCE_URI,
name: 'UI9000 Chart',
mimeType: MCP_APP_RESOURCE_MIME,
description: 'Interactive chart surface for show_workspace (hosted mcp-ui App View).',
loadHtml: loadHtml ?? (() => loadChartAppHtml(baseUrl)),
ui: { csp: buildWorkspaceChartCsp(baseUrl) },
};
}