mcp-web-ui
Version:
Ultra-lightweight vanilla JavaScript framework for MCP servers - Zero dependencies, perfect security, 2-3KB bundle size
269 lines • 10.5 kB
JavaScript
/**
* ResourceManager - Configuration-driven resource loading
* Replaces hardcoded CSS/JS loading in UIServer with flexible, schema-driven approach
* Now supports CSS living with MCP servers instead of in framework
*/
import path from 'path';
import fs from 'fs';
export class ResourceManager {
config;
projectRoot;
staticBasePath;
constructor(config, projectRoot, staticBasePath = '/static') {
this.config = config;
this.projectRoot = projectRoot;
this.staticBasePath = staticBasePath;
}
/**
* Determine which resources to load based on UI schema
* This replaces hardcoded resource loading
*/
getRequiredResources(schema) {
const result = {
css: [],
javascript: [],
inlineCSS: '',
preloadLinks: []
};
// 1. Load theme CSS from MCP server (this includes styles.css)
const themeCSS = this.getThemeCSS(schema);
result.css.push(...themeCSS);
// 3. Load required JavaScript components
const requiredJS = this.getRequiredJavaScript(schema);
result.javascript.push(...requiredJS);
// 4. Generate inline CSS for runtime customization
result.inlineCSS = this.generateInlineCSS(schema);
// 5. Generate preload links for performance
result.preloadLinks = this.generatePreloadLinks(schema);
return result;
}
/**
* Schema-driven theme CSS loading from MCP server
* Simple: always load styles.css if MCP CSS directory is configured
* Uses configurable static base path for gateway proxy compatibility
*/
getThemeCSS(schema) {
const css = [];
// If MCP server directory is configured, load app styles
if (this.config.resources.css.mcpServerDirectory) {
css.push(`${this.staticBasePath}/styles.css`);
}
return css;
}
/**
* Dynamic JavaScript loading based on schema requirements
*/
getRequiredJavaScript(schema) {
const js = [];
// If bundling is enabled, only load the bundled file
if (this.config.resources.javascript.enableBundling) {
js.push(`${this.staticBasePath}/mcp-framework.js`);
}
else {
// Load individual files only if bundling is disabled
for (const componentConfig of this.config.resources.javascript.components) {
if (componentConfig.loadCondition(schema)) {
js.push(...componentConfig.files.map(file => `${this.staticBasePath}/${file}`));
}
}
}
return js;
}
/**
* Generate inline CSS for runtime customization
*/
generateInlineCSS(schema) {
const css = [];
// Add CSS custom properties based on schema
css.push(':root {');
// Theme-specific CSS variables
if (schema.title.toLowerCase().includes('todoodles')) {
css.push(' --app-primary: #6366f1;');
css.push(' --app-secondary: #10b981;');
}
else if (schema.title.toLowerCase().includes('grocery')) {
css.push(' --app-primary: #059669;');
css.push(' --app-secondary: #dc2626;');
}
css.push('}');
// Component-specific styling
for (const component of schema.components) {
css.push(this.getComponentCSS(component));
}
return css.join('\n');
}
/**
* Check if schema matches theme conditions
*/
matchesThemeConditions(schema, theme) {
if (!theme.conditions)
return false;
// Check schema title conditions
if (theme.conditions.schemaTitle) {
const titleMatch = theme.conditions.schemaTitle.some(title => schema.title.toLowerCase().includes(title.toLowerCase()));
if (titleMatch)
return true;
}
// Check component type conditions
if (theme.conditions.componentTypes) {
const componentMatch = theme.conditions.componentTypes.some(type => schema.components.some(comp => comp.type === type));
if (componentMatch)
return true;
}
return false;
}
/**
* Generate component-specific CSS
*/
getComponentCSS(component) {
const css = [];
// Add component-specific styling based on configuration
if (component.config?.customCSS) {
css.push(component.config.customCSS);
}
return css.join('\n');
}
/**
* Generate preload links for performance
*/
generatePreloadLinks(schema) {
const preloads = [];
// Preload theme CSS (this includes styles.css)
const themeCSS = this.getThemeCSS(schema);
for (const css of themeCSS) {
preloads.push(`<link rel="preload" href="${css}" as="style">`);
}
return preloads;
}
/**
* Bundle JavaScript files for single request
*/
async bundleJavaScript(schema) {
if (!this.config.resources.javascript.enableBundling) {
return '';
}
const bundledJS = [];
bundledJS.push('// MCP Framework Bundle - Generated by ResourceManager');
bundledJS.push('// Configuration-driven loading based on UI Schema\n');
// 1. Load framework files first
for (const frameworkFile of this.config.resources.javascript.framework) {
try {
const filePath = path.join(this.projectRoot, 'src/vanilla', frameworkFile);
if (fs.existsSync(filePath)) {
bundledJS.push(`// === Framework: ${frameworkFile} ===`);
bundledJS.push(fs.readFileSync(filePath, 'utf8'));
bundledJS.push('');
}
else {
console.warn(`Framework file not found: ${filePath}`);
}
}
catch (error) {
console.warn(`Failed to bundle framework file ${frameworkFile}:`, error);
}
}
// 2. Load component files based on schema
for (const componentConfig of this.config.resources.javascript.components) {
if (componentConfig.loadCondition(schema)) {
for (const componentFile of componentConfig.files) {
try {
const filePath = path.join(this.projectRoot, 'src/vanilla', componentFile);
if (fs.existsSync(filePath)) {
bundledJS.push(`// === Component: ${componentFile} ===`);
bundledJS.push(fs.readFileSync(filePath, 'utf8'));
bundledJS.push('');
}
else {
console.warn(`Component file not found: ${filePath}`);
}
}
catch (error) {
console.warn(`Failed to bundle component file ${componentFile}:`, error);
}
}
}
}
return bundledJS.join('\n');
}
/**
* Get all CSS content for bundling from MCP server directories
*/
async bundleCSS(schema) {
const cssFiles = this.getThemeCSS(schema);
const bundledCSS = [];
bundledCSS.push('/* MCP Theme Bundle - Generated by ResourceManager */');
bundledCSS.push('/* CSS loaded from MCP server directories */\n');
for (const cssFile of cssFiles) {
try {
let filePath = '';
// Look in MCP server directory first
if (this.config.resources.css.mcpServerDirectory) {
const cssDir = this.config.resources.css.mcpServerDirectory;
const attempt = path.join(cssDir, cssFile.replace('/static/styles.css', 'styles.css'));
if (fs.existsSync(attempt)) {
filePath = attempt;
}
}
// Fallback to framework directories if not found in MCP server
if (!filePath) {
const directories = this.config.resources.static.directories;
for (const dir of directories) {
const absDir = path.isAbsolute(dir) ? dir : path.join(this.projectRoot, dir);
const attempt = path.join(absDir, cssFile.replace('/static/', ''));
if (fs.existsSync(attempt)) {
filePath = attempt;
break;
}
}
}
if (fs.existsSync(filePath)) {
bundledCSS.push(`/* === ${cssFile} === */`);
bundledCSS.push(fs.readFileSync(filePath, 'utf8'));
bundledCSS.push('');
}
else {
bundledCSS.push(`/* CSS file not found: ${cssFile} */`);
}
}
catch (error) {
console.warn(`Failed to bundle ${cssFile}:`, error);
bundledCSS.push(`/* Error loading ${cssFile}: ${error} */`);
}
}
return bundledCSS.join('\n');
}
/**
* Validate that all required resources exist
*/
validateResources(schema) {
const missing = [];
const resources = this.getRequiredResources(schema);
const directories = this.config.resources.static.directories;
// Check CSS files
for (const css of resources.css) {
let found = false;
for (const dir of directories) {
const absDir = path.isAbsolute(dir) ? dir : path.join(this.projectRoot, dir);
const attempt = path.join(absDir, css.replace('/static/', ''));
if (fs.existsSync(attempt)) {
found = true;
break;
}
}
if (!found)
missing.push(css);
}
// Check JS files
for (const js of resources.javascript) {
const filePath = path.join(this.projectRoot, 'src/vanilla', js.replace('/static/', ''));
if (!fs.existsSync(filePath)) {
missing.push(js);
}
}
return {
valid: missing.length === 0,
missing
};
}
}
//# sourceMappingURL=ResourceManager.js.map