mcp-web-ui
Version:
Ultra-lightweight vanilla JavaScript framework for MCP servers - Zero dependencies, perfect security, 2-3KB bundle size
212 lines • 8.37 kB
JavaScript
import { SessionManager } from './session/SessionManager.js';
import { GenericUIServer } from './server/GenericUIServer.js';
import { UIServerConfigBuilder } from './server/UIServerConfig.js';
/**
* Main MCP Web UI framework class
* Orchestrates session management and dynamic UI servers
* Now uses GenericUIServer with MCP server CSS architecture
*/
export class MCPWebUI {
sessionManager;
activeServers = new Map();
config;
constructor(config) {
// Set defaults
this.config = {
sessionTimeout: 30 * 60 * 1000, // 30 minutes
pollInterval: 2000, // 2 seconds
portRange: [3000, 65535],
enableLogging: true,
baseUrl: 'localhost',
// If baseUrl is not localhost, default to binding all interfaces
bindAddress: config.baseUrl && config.baseUrl !== 'localhost' ? '0.0.0.0' : 'localhost',
cssPath: './static', // Default MCP server CSS path
serverName: '', // Default server name
...config
};
this.sessionManager = new SessionManager(this.config.sessionTimeout, this.config.portRange, this.config.baseUrl);
// Set up automatic cleanup - check for expired sessions every minute
this.startAutomaticCleanup();
// Cleanup on process exit
process.on('SIGINT', () => this.shutdown());
process.on('SIGTERM', () => this.shutdown());
}
/**
* Start automatic cleanup of expired sessions
*/
startAutomaticCleanup() {
setInterval(async () => {
await this.cleanupExpiredSessions();
}, 60 * 1000); // Check every minute
}
/**
* Cleanup expired sessions and their UI servers
*/
async cleanupExpiredSessions() {
const now = new Date();
const activeSessions = this.sessionManager.getActiveSessions();
const expiredSessions = activeSessions.filter(session => now > session.expiresAt);
if (expiredSessions.length > 0) {
this.log('INFO', `Found ${expiredSessions.length} expired sessions, cleaning up...`);
for (const session of expiredSessions) {
this.log('INFO', `Session ${session.id} expired (${session.expiresAt.toISOString()}), terminating...`);
await this.terminateSession(session.id);
}
}
}
/**
* Create a new UI session for a user
* Returns the session with URL that can be shared
* Automatically cleans up any existing session for the same user
*/
async createSession(userId) {
try {
// Check for existing session for this user and clean it up
const existingSession = this.sessionManager.getSessionByUserId(userId);
if (existingSession) {
this.log('INFO', `Cleaning up existing session for user ${userId} before creating new one`);
await this.terminateSession(existingSession.id);
}
// Create session (SessionManager will also check, but this ensures UI server cleanup)
const session = this.sessionManager.createSession(userId);
// Create configuration for MCP server CSS architecture
const uiConfig = this.createUIServerConfig();
// Create and start GenericUIServer with MCP server CSS support
const uiServer = new GenericUIServer(session, this.config.schema, this.config.dataSource, this.config.onUpdate, this.sessionManager, uiConfig, this.config.pollInterval, this.config.bindAddress);
await uiServer.start();
this.activeServers.set(session.id, uiServer);
this.log('INFO', `Created UI session for user ${userId}: ${session.url}`);
return session;
}
catch (error) {
this.log('ERROR', `Failed to create UI session for user ${userId}: ${error}`);
throw error;
}
}
/**
* Create UI server configuration with MCP server CSS support
* Environment variable driven approach
*/
createUIServerConfig() {
const configBuilder = UIServerConfigBuilder.create();
const config = configBuilder.build();
// CSS path priority:
// 1. Explicit config cssPath
// 2. Environment variable MCP_WEB_UI_CSS_PATH
// 3. Default: ./static
config.resources.css.mcpServerDirectory =
this.config.cssPath ||
process.env.MCP_WEB_UI_CSS_PATH ||
'./static';
return config;
}
/**
* Get active session by token (for validation)
*/
getSessionByToken(token) {
return this.sessionManager.getSessionByToken(token);
}
/**
* Manually terminate a session
*/
async terminateSession(sessionId) {
try {
const uiServer = this.activeServers.get(sessionId);
if (uiServer) {
await uiServer.stop();
this.activeServers.delete(sessionId);
}
const success = this.sessionManager.terminateSession(sessionId);
if (success) {
this.log('INFO', `Terminated session ${sessionId}`);
}
return success;
}
catch (error) {
this.log('ERROR', `Failed to terminate session ${sessionId}: ${error}`);
return false;
}
}
/**
* Get stats about active sessions and servers
*/
getStats() {
const sessionStats = this.sessionManager.getStats();
return {
...sessionStats,
activeServers: this.activeServers.size,
serverPorts: Array.from(this.activeServers.values()).map(server => server.session?.port).filter(Boolean)
};
}
/**
* Cleanup and shutdown all sessions and servers
*/
async shutdown() {
this.log('INFO', 'Shutting down MCPWebUI...');
// Stop all active servers
const stopPromises = Array.from(this.activeServers.values()).map(server => server.stop().catch(error => this.log('ERROR', `Error stopping server: ${error}`)));
await Promise.all(stopPromises);
this.activeServers.clear();
// Shutdown session manager
this.sessionManager.shutdown();
this.log('INFO', 'MCPWebUI shutdown complete');
}
/**
* Create MCP tool definition for get_web_ui
*/
getMCPToolDefinition() {
return {
name: "get_web_ui",
description: `Get a web interface for ${this.config.schema.title}`,
inputSchema: {
type: "object",
properties: {
extend_minutes: {
type: "number",
description: "Minutes to extend session (default: 30)",
minimum: 5,
maximum: 120
}
},
additionalProperties: false
}
};
}
/**
* Handle the get_web_ui tool call
* This is what MCP servers will call
*/
async handleGetWebUI(userId, extendMinutes = 30) {
try {
const session = await this.createSession(userId);
return {
content: [{
type: "text",
text: `🌐 Your ${this.config.schema.title} dashboard is ready!\n\n` +
`🔗 **URL**: ${session.url}\n\n` +
`⏰ **Session expires**: ${session.expiresAt.toLocaleString()}\n` +
`👤 **User**: ${userId}\n\n` +
`💡 *Tip: Bookmark this link or use the "Extend" button in the UI to keep your session active.*`
}]
};
}
catch (error) {
return {
content: [{
type: "text",
text: `❌ Failed to create web UI: ${error instanceof Error ? error.message : 'Unknown error'}`
}]
};
}
}
/**
* Simple logging utility
*/
log(level, message) {
if (this.config.enableLogging) {
const timestamp = new Date().toISOString();
console.error(`[${timestamp}][${level}][MCPWebUI] ${message}`);
}
}
}
//# sourceMappingURL=MCPWebUI.js.map