Skip to main content
Glama
jonthebeef

Superdesign MCP Server

by jonthebeef

superdesign_gallery

Create an HTML gallery to display all designs from a specified workspace path for easy browser viewing. Ideal for organizing and reviewing Superdesign MCP Server's generated UI designs, wireframes, and components.

Instructions

Generate an HTML gallery to view all designs in a browser

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
workspace_pathNoWorkspace path (defaults to current directory)

Implementation Reference

  • The handler for the superdesign_gallery tool. It validates input with GallerySchema, lists design files, generates a full HTML gallery page using generateGalleryHTML helper, constructs detailed instructions for Claude Code to write the gallery.html file and open it in the browser.
    case "superdesign_gallery": { const { workspace_path } = GallerySchema.parse(args); try { const superdesignDir = getSuperdeignDirectory(workspace_path); const designIterationsDir = path.join(superdesignDir, 'design_iterations'); if (!existsSync(designIterationsDir)) { return { content: [{ type: "text", text: "No design iterations found. Generate some designs first using superdesign_generate." }], }; } const designFiles = await glob('*.{html,svg}', { cwd: designIterationsDir }); if (designFiles.length === 0) { return { content: [{ type: "text", text: "No design files found in design_iterations directory." }], }; } const galleryPath = path.join(superdesignDir, 'gallery.html'); // Generate gallery HTML const galleryHtml = generateGalleryHTML(designFiles, superdesignDir); let specifications = `GALLERY GENERATION SPECIFICATION FOR CLAUDE CODE: IMPORTANT: You must create a gallery HTML file to view all designs in a browser. === GALLERY PARAMETERS === - Gallery file: ${galleryPath} - Design files found: ${designFiles.length} - Directory: ${designIterationsDir} === FILES TO DISPLAY === ${designFiles.map((file, index) => `${index + 1}. ${file}`).join('\n')} === GALLERY HTML CONTENT === ${galleryHtml} === EXECUTION INSTRUCTIONS === 1. Create the gallery file at: ${galleryPath} 2. Write the gallery HTML content above to the file 3. **AUTOMATICALLY OPEN** the gallery in the default browser using: open "${galleryPath}" 4. The gallery will show all design variations with enhanced MCP integration 5. Gallery features: smart refresh, auto-refresh, dark mode, file metadata display Please proceed to create the gallery file and **automatically open it in the browser**.`; return { content: [{ type: "text", text: specifications }], }; } catch (error: any) { return { content: [{ type: "text", text: `Error generating gallery: ${error.message}` }], }; } } case "superdesign_delete": { const { filename, workspace_path } = DeleteDesignSchema.parse(args); try {
  • Zod input schema for the superdesign_gallery tool defining optional workspace_path parameter.
    const GallerySchema = z.object({ workspace_path: z.string().optional().describe("Workspace path (defaults to current directory)") });
  • src/index.ts:2002-2010 (registration)
    Registration of the superdesign_gallery tool in the MCP server's listTools response, including name, description, and inputSchema matching the Zod schema.
    name: "superdesign_gallery", description: "Generate an HTML gallery to view all designs in a browser", inputSchema: { type: "object", properties: { workspace_path: { type: "string", description: "Workspace path (defaults to current directory)" } }, }, },
  • Core helper function that generates the complete HTML for the design gallery, including responsive grid, file previews (iframes/objects), metadata display, dark/light theme, auto-refresh logic, and smart file change detection using embedded manifest.
    function generateGalleryHTML(designFiles: string[], superdesignDir: string): string { const metadata = getDesignMetadata(superdesignDir); const metadataMap = new Map(metadata.map(m => [m.fileName, m])); // Generate file manifest for change detection const fileManifest = designFiles.map(file => { const fileMetadata = metadataMap.get(file); const designIterationsDir = path.join(superdesignDir, 'design_iterations'); const filePath = path.join(designIterationsDir, file); const stats = existsSync(filePath) ? statSync(filePath) : null; return { name: file, size: stats?.size || 0, modified: stats?.mtime.getTime() || 0, created: fileMetadata?.createdAt || stats?.birthtime.toISOString() || new Date().toISOString() }; }); const designCards = designFiles.map((file, index) => { const fileExtension = path.extname(file).toLowerCase(); const fileName = path.basename(file, fileExtension); const relativePath = `./design_iterations/${file}`; const fileMetadata = metadataMap.get(file); const manifest = fileManifest.find(f => f.name === file); let previewContent = ''; if (fileExtension === '.html') { previewContent = `<iframe src="${relativePath}" loading="lazy" data-file="${file}"></iframe>`; } else if (fileExtension === '.svg') { previewContent = `<object data="${relativePath}" type="image/svg+xml" class="svg-preview" data-file="${file}"></object>`; } // Format file size const formatFileSize = (bytes: number): string => { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; }; // Format date const formatDate = (dateString: string): string => { const date = new Date(dateString); return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', hour12: false }); }; return ` <div class="design-card" data-file="${file}" data-created="${fileMetadata?.createdAt || ''}" data-modified="${manifest?.modified || 0}"> <div class="design-header"> <div class="design-title"> <h3>${fileName}</h3> <span class="design-type">${fileExtension.toUpperCase()}</span> </div> <div class="design-meta"> ${fileMetadata ? ` <span class="design-size">${formatFileSize(fileMetadata.fileSize)}</span> <span class="design-date">${formatDate(fileMetadata.createdAt)}</span> ` : manifest ? ` <span class="design-size">${formatFileSize(manifest.size)}</span> <span class="design-date">${formatDate(manifest.created)}</span> ` : ''} </div> </div> <div class="design-preview"> ${previewContent} </div> <div class="design-actions"> <button onclick="openFullscreen('${relativePath}')" class="btn-primary">View Full</button> <button onclick="copyPath('${relativePath}')" class="btn-secondary">Copy Path</button> <button onclick="deleteDesign('${file}')" class="btn-danger">Delete</button> </div> </div> `; }).join(''); return `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Superdesign Gallery</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } :root { --bg-primary: #ffffff; --bg-secondary: #f8fafc; --bg-card: #ffffff; --text-primary: #1a202c; --text-secondary: #718096; --border-color: #e2e8f0; --shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); --shadow-hover: 0 10px 25px -5px rgba(0, 0, 0, 0.2); } [data-theme="dark"] { --bg-primary: #1a202c; --bg-secondary: #2d3748; --bg-card: #2d3748; --text-primary: #f7fafc; --text-secondary: #a0aec0; --border-color: #4a5568; --shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3); --shadow-hover: 0 10px 25px -5px rgba(0, 0, 0, 0.4); } body { font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; background: var(--bg-primary); color: var(--text-primary); min-height: 100vh; padding: 20px; transition: background 0.3s ease, color 0.3s ease; } .header { margin-bottom: 40px; display: flex; justify-content: space-between; align-items: flex-start; } .header-content h1 { font-size: 2.5rem; color: var(--text-primary); margin-bottom: 8px; font-weight: 600; letter-spacing: -0.02em; } .header-content p { color: var(--text-secondary); font-size: 1rem; font-weight: 400; } .header-controls { display: flex; gap: 12px; align-items: center; } .refresh-controls { display: flex; gap: 8px; align-items: center; } .auto-refresh-toggle { display: flex; align-items: center; gap: 6px; font-size: 0.875rem; color: var(--text-secondary); cursor: pointer; user-select: none; } .auto-refresh-toggle input[type="checkbox"] { width: 16px; height: 16px; accent-color: var(--text-primary); } .btn-secondary { background: var(--bg-card); border: 1px solid var(--border-color); color: var(--text-secondary); padding: 6px 12px; border-radius: 6px; cursor: pointer; font-family: inherit; font-size: 0.875rem; transition: all 0.2s ease; display: flex; align-items: center; gap: 6px; } .btn-secondary:hover { background: var(--bg-secondary); transform: translateY(-1px); } .btn-secondary:disabled { opacity: 0.5; cursor: not-allowed; } .btn-secondary:disabled:hover { transform: none; } .theme-toggle { background: var(--bg-card); border: 1px solid var(--border-color); color: var(--text-primary); padding: 8px 16px; border-radius: 8px; cursor: pointer; font-family: inherit; font-size: 0.875rem; transition: all 0.2s ease; display: flex; align-items: center; gap: 8px; } .theme-toggle:hover { background: var(--bg-secondary); transform: translateY(-1px); } .updated-indicator { position: absolute; top: 8px; right: 8px; width: 12px; height: 12px; background: #48bb78; border-radius: 50%; border: 2px solid var(--bg-card); animation: pulse 2s infinite; } @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } .refresh-indicator { display: inline-block; animation: spin 1s linear infinite; } @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } .gallery-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(400px, 1fr)); gap: 24px; max-width: 1400px; } .design-card { background: var(--bg-card); border: 1px solid var(--border-color); border-radius: 12px; box-shadow: var(--shadow); overflow: hidden; transition: transform 0.2s ease, box-shadow 0.2s ease; } .design-card:hover { transform: translateY(-4px); box-shadow: var(--shadow-hover); } .design-header { padding: 16px; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: flex-start; } .design-title { display: flex; align-items: center; gap: 8px; } .design-title h3 { color: var(--text-primary); font-size: 1rem; font-weight: 500; font-family: inherit; } .design-type { background: var(--bg-secondary); color: var(--text-secondary); padding: 4px 8px; border-radius: 4px; font-size: 0.75rem; font-weight: 400; font-family: inherit; } .design-meta { display: flex; flex-direction: column; align-items: flex-end; gap: 2px; } .design-size, .design-date { color: var(--text-secondary); font-size: 0.75rem; font-family: inherit; } .design-date { opacity: 0.8; } .design-preview { height: 300px; position: relative; overflow: hidden; } .design-preview iframe { width: 100%; height: 100%; border: none; transform: scale(0.5); transform-origin: top left; width: 200%; height: 200%; } .design-preview .svg-preview { width: 100%; height: 100%; object-fit: contain; background: var(--bg-primary); } .design-actions { padding: 16px; display: flex; gap: 8px; } .btn-primary { background: #4299e1; color: white; border: none; padding: 8px 16px; border-radius: 6px; font-size: 0.875rem; font-family: inherit; cursor: pointer; transition: background 0.2s ease; } .btn-primary:hover { background: #3182ce; } .btn-secondary { background: var(--bg-secondary); color: var(--text-secondary); border: 1px solid var(--border-color); padding: 8px 16px; border-radius: 6px; font-size: 0.875rem; font-family: inherit; cursor: pointer; transition: background 0.2s ease; } .btn-secondary:hover { background: var(--border-color); } .btn-danger { background: #e53e3e; color: white; border: none; padding: 8px 16px; border-radius: 6px; font-size: 0.875rem; font-family: inherit; cursor: pointer; transition: background 0.2s ease; } .btn-danger:hover { background: #c53030; } .empty-state { text-align: center; padding: 60px 20px; color: #718096; } .empty-state h3 { font-size: 1.25rem; margin-bottom: 8px; } @media (max-width: 768px) { .gallery-grid { grid-template-columns: 1fr; } .design-preview { height: 200px; } .header { flex-direction: column; gap: 16px; align-items: flex-start; } } </style> </head> <body> <div class="header"> <div class="header-content"> <h1>superdesign.gallery</h1> <p id="file-count">${designFiles.length} designs found</p> </div> <div class="header-controls"> <div class="refresh-controls"> <button id="refresh-btn" class="btn-secondary" onclick="refreshGallery()"> <span id="refresh-icon">🔄</span> <span id="refresh-text">Refresh</span> </button> <label class="auto-refresh-toggle"> <input type="checkbox" id="auto-refresh" onchange="toggleAutoRefresh()"> <span>Auto-refresh</span> </label> </div> <button class="theme-toggle" onclick="toggleTheme()"> <span id="theme-icon">🌙</span> <span id="theme-text">Dark</span> </button> </div> </div> <div class="gallery-grid"> ${designCards} </div> <script> // Theme management const theme = localStorage.getItem('theme') || 'light'; document.documentElement.setAttribute('data-theme', theme); updateThemeButton(); function toggleTheme() { const currentTheme = document.documentElement.getAttribute('data-theme'); const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; document.documentElement.setAttribute('data-theme', newTheme); localStorage.setItem('theme', newTheme); updateThemeButton(); } function updateThemeButton() { const theme = document.documentElement.getAttribute('data-theme'); const icon = document.getElementById('theme-icon'); const text = document.getElementById('theme-text'); if (theme === 'dark') { icon.textContent = '☀️'; text.textContent = 'Light'; } else { icon.textContent = '🌙'; text.textContent = 'Dark'; } } function openFullscreen(path) { window.open(path, '_blank'); } function copyPath(path) { navigator.clipboard.writeText(path).then(() => { showToast('Path copied to clipboard!', 'success'); }); } function deleteDesign(fileName) { if (confirm(\`Are you sure you want to delete \${fileName}?\`)) { // This would need to be implemented as an MCP tool // For now, just show a message showToast('Delete functionality coming soon!', 'info'); } } function showToast(message, type = 'info') { const colors = { success: '#48bb78', error: '#e53e3e', info: '#4299e1', warning: '#ed8936' }; const toast = document.createElement('div'); toast.textContent = message; toast.style.cssText = \` position: fixed; top: 20px; right: 20px; background: \${colors[type] || colors.info}; color: white; padding: 12px 20px; border-radius: 6px; z-index: 1000; font-size: 14px; font-family: inherit; animation: slideIn 0.3s ease; \`; document.body.appendChild(toast); setTimeout(() => { toast.style.animation = 'slideOut 0.3s ease'; setTimeout(() => toast.remove(), 300); }, 3000); } // File manifest for change detection // This manifest is generated by the MCP server and embedded in the gallery // In production, this could be updated via WebSocket or Server-Sent Events const fileManifest = \${JSON.stringify(fileManifest)}; // Integration status let mcpIntegrationAvailable = false; // Check if MCP integration is available (for future WebSocket/SSE integration) async function checkMcpIntegration() { try { // This is where we could check for WebSocket or SSE endpoints // For now, we'll use the static manifest approach mcpIntegrationAvailable = false; return mcpIntegrationAvailable; } catch (error) { console.log('MCP real-time integration not available, using static mode'); return false; } } let lastUpdateCheck = Date.now(); let autoRefreshInterval; let isRefreshing = false; // Smart refresh functionality function refreshGallery() { if (isRefreshing) return; isRefreshing = true; const refreshBtn = document.getElementById('refresh-btn'); const refreshIcon = document.getElementById('refresh-icon'); const refreshText = document.getElementById('refresh-text'); // Show loading state refreshBtn.disabled = true; refreshIcon.classList.add('refresh-indicator'); refreshText.textContent = 'Refreshing...'; // Check for file changes checkForFileChanges() .then(hasChanges => { if (hasChanges) { // Reload the page to get updated content location.reload(); } else { showToast('Gallery is up to date', 'info'); } }) .catch(error => { showToast('Error checking for updates', 'error'); console.error('Refresh error:', error); }) .finally(() => { // Reset loading state isRefreshing = false; refreshBtn.disabled = false; refreshIcon.classList.remove('refresh-indicator'); refreshText.textContent = 'Refresh'; }); } async function checkForFileChanges() { try { // For static gallery, we'll use a smart polling approach // In a real production environment, this would connect to the MCP server // via WebSocket or Server-Sent Events for real-time updates // Get current file stats from iframe document timestamps const currentFiles = await getCurrentFileStats(); let hasChanges = false; // Compare with manifest for (const currentFile of currentFiles) { const manifestFile = fileManifest.find(f => f.name === currentFile.name); if (!manifestFile) { // New file detected hasChanges = true; break; } // Use a more lenient comparison for iframe-based detection const timeDiff = Math.abs(currentFile.modified - manifestFile.modified); if (timeDiff > 1000) { // Allow 1 second tolerance // File potentially changed markFileAsUpdated(currentFile.name); hasChanges = true; } } // Check for deleted files const currentFileNames = new Set(currentFiles.map(f => f.name)); for (const manifestFile of fileManifest) { if (!currentFileNames.has(manifestFile.name)) { // File deleted hasChanges = true; break; } } return hasChanges; } catch (error) { console.error('Error checking file changes:', error); return false; } } async function getCurrentFileStats() { // Enhanced file detection using multiple methods const iframes = document.querySelectorAll('iframe[data-file]'); const files = []; for (const iframe of iframes) { const fileName = iframe.getAttribute('data-file'); if (fileName) { let lastModified = Date.now(); try { // Try to get file info from iframe document if (iframe.contentDocument?.lastModified) { lastModified = new Date(iframe.contentDocument.lastModified).getTime(); } // Alternative: check if iframe has loaded successfully const iframeLoaded = iframe.contentDocument && iframe.contentDocument.readyState === 'complete'; if (!iframeLoaded) { // File might be missing or corrupted lastModified = 0; } // Try to get estimated file size from content length let estimatedSize = 0; if (iframe.contentDocument?.documentElement) { estimatedSize = iframe.contentDocument.documentElement.outerHTML.length; } files.push({ name: fileName, modified: lastModified, size: estimatedSize }); } catch (error) { // Cross-origin or other access issues files.push({ name: fileName, modified: Date.now(), size: 0 }); } } } return files; } function toggleAutoRefresh() { const autoRefreshCheckbox = document.getElementById('auto-refresh'); if (autoRefreshCheckbox.checked) { // Start auto-refresh every 5 seconds autoRefreshInterval = setInterval(() => { if (!isRefreshing) { checkForFileChanges().then(hasChanges => { if (hasChanges) { showToast('Changes detected, refreshing...', 'info'); setTimeout(() => location.reload(), 1000); } }); } }, 5000); showToast('Auto-refresh enabled (5s interval)', 'success'); } else { // Stop auto-refresh if (autoRefreshInterval) { clearInterval(autoRefreshInterval); autoRefreshInterval = null; } showToast('Auto-refresh disabled', 'info'); } } // Enhanced file change detection with visual indicators function markFileAsUpdated(fileName) { const card = document.querySelector(\`[data-file="\${fileName}"]\`); if (card) { // Add updated indicator const indicator = document.createElement('div'); indicator.className = 'updated-indicator'; indicator.title = 'File updated'; card.style.position = 'relative'; card.appendChild(indicator); // Remove indicator after 5 seconds setTimeout(() => { indicator.remove(); }, 5000); } } // Initialize auto-refresh state from localStorage const autoRefreshEnabled = localStorage.getItem('autoRefresh') === 'true'; document.getElementById('auto-refresh').checked = autoRefreshEnabled; if (autoRefreshEnabled) { toggleAutoRefresh(); } // Save auto-refresh state document.getElementById('auto-refresh').addEventListener('change', () => { localStorage.setItem('autoRefresh', document.getElementById('auto-refresh').checked); }); // Add CSS animations const style = document.createElement('style'); style.textContent = \` @keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } @keyframes slideOut { from { transform: translateX(0); opacity: 1; } to { transform: translateX(100%); opacity: 0; } } \`; document.head.appendChild(style); </script> </body> </html>`; }

Other Tools

Related Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jonthebeef/superdesign-mcp-claude-code'

If you have feedback or need assistance with the MCP directory API, please join our Discord server