UNPKG

@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
/** * 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) }, }; }