hatch-slidev-builder-mcp
Version:
A comprehensive MCP server for creating Slidev presentations with component library, interactive elements, and team collaboration features
736 lines (661 loc) • 23.7 kB
JavaScript
/**
* Generate Assets Tool - Universal asset creation for presentations
* Replaces generateChart and provides comprehensive asset generation capabilities
* Handles: charts, Python simulations, 3D elements, interactive widgets, audio, video, images
*/
import { z } from 'zod';
import path from 'path';
import fs from 'fs/promises';
import { exec } from 'child_process';
import { promisify } from 'util';
const execAsync = promisify(exec);
export const generateAssetsSchema = z.object({
assetType: z.enum(['chart', 'python-simulation', '3d-element', 'interactive-widget', 'audio', 'video', 'image']).describe('Type of asset to generate'),
content: z.string().describe('Content description or data for the asset'),
outputDir: z.string().describe('Output directory for the generated asset'),
specifications: z.object({
title: z.string().optional().describe('Title for the asset'),
dimensions: z.object({
width: z.number().default(800),
height: z.number().default(600)
}).optional(),
style: z.string().optional().default('modern').describe('Visual style (modern, corporate, minimal, etc.)'),
colors: z.array(z.string()).optional().describe('Color palette to use'),
format: z.string().optional().default('png').describe('Output format (png, svg, jpg, mp4, etc.)'),
interactive: z.boolean().default(false).describe('Whether the asset should be interactive'),
data: z.any().optional().describe('Raw data for charts or simulations'),
pythonLibraries: z.array(z.string()).optional().describe('Python libraries to use'),
threeJsComponents: z.array(z.string()).optional().describe('Three.js components to include'),
audioSettings: z.object({
duration: z.number().optional(),
tempo: z.number().optional(),
volume: z.number().optional()
}).optional()
}).describe('Detailed specifications for asset generation'),
brandGuidelines: z.string().optional().default('hatch-corporate').describe('Brand guidelines to follow'),
optimize: z.boolean().default(true).describe('Optimize asset for presentation use')
});
export async function generateAssets(args) {
try {
console.log(`🎨 Generating ${args.assetType} asset...`);
// Ensure output directory exists
await fs.mkdir(args.outputDir, { recursive: true });
let result;
switch (args.assetType) {
case 'chart':
result = await generateChart(args);
break;
case 'python-simulation':
result = await generatePythonSimulation(args);
break;
case '3d-element':
result = await generate3DElement(args);
break;
case 'interactive-widget':
result = await generateInteractiveWidget(args);
break;
case 'audio':
result = await generateAudio(args);
break;
case 'video':
result = await generateVideo(args);
break;
case 'image':
result = await generateImage(args);
break;
default:
throw new Error(`Unsupported asset type: ${args.assetType}`);
}
console.log(`✅ Asset generated successfully: ${result.filePath}`);
return {
success: true,
assetType: args.assetType,
filePath: result.filePath,
metadata: result.metadata,
usage: result.usage,
message: `${args.assetType} asset generated successfully`
};
}
catch (error) {
console.error(`❌ Error generating ${args.assetType}:`, error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error occurred',
assetType: args.assetType
};
}
}
/**
* Generate chart assets using Python/matplotlib or other charting libraries
*/
async function generateChart(args) {
const fileName = `chart_${Date.now()}.${args.specifications.format}`;
const filePath = path.join(args.outputDir, fileName);
// Generate Python script for chart creation
const pythonScript = generateChartPythonScript(args);
const scriptPath = path.join(args.outputDir, `generate_${Date.now()}.py`);
await fs.writeFile(scriptPath, pythonScript);
try {
// Execute Python script
await execAsync(`python "${scriptPath}"`);
// Clean up script file
await fs.unlink(scriptPath);
return {
filePath,
metadata: {
type: 'chart',
format: args.specifications.format,
dimensions: args.specifications.dimensions,
generated: new Date().toISOString()
},
usage: `Use in Slidev with: <img src="${fileName}" alt="${args.specifications.title || 'Chart'}" />`
};
}
catch (error) {
// Clean up script file on error
try {
await fs.unlink(scriptPath);
}
catch { }
throw error;
}
}
/**
* Generate Python simulation files
*/
async function generatePythonSimulation(args) {
const fileName = `simulation_${Date.now()}.py`;
const filePath = path.join(args.outputDir, fileName);
const simulationCode = generatePythonSimulationCode(args);
await fs.writeFile(filePath, simulationCode);
// Also create a Jupyter notebook version for development
const notebookPath = path.join(args.outputDir, `simulation_${Date.now()}.ipynb`);
const notebook = createJupyterNotebook(simulationCode, args.specifications.title || 'Simulation');
await fs.writeFile(notebookPath, JSON.stringify(notebook, null, 2));
return {
filePath,
metadata: {
type: 'python-simulation',
notebook: notebookPath,
libraries: args.specifications.pythonLibraries || ['numpy', 'matplotlib', 'pandas'],
generated: new Date().toISOString()
},
usage: `Run simulation with: python "${fileName}"`
};
}
/**
* Generate 3D elements using Three.js
*/
async function generate3DElement(args) {
const fileName = `3d_element_${Date.now()}.js`;
const filePath = path.join(args.outputDir, fileName);
const threeJsCode = generateThreeJsCode(args);
await fs.writeFile(filePath, threeJsCode);
// Create accompanying HTML file for testing
const htmlPath = path.join(args.outputDir, `3d_element_${Date.now()}.html`);
const htmlContent = createThreeJsHTML(fileName, args.specifications.title || '3D Element');
await fs.writeFile(htmlPath, htmlContent);
return {
filePath,
metadata: {
type: '3d-element',
htmlDemo: htmlPath,
components: args.specifications.threeJsComponents || ['scene', 'camera', 'renderer'],
generated: new Date().toISOString()
},
usage: `Import in Slidev component: import ThreeElement from './${fileName}'`
};
}
/**
* Generate interactive widgets
*/
async function generateInteractiveWidget(args) {
const fileName = `widget_${Date.now()}.vue`;
const filePath = path.join(args.outputDir, fileName);
const vueComponent = generateVueWidgetCode(args);
await fs.writeFile(filePath, vueComponent);
return {
filePath,
metadata: {
type: 'interactive-widget',
framework: 'vue',
interactive: true,
generated: new Date().toISOString()
},
usage: `Use in Slidev slide with: <${path.basename(fileName, '.vue')} />`
};
}
/**
* Generate audio assets
*/
async function generateAudio(args) {
const fileName = `audio_${Date.now()}.wav`;
const filePath = path.join(args.outputDir, fileName);
// Create placeholder audio generation script
const audioScript = generateAudioScript(args);
const scriptPath = path.join(args.outputDir, `generate_audio_${Date.now()}.py`);
await fs.writeFile(scriptPath, audioScript);
return {
filePath,
metadata: {
type: 'audio',
duration: args.specifications.audioSettings?.duration || 10,
format: 'wav',
generated: new Date().toISOString()
},
usage: `Add to slide with: <audio src="${fileName}" controls />`
};
}
/**
* Generate video assets
*/
async function generateVideo(args) {
const fileName = `video_${Date.now()}.mp4`;
const filePath = path.join(args.outputDir, fileName);
// Create video generation script
const videoScript = generateVideoScript(args);
const scriptPath = path.join(args.outputDir, `generate_video_${Date.now()}.py`);
await fs.writeFile(scriptPath, videoScript);
return {
filePath,
metadata: {
type: 'video',
format: 'mp4',
dimensions: args.specifications.dimensions,
generated: new Date().toISOString()
},
usage: `Embed in slide with: <video src="${fileName}" controls />`
};
}
/**
* Generate image assets
*/
async function generateImage(args) {
const fileName = `image_${Date.now()}.${args.specifications.format}`;
const filePath = path.join(args.outputDir, fileName);
// Create image generation script (placeholder for AI image generation)
const imageScript = generateImageScript(args);
const scriptPath = path.join(args.outputDir, `generate_image_${Date.now()}.py`);
await fs.writeFile(scriptPath, imageScript);
return {
filePath,
metadata: {
type: 'image',
format: args.specifications.format,
dimensions: args.specifications.dimensions,
generated: new Date().toISOString()
},
usage: `Use in slide with: <img src="${fileName}" alt="${args.specifications.title || 'Generated Image'}" />`
};
}
// Helper functions for code generation
function generateChartPythonScript(args) {
const colors = args.specifications.colors || ['#095078', '#E84B37', '#ACBCC8'];
const title = args.specifications.title || 'Chart';
const dimensions = args.specifications.dimensions || { width: 800, height: 600 };
return `import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
# Set up the plot
plt.figure(figsize=(${dimensions.width / 100}, ${dimensions.height / 100}))
plt.style.use('seaborn-v0_8' if 'seaborn-v0_8' in plt.style.available else 'default')
# Chart data (based on content: ${args.content})
# TODO: Parse actual data from content
x = np.linspace(0, 10, 100)
y = np.sin(x)
# Create the chart
plt.plot(x, y, color='${colors[0]}', linewidth=2)
plt.title('${title}', fontsize=16, fontweight='bold', color='#425563')
plt.xlabel('X Axis', fontsize=12, color='#595959')
plt.ylabel('Y Axis', fontsize=12, color='#595959')
plt.grid(True, alpha=0.3)
# Apply brand colors
plt.gca().spines['top'].set_color('${colors[1]}')
plt.gca().spines['right'].set_color('${colors[1]}')
plt.gca().spines['bottom'].set_color('#425563')
plt.gca().spines['left'].set_color('#425563')
# Save the chart
plt.tight_layout()
plt.savefig('${path.join(args.outputDir, `chart_${Date.now()}.${args.specifications.format}`)}',
dpi=300, bbox_inches='tight', facecolor='white')
plt.close()
print("Chart generated successfully!")
`;
}
function generatePythonSimulationCode(args) {
const title = args.specifications.title || 'Simulation';
const libraries = args.specifications.pythonLibraries || ['numpy', 'matplotlib', 'pandas'];
return `"""
${title}
Generated for: ${args.content}
"""
${libraries.map(lib => `import ${lib.split('/')[0]} as ${lib.split('/')[0].substring(0, 3)}`).join('\n')}
import matplotlib.pyplot as plt
import numpy as np
class ${title.replace(/\s+/g, '')}Simulation:
def __init__(self):
self.data = []
self.results = {}
def generate_data(self, n_points=1000):
"""Generate simulation data"""
# TODO: Implement actual simulation logic based on: ${args.content}
self.data = np.random.normal(0, 1, n_points)
return self.data
def run_simulation(self):
"""Run the main simulation"""
data = self.generate_data()
self.results = {
'mean': np.mean(data),
'std': np.std(data),
'min': np.min(data),
'max': np.max(data)
}
return self.results
def visualize(self):
"""Create visualization of results"""
plt.figure(figsize=(10, 6))
plt.hist(self.data, bins=50, alpha=0.7, color='#095078')
plt.title('${title} Results')
plt.xlabel('Value')
plt.ylabel('Frequency')
plt.grid(True, alpha=0.3)
plt.show()
def export_results(self, filename='simulation_results.csv'):
"""Export results to CSV"""
import pandas as pd
df = pd.DataFrame({'data': self.data})
df.to_csv(filename, index=False)
print(f"Results exported to {filename}")
if __name__ == "__main__":
sim = ${title.replace(/\s+/g, '')}Simulation()
results = sim.run_simulation()
print(f"Simulation Results: {results}")
sim.visualize()
`;
}
function generateThreeJsCode(args) {
const title = args.specifications.title || '3D Element';
return `/**
* ${title}
* Generated for: ${args.content}
*/
import * as THREE from 'three';
export class ${title.replace(/\s+/g, '')}3D {
constructor(container) {
this.container = container;
this.scene = new THREE.Scene();
this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
this.renderer = new THREE.WebGLRenderer({ antialias: true });
this.init();
}
init() {
// Set up renderer
this.renderer.setSize(this.container.clientWidth, this.container.clientHeight);
this.renderer.setClearColor(0xf0f0f0);
this.container.appendChild(this.renderer.domElement);
// Add lights
const ambientLight = new THREE.AmbientLight(0x404040, 0.6);
this.scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(10, 10, 5);
this.scene.add(directionalLight);
// Create 3D object (example: rotating cube with brand colors)
const geometry = new THREE.BoxGeometry(2, 2, 2);
const material = new THREE.MeshPhongMaterial({
color: 0x095078, // Hatch primary blue
transparent: true,
opacity: 0.8
});
this.cube = new THREE.Mesh(geometry, material);
this.scene.add(this.cube);
// Position camera
this.camera.position.z = 5;
// Start animation
this.animate();
}
animate() {
requestAnimationFrame(() => this.animate());
// Rotate the cube
this.cube.rotation.x += 0.01;
this.cube.rotation.y += 0.01;
this.renderer.render(this.scene, this.camera);
}
resize() {
const width = this.container.clientWidth;
const height = this.container.clientHeight;
this.camera.aspect = width / height;
this.camera.updateProjectionMatrix();
this.renderer.setSize(width, height);
}
}
// Usage in Slidev
export default function create${title.replace(/\s+/g, '')}(container) {
return new ${title.replace(/\s+/g, '')}3D(container);
}
`;
}
function generateVueWidgetCode(args) {
const title = args.specifications.title || 'Interactive Widget';
return `<template>
<div class="interactive-widget">
<h3 class="widget-title">${title}</h3>
<div class="widget-content">
<p class="widget-description">${args.content}</p>
<!-- Interactive Controls -->
<div class="controls">
<label>
Value: {{ value }}
<input
type="range"
v-model="value"
min="0"
max="100"
class="slider"
/>
</label>
</div>
<!-- Results Display -->
<div class="results">
<div class="result-item">
<span class="label">Current Value:</span>
<span class="value">{{ value }}</span>
</div>
<div class="result-item">
<span class="label">Calculated Result:</span>
<span class="value">{{ calculatedResult }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const value = ref(50)
const calculatedResult = computed(() => {
// Example calculation - customize based on widget purpose
return (value.value * 1.5).toFixed(2)
})
</script>
<style scoped>
.interactive-widget {
background: white;
border-radius: 8px;
padding: 1.5rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border-left: 4px solid #095078;
max-width: 400px;
margin: 1rem auto;
}
.widget-title {
color: #095078;
font-size: 1.2rem;
font-weight: bold;
margin-bottom: 1rem;
text-align: center;
}
.widget-description {
color: #425563;
font-size: 0.9rem;
margin-bottom: 1rem;
line-height: 1.4;
}
.controls {
margin-bottom: 1rem;
}
.controls label {
display: block;
color: #595959;
font-weight: 500;
margin-bottom: 0.5rem;
}
.slider {
width: 100%;
margin-top: 0.5rem;
accent-color: #E84B37;
}
.results {
border-top: 1px solid #ACBCC8;
padding-top: 1rem;
}
.result-item {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.label {
color: #595959;
font-weight: 500;
}
.value {
color: #095078;
font-weight: bold;
}
</style>
`;
}
function generateAudioScript(args) {
return `"""
Audio Generation Script
Generated for: ${args.content}
"""
# Placeholder for audio generation
# Install required libraries: pip install pydub numpy
import numpy as np
from pydub import AudioSegment
from pydub.generators import Sine
def generate_audio():
duration = ${args.specifications.audioSettings?.duration || 10} * 1000 # milliseconds
# Generate simple tone (replace with actual audio generation)
tone = Sine(440).to_audio_segment(duration=duration)
# Export audio
output_path = "${path.join(args.outputDir, `audio_${Date.now()}.wav`)}"
tone.export(output_path, format="wav")
print(f"Audio generated: {output_path}")
if __name__ == "__main__":
generate_audio()
`;
}
function generateVideoScript(args) {
return `"""
Video Generation Script
Generated for: ${args.content}
"""
# Placeholder for video generation
# Install required libraries: pip install opencv-python numpy
import cv2
import numpy as np
def generate_video():
width = ${args.specifications.dimensions?.width || 800}
height = ${args.specifications.dimensions?.height || 600}
fps = 30
duration = 5 # seconds
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
output_path = "${path.join(args.outputDir, `video_${Date.now()}.mp4`)}"
out = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
for frame in range(fps * duration):
# Create simple animated frame (replace with actual content)
img = np.zeros((height, width, 3), dtype=np.uint8)
# Add some animation
center = (width // 2, height // 2)
radius = int(50 + 30 * np.sin(frame * 0.1))
cv2.circle(img, center, radius, (9, 80, 120), -1) # Hatch blue
out.write(img)
out.release()
print(f"Video generated: {output_path}")
if __name__ == "__main__":
generate_video()
`;
}
function generateImageScript(args) {
return `"""
Image Generation Script
Generated for: ${args.content}
"""
# Placeholder for image generation
# Install required libraries: pip install Pillow numpy
from PIL import Image, ImageDraw, ImageFont
import numpy as np
def generate_image():
width = ${args.specifications.dimensions?.width || 800}
height = ${args.specifications.dimensions?.height || 600}
# Create image with brand colors
img = Image.new('RGB', (width, height), color=(172, 188, 200)) # Hatch secondary blue
draw = ImageDraw.Draw(img)
# Add content (replace with actual image generation)
title = "${args.specifications.title || 'Generated Image'}"
try:
font = ImageFont.truetype("arial.ttf", 48)
except:
font = ImageFont.load_default()
# Calculate text position
bbox = draw.textbbox((0, 0), title, font=font)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
x = (width - text_width) // 2
y = (height - text_height) // 2
# Draw text
draw.text((x, y), title, fill=(9, 80, 120), font=font) # Hatch primary blue
# Save image
output_path = "${path.join(args.outputDir, `image_${Date.now()}.${args.specifications.format}`)}"
img.save(output_path, format='${args.specifications.format?.toUpperCase()}')
print(f"Image generated: {output_path}")
if __name__ == "__main__":
generate_image()
`;
}
function createJupyterNotebook(code, title) {
return {
cells: [
{
cell_type: "markdown",
metadata: {},
source: [`# ${title}\n`, `\n`, `Generated simulation code for interactive development.`]
},
{
cell_type: "code",
execution_count: null,
metadata: {},
outputs: [],
source: code.split('\n')
}
],
metadata: {
kernelspec: {
display_name: "Python 3",
language: "python",
name: "python3"
},
language_info: {
name: "python",
version: "3.8.0"
}
},
nbformat: 4,
nbformat_minor: 4
};
}
function createThreeJsHTML(scriptFile, title) {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${title} - 3D Demo</title>
<style>
body {
margin: 0;
padding: 20px;
font-family: Arial, sans-serif;
background: #f0f0f0;
}
#container {
width: 800px;
height: 600px;
margin: 0 auto;
border: 2px solid #095078;
border-radius: 8px;
overflow: hidden;
}
h1 {
text-align: center;
color: #095078;
}
</style>
</head>
<body>
<h1>${title}</h1>
<div id="container"></div>
<script type="module">
import create3DElement from './${scriptFile}';
const container = document.getElementById('container');
const element = create3DElement(container);
// Handle window resize
window.addEventListener('resize', () => {
if (element.resize) {
element.resize();
}
});
</script>
</body>
</html>`;
}