hatch-slidev-builder-mcp
Version:
A comprehensive MCP server for creating Slidev presentations with component library, interactive elements, and team collaboration features
342 lines (307 loc) • 10.4 kB
JavaScript
import * as fs from 'fs-extra';
import * as path from 'path';
export async function createComponent(args) {
const { name, category, author, description, scope, parameters = [] } = args;
try {
// Create component directory structure
const componentDir = path.join(__dirname, '..', 'components', scope, name);
await fs.ensureDir(componentDir);
// Generate component.vue file
const vueComponent = generateVueComponent(name, parameters);
await fs.writeFile(path.join(componentDir, 'component.vue'), vueComponent);
// Generate config.json
const config = {
name: `${scope}/${name}`,
version: '1.0.0',
author,
description,
category,
tags: [category, scope],
parameters: parameters.reduce((acc, param) => {
acc[param.name] = {
type: param.type,
required: param.required,
default: param.default,
options: param.options,
description: param.description
};
return acc;
}, {}),
dependencies: ['vue-3'],
created: new Date().toISOString(),
examples: [`${name}-basic.md`]
};
await fs.writeFile(path.join(componentDir, 'config.json'), JSON.stringify(config, null, 2));
// Generate styles.css
const styles = generateComponentStyles(name, scope);
await fs.writeFile(path.join(componentDir, 'styles.css'), styles);
// Generate example file
const example = generateExampleFile(name, parameters, scope);
await fs.writeFile(path.join(componentDir, `${name}-basic.md`), example);
// Generate package.json
const packageJson = {
name: `@${scope}/slidev-component-${name}`,
version: '1.0.0',
description,
author,
keywords: ['slidev', 'component', scope, category],
peerDependencies: {
'vue': '^3.0.0',
'@slidev/client': '^0.47.0'
}
};
await fs.writeFile(path.join(componentDir, 'package.json'), JSON.stringify(packageJson, null, 2));
// Update component registry
await updateComponentRegistry(scope, name, config);
return {
content: [
{
type: 'text',
text: `✅ Successfully created component: ${scope}/${name}\n\n` +
`📁 Component Directory: ${componentDir}\n` +
`📂 Category: ${category}\n` +
`👤 Author: ${author}\n` +
`🔧 Parameters: ${parameters.length}\n` +
`📄 Files Created:\n` +
` • component.vue - Vue component\n` +
` • config.json - Component metadata\n` +
` • styles.css - Component styles\n` +
` • ${name}-basic.md - Usage example\n` +
` • package.json - Package configuration\n\n` +
`🚀 Next Steps:\n` +
`1. Edit component.vue to implement your component\n` +
`2. Customize styles.css for visual design\n` +
`3. Test with: add_component --name=${scope}/${name}\n` +
`4. Publish with: publish_component --path=${componentDir}`
}
]
};
}
catch (error) {
throw new Error(`Failed to create component: ${error instanceof Error ? error.message : String(error)}`);
}
}
function generateVueComponent(name, parameters) {
const props = parameters.map(param => {
let propDef = `${param.name}: {\n type: ${getVueType(param.type)},\n required: ${param.required}`;
if (param.default !== undefined) {
propDef += `,\n default: ${JSON.stringify(param.default)}`;
}
propDef += '\n }';
return propDef;
}).join(',\n ');
const paramBindings = parameters.map(p => p.name).join(', ');
return `<template>
<div class="${name}-component">
<div class="component-header">
<h3 class="component-title">{{ title || '${name.charAt(0).toUpperCase() + name.slice(1)} Component' }}</h3>
</div>
<div class="component-content">
<!-- TODO: Implement your component logic here -->
<div class="component-placeholder">
<p>🚧 Component Implementation Needed</p>
${parameters.length > 0 ? `<div class="component-params">
<h4>Available Parameters:</h4>
<ul>
${parameters.map(p => ` <li><strong>${p.name}</strong>: {{ ${p.name} }}</li>`).join('\n')}
</ul>
</div>` : ''}
</div>
</div>
<div class="component-footer">
<slot name="footer"></slot>
</div>
</div>
</template>
<script setup lang="ts">
import { defineProps } from 'vue'
// Component props based on parameters
const props = defineProps({
title: {
type: String,
required: false
},${props ? '\n ' + props : ''}
})
// TODO: Add your component logic here
// Example:
// const computedValue = computed(() => {
// return someCalculation(${paramBindings})
// })
</script>
<style scoped>
.${name}-component {
/* Import component-specific styles */
@import './styles.css';
}
.component-header {
margin-bottom: 1rem;
}
.component-title {
color: var(--hatch-primary, #00A651);
font-weight: 600;
margin: 0;
}
.component-content {
flex: 1;
}
.component-placeholder {
padding: 2rem;
text-align: center;
border: 2px dashed var(--hatch-gray, #CCCCCC);
border-radius: 8px;
background-color: var(--hatch-gray-light, #F5F5F5);
}
.component-params {
margin-top: 1rem;
text-align: left;
}
.component-params h4 {
color: var(--hatch-secondary, #004225);
margin-bottom: 0.5rem;
}
.component-params ul {
list-style: none;
padding: 0;
}
.component-params li {
margin-bottom: 0.25rem;
font-family: monospace;
font-size: 0.9rem;
}
</style>`;
}
function generateComponentStyles(name, scope) {
return `/* ${scope}/${name} Component Styles */
.${name}-component {
/* Base component styling */
display: flex;
flex-direction: column;
border-radius: 8px;
overflow: hidden;
/* Hatch brand integration */
border: 1px solid var(--hatch-gray, #CCCCCC);
background: var(--hatch-bg, #FFFFFF);
color: var(--hatch-text, #333333);
/* Animation */
transition: all 0.2s ease;
}
.${name}-component:hover {
box-shadow: 0 4px 12px rgba(0, 166, 81, 0.1);
border-color: var(--hatch-primary, #00A651);
}
/* Component-specific styles - customize these */
.${name}-content {
padding: 1rem;
}
.${name}-interactive {
cursor: pointer;
transition: transform 0.2s ease;
}
.${name}-interactive:hover {
transform: translateY(-1px);
}
/* Responsive design */
@media (max-width: 768px) {
.${name}-component {
margin: 0.5rem 0;
}
}
/* Dark theme support */
@media (prefers-color-scheme: dark) {
.${name}-component {
background: var(--hatch-dark-bg, #1a1a1a);
color: var(--hatch-dark-text, #e5e5e5);
}
}`;
}
function generateExampleFile(name, parameters, scope) {
const exampleParams = parameters.map(param => {
let value;
switch (param.type) {
case 'string':
value = param.options ? `"${param.options[0]}"` : `"Example ${param.name}"`;
break;
case 'number':
value = param.default || 42;
break;
case 'boolean':
value = param.default || true;
break;
case 'enum':
value = param.options ? `"${param.options[0]}"` : '"option1"';
break;
case 'array':
value = '[1, 2, 3]';
break;
default:
value = '{}';
}
return ` ${param.name}: ${value}`;
}).join(',\n');
return `# ${name} Component Example
This example shows how to use the \`${scope}/${name}\` component in your presentations.
## Basic Usage
\`\`\`javascript
// Add the component to a slide
await mcp.callTool('add_component', {
deckPath: './my-presentation',
slideNumber: 2,
componentName: '${scope}/${name}',
parameters: {
${exampleParams}
},
position: { x: 100, y: 100 }
});
\`\`\`
## Component Parameters
${parameters.map(param => `- **${param.name}** (${param.type}${param.required ? ', required' : ', optional'}): ${param.description || 'Parameter description'}`).join('\n')}
## Advanced Usage
\`\`\`javascript
// Using with dynamic data
await mcp.callTool('add_component', {
componentName: '${scope}/${name}',
parameters: {
${exampleParams},
// Add any additional customization
},
styling: {
width: 800,
height: 400
}
});
\`\`\`
## Tips
- This component integrates seamlessly with Hatch themes
- Use \`list_components --search="${name}"\` to find related components
- Combine with other components for complex layouts
## Next Steps
1. Customize the component parameters for your use case
2. Try different styling options
3. Combine with other ${scope} components
4. Share feedback with the component author
`;
}
function getVueType(paramType) {
switch (paramType) {
case 'string': return 'String';
case 'number': return 'Number';
case 'boolean': return 'Boolean';
case 'array': return 'Array';
case 'object': return 'Object';
default: return 'String';
}
}
async function updateComponentRegistry(scope, name, config) {
const registryPath = path.join(__dirname, '..', 'components', 'registry.json');
let registry;
try {
const registryContent = await fs.readFile(registryPath, 'utf-8');
registry = JSON.parse(registryContent);
}
catch (error) {
// Create new registry if it doesn't exist
registry = { components: {} };
}
registry.components[`${scope}/${name}`] = config;
await fs.writeFile(registryPath, JSON.stringify(registry, null, 2));
}