UNPKG

@wonderwhy-er/desktop-commander

Version:

MCP server for terminal operations and file editing

87 lines (86 loc) 3.72 kB
/** * Server-side UI resource loading helpers for MCP responses. It resolves packaged UI assets, reads files safely, and exposes structured resource payloads to clients. */ import fs from 'fs/promises'; import path from 'path'; import { fileURLToPath } from 'url'; import { CONFIG_EDITOR_RESOURCE_URI, FILE_PREVIEW_RESOURCE_URI } from './contracts.js'; const UI_RESOURCE_MIME_TYPE = 'text/html;profile=mcp-app'; export const FILE_PREVIEW_RESOURCE = { uri: FILE_PREVIEW_RESOURCE_URI, name: 'Desktop Commander File Preview', description: 'Markdown-first preview surface for read_file structured content.', mimeType: UI_RESOURCE_MIME_TYPE }; export const CONFIG_EDITOR_RESOURCE = { uri: CONFIG_EDITOR_RESOURCE_URI, name: 'Desktop Commander Config Editor', description: 'Interactive editor for Desktop Commander configuration values.', mimeType: UI_RESOURCE_MIME_TYPE }; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const DIST_FILE_PREVIEW_DIR = path.resolve(__dirname, 'file-preview'); const DIST_CONFIG_EDITOR_DIR = path.resolve(__dirname, 'config-editor'); function replaceOrThrow(source, pattern, replacement, context) { if (!pattern.test(source)) { throw new Error(`UI template is missing expected ${context}`); } return source.replace(pattern, () => replacement); } function inlineTemplateAssets(templateHtml, css, runtime) { const safeCss = css.replace(/<\/style/gi, '<\\/style'); const safeRuntime = runtime.replace(/<\/script/gi, '<\\/script'); const cssInlined = replaceOrThrow(templateHtml, /<link[^>]*href=["']\.\/styles\.css["'][^>]*>/i, `<style>${safeCss}</style>`, 'styles.css link tag'); const runtimeInlined = replaceOrThrow(cssInlined, /<script[^>]*src=["']\.\/[^"']+["'][^>]*>\s*<\/script>/i, `<script>${safeRuntime}</script>`, 'runtime script tag'); if (/href=["']\.\/styles\.css["']/i.test(runtimeInlined) || /<script[^>]*src=["']\.\/[^"']+["']/i.test(runtimeInlined)) { throw new Error('UI template still contains external static asset references after inlining'); } return runtimeInlined; } async function readInlinedResourceHtml(distDir, runtimeFileName) { const [templateHtml, css, runtime] = await Promise.all([ fs.readFile(path.join(distDir, 'index.html'), 'utf8'), fs.readFile(path.join(distDir, 'styles.css'), 'utf8'), fs.readFile(path.join(distDir, runtimeFileName), 'utf8') ]); return inlineTemplateAssets(templateHtml, css, runtime); } export async function getFilePreviewResourceText() { return readInlinedResourceHtml(DIST_FILE_PREVIEW_DIR, 'preview-runtime.js'); } export async function getConfigEditorResourceText() { return readInlinedResourceHtml(DIST_CONFIG_EDITOR_DIR, 'config-editor-runtime.js'); } const READABLE_UI_RESOURCES = { [FILE_PREVIEW_RESOURCE_URI]: { mimeType: FILE_PREVIEW_RESOURCE.mimeType, getText: getFilePreviewResourceText }, [CONFIG_EDITOR_RESOURCE_URI]: { mimeType: CONFIG_EDITOR_RESOURCE.mimeType, getText: getConfigEditorResourceText } }; export function listUiResources() { return [FILE_PREVIEW_RESOURCE, CONFIG_EDITOR_RESOURCE]; } export async function readUiResource(uri) { const resource = READABLE_UI_RESOURCES[uri]; if (!resource) { return null; } const resourceText = await resource.getText(); const resourceMeta = resource.getMeta?.(); return { contents: [ { uri, mimeType: resource.mimeType, text: resourceText, ...(resourceMeta ? { _meta: resourceMeta } : {}) } ] }; }