@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.
93 lines (92 loc) • 3.56 kB
JavaScript
/**
* Hosted data-link origin — same remote mcp-ui npx uses (`https://mcp.ui9000.com`).
* Stdio (Cursor / Claude) POSTs payloads there so mcp.json needs no env.
* In-process tests and loopback `MCP_BASE_URL` keep the local TTL store.
*/
export const DEFAULT_HOSTED_MCP_BASE_URL = 'https://mcp.ui9000.com';
/** Set by `startWorkspaceServer` in hosted mode. Tests and loopback must not set this. */
export const REMOTE_PERSIST_ENV = 'MCP_DATA_LINK_REMOTE_PERSIST';
export const HOSTED_DATA_LINK_PERSIST_TIMEOUT_MS = 15_000;
const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '::1']);
export function isRemoteDataLinkBase(baseUrl) {
try {
const host = new URL(baseUrl).hostname;
return !LOOPBACK_HOSTS.has(host);
}
catch {
return false;
}
}
export function shouldRemotePersistDataLink(env = process.env) {
const raw = (env[REMOTE_PERSIST_ENV] || '').trim().toLowerCase();
return raw === '1' || raw === 'true' || raw === 'yes';
}
export function enableRemotePersist(env) {
env[REMOTE_PERSIST_ENV] = '1';
}
export function dataLinkCreateUrl(baseUrl) {
return `${normalizeHostedBaseUrl(baseUrl)}/v1/data-links`;
}
function normalizeHostedBaseUrl(raw) {
const trimmed = raw.trim().replace(/\/+$/, '');
if (!trimmed)
return trimmed;
try {
const url = new URL(trimmed);
if (url.pathname.replace(/\/+$/, '') === '/mcp')
return url.origin;
return trimmed;
}
catch {
return trimmed;
}
}
function isAbortError(error) {
return Boolean(error && typeof error === 'object' && 'name' in error && error.name === 'AbortError');
}
export async function persistHostedDataLink(payload, baseUrl, fetchImpl = fetch) {
const url = dataLinkCreateUrl(baseUrl);
let response;
try {
response = await fetchImpl(url, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
config: { source: 'show_workspace' },
data: payload,
}),
signal: AbortSignal.timeout(HOSTED_DATA_LINK_PERSIST_TIMEOUT_MS),
});
}
catch (error) {
if (isAbortError(error)) {
throw new Error(`Hosted data-link save timed out after ${HOSTED_DATA_LINK_PERSIST_TIMEOUT_MS}ms at ${url}`);
}
const detail = error instanceof Error ? error.message : String(error);
throw new Error(`Hosted data-link save failed at ${url}: ${detail.slice(0, 300)}`);
}
if (!response.ok) {
const detail = await response.text().catch(() => '');
throw new Error(`Hosted data-link save failed (${response.status}) at ${url}${detail ? `: ${detail.slice(0, 300)}` : ''}`);
}
const body = (await response.json());
if (!body?.id || typeof body.dataUrl !== 'string' || !body.dataUrl) {
throw new Error(`Hosted data-link save returned an invalid body from ${url}`);
}
let dataOrigin;
let expectedOrigin;
try {
dataOrigin = new URL(body.dataUrl).origin;
expectedOrigin = new URL(normalizeHostedBaseUrl(baseUrl)).origin;
}
catch {
throw new Error('Hosted data-link save returned an invalid dataUrl');
}
if (dataOrigin !== expectedOrigin) {
throw new Error(`Hosted data-link save returned a dataUrl origin (${dataOrigin}) that does not match ${expectedOrigin}`);
}
return { id: body.id, dataUrl: body.dataUrl };
}