Skip to main content
Glama

McFlow

generator.ts24.9 kB
import path from 'path'; import fs from 'fs/promises'; import { execSync } from 'child_process'; export interface AppConfig { name: string; projectPath: string; features?: { dashboard?: boolean; api?: boolean; database?: boolean; webhooks?: boolean; approvals?: boolean; }; stages?: string[]; } export class AppGenerator { constructor(private projectPath: string) {} /** * Generate a Next.js app for workflow data management */ async generateApp(config: AppConfig): Promise<void> { const appPath = path.join(this.projectPath, config.name); // Check if app already exists try { await fs.access(appPath); throw new Error(`App directory ${config.name} already exists`); } catch (error: any) { if (error.code !== 'ENOENT') throw error; } // Create app structure await this.createAppStructure(appPath); // Generate files based on features await this.generatePackageJson(appPath, config); await this.generateDatabaseSchema(appPath); await this.generateApiEndpoints(appPath, config); await this.generateDashboard(appPath, config); await this.generateComponents(appPath, config); await this.generateStyles(appPath); await this.generateEnvFile(appPath); // Initialize git ignore await this.generateGitIgnore(appPath); } private async createAppStructure(appPath: string): Promise<void> { const dirs = [ 'app', 'app/api', 'app/api/webhook', 'app/api/workflow', 'app/dashboard', 'app/items', 'components', 'lib', 'styles', 'public', 'data' ]; for (const dir of dirs) { await fs.mkdir(path.join(appPath, dir), { recursive: true }); } } private async generatePackageJson(appPath: string, config: AppConfig): Promise<void> { const packageJson = { name: config.name, version: '0.1.0', private: true, scripts: { dev: 'next dev', build: 'next build', start: 'next start', 'db:init': 'node scripts/init-db.js' }, dependencies: { next: '14.2.0', react: '^18', 'react-dom': '^18', sqlite3: '^5.1.6', sqlite: '^5.0.0', axios: '^1.6.0', 'date-fns': '^3.0.0', clsx: '^2.0.0' }, devDependencies: { '@types/node': '^20', '@types/react': '^18', '@types/react-dom': '^18', typescript: '^5', tailwindcss: '^3.4.0', autoprefixer: '^10.0.0', postcss: '^8' } }; await fs.writeFile( path.join(appPath, 'package.json'), JSON.stringify(packageJson, null, 2) ); } private async generateDatabaseSchema(appPath: string): Promise<void> { const dbSetup = `import { Database } from 'sqlite'; import sqlite3 from 'sqlite3'; import { open } from 'sqlite'; import path from 'path'; let db: Database | null = null; export async function getDb() { if (!db) { db = await open({ filename: path.join(process.cwd(), 'data', 'workflow.db'), driver: sqlite3.Database }); // Initialize tables await db.exec(\` CREATE TABLE IF NOT EXISTS workflow_items ( id TEXT PRIMARY KEY, status TEXT DEFAULT 'pending', stage TEXT, data JSON, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS workflow_executions ( id TEXT PRIMARY KEY, workflow_id TEXT, workflow_name TEXT, item_id TEXT, status TEXT, started_at DATETIME DEFAULT CURRENT_TIMESTAMP, ended_at DATETIME, metadata JSON, FOREIGN KEY (item_id) REFERENCES workflow_items(id) ); CREATE TABLE IF NOT EXISTS node_executions ( id INTEGER PRIMARY KEY AUTOINCREMENT, execution_id TEXT, node_id TEXT, node_type TEXT, input JSON, output JSON, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (execution_id) REFERENCES workflow_executions(id) ); CREATE TABLE IF NOT EXISTS workflow_checkpoints ( id INTEGER PRIMARY KEY AUTOINCREMENT, item_id TEXT, checkpoint_name TEXT, node_id TEXT, checkpoint_data JSON, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE(item_id, checkpoint_name) ); CREATE TABLE IF NOT EXISTS workflow_errors ( id INTEGER PRIMARY KEY AUTOINCREMENT, execution_id TEXT, error_message TEXT, error_details JSON, node_id TEXT, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_items_status ON workflow_items(status); CREATE INDEX IF NOT EXISTS idx_items_stage ON workflow_items(stage); CREATE INDEX IF NOT EXISTS idx_executions_item ON workflow_executions(item_id); CREATE INDEX IF NOT EXISTS idx_checkpoints_item ON workflow_checkpoints(item_id); \`); } return db; } export async function createItem(id: string, data: any, stage?: string) { const db = await getDb(); await db.run( 'INSERT INTO workflow_items (id, data, stage) VALUES (?, ?, ?)', [id, JSON.stringify(data), stage || 'created'] ); } export async function updateItem(id: string, updates: any) { const db = await getDb(); const current = await db.get('SELECT data FROM workflow_items WHERE id = ?', id); const merged = { ...JSON.parse(current.data), ...updates }; await db.run( 'UPDATE workflow_items SET data = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [JSON.stringify(merged), id] ); } export async function getItems(filter?: { status?: string; stage?: string }) { const db = await getDb(); let query = 'SELECT * FROM workflow_items WHERE 1=1'; const params: any[] = []; if (filter?.status) { query += ' AND status = ?'; params.push(filter.status); } if (filter?.stage) { query += ' AND stage = ?'; params.push(filter.stage); } query += ' ORDER BY created_at DESC'; return await db.all(query, params); } export async function saveCheckpoint(itemId: string, checkpointName: string, data: any, nodeId?: string) { const db = await getDb(); await db.run( \`INSERT OR REPLACE INTO workflow_checkpoints (item_id, checkpoint_name, checkpoint_data, node_id) VALUES (?, ?, ?, ?)\`, [itemId, checkpointName, JSON.stringify(data), nodeId] ); } export async function getCheckpoint(itemId: string, checkpointName: string) { const db = await getDb(); return await db.get( 'SELECT * FROM workflow_checkpoints WHERE item_id = ? AND checkpoint_name = ?', [itemId, checkpointName] ); } `; await fs.writeFile(path.join(appPath, 'lib', 'db.ts'), dbSetup); } private async generateApiEndpoints(appPath: string, config: AppConfig): Promise<void> { // Webhook receiver endpoint const webhookEndpoint = `import { NextRequest, NextResponse } from 'next/server'; import { createItem, updateItem } from '@/lib/db'; export async function POST(req: NextRequest) { try { const body = await req.json(); const { action, itemId, data } = body; switch (action) { case 'create_item': await createItem(itemId, data); return NextResponse.json({ success: true, itemId }); case 'update_item': await updateItem(itemId, data); return NextResponse.json({ success: true, itemId }); default: return NextResponse.json({ error: 'Unknown action' }, { status: 400 }); } } catch (error: any) { return NextResponse.json({ error: error.message }, { status: 500 }); } } `; await fs.writeFile( path.join(appPath, 'app', 'api', 'webhook', 'receive', 'route.ts'), webhookEndpoint ); // Workflow storage endpoint const workflowStorage = `import { NextRequest, NextResponse } from 'next/server'; import { getDb, saveCheckpoint } from '@/lib/db'; export async function POST(req: NextRequest) { try { const body = await req.json(); const { action, executionId, itemId, ...data } = body; const db = await getDb(); switch (action) { case 'start_execution': await db.run( \`INSERT INTO workflow_executions (id, workflow_id, workflow_name, item_id, status, metadata) VALUES (?, ?, ?, ?, 'running', ?)\`, [executionId || data.executionId, data.workflowId, data.workflowName, itemId, JSON.stringify(data.metadata)] ); return NextResponse.json({ success: true, executionId }); case 'end_execution': await db.run( \`UPDATE workflow_executions SET status = ?, ended_at = CURRENT_TIMESTAMP WHERE id = ?\`, [data.status || 'completed', executionId] ); return NextResponse.json({ success: true, executionId }); case 'store_node': await db.run( \`INSERT INTO node_executions (execution_id, node_id, node_type, input, output) VALUES (?, ?, ?, ?, ?)\`, [executionId, data.nodeId, data.nodeType, JSON.stringify(data.input), JSON.stringify(data.output)] ); return NextResponse.json({ success: true }); case 'save_checkpoint': await saveCheckpoint(itemId, data.checkpointName, data.checkpointData, data.nodeId); return NextResponse.json({ success: true }); case 'track_error': await db.run( \`INSERT INTO workflow_errors (execution_id, error_message, error_details, node_id) VALUES (?, ?, ?, ?)\`, [executionId, data.errorMessage, JSON.stringify(data.errorDetails), data.nodeId] ); return NextResponse.json({ success: true }); default: return NextResponse.json({ error: 'Unknown action' }, { status: 400 }); } } catch (error: any) { console.error('Storage error:', error); return NextResponse.json({ error: error.message }, { status: 500 }); } } `; await fs.writeFile( path.join(appPath, 'app', 'api', 'workflow', 'store', 'route.ts'), workflowStorage ); // Workflow retrieval endpoint const workflowRetrieve = `import { NextRequest, NextResponse } from 'next/server'; import { getCheckpoint, getDb } from '@/lib/db'; export async function GET(req: NextRequest) { try { const { searchParams } = new URL(req.url); const action = searchParams.get('action'); const itemId = searchParams.get('itemId'); const checkpointName = searchParams.get('checkpointName'); switch (action) { case 'get_checkpoint': if (!itemId || !checkpointName) { return NextResponse.json({ error: 'Missing parameters' }, { status: 400 }); } const checkpoint = await getCheckpoint(itemId, checkpointName); return NextResponse.json({ checkpointData: checkpoint ? JSON.parse(checkpoint.checkpoint_data) : null }); case 'get_workflow_history': if (!itemId) { return NextResponse.json({ error: 'Missing itemId' }, { status: 400 }); } const db = await getDb(); const executions = await db.all( 'SELECT * FROM workflow_executions WHERE item_id = ? ORDER BY started_at DESC', itemId ); return NextResponse.json({ executions }); default: return NextResponse.json({ error: 'Unknown action' }, { status: 400 }); } } catch (error: any) { return NextResponse.json({ error: error.message }, { status: 500 }); } } `; await fs.writeFile( path.join(appPath, 'app', 'api', 'workflow', 'retrieve', 'route.ts'), workflowRetrieve ); } private async generateDashboard(appPath: string, config: AppConfig): Promise<void> { const dashboardPage = `'use client'; import { useState, useEffect } from 'react'; import StatsCard from '@/components/StatsCard'; import ItemsTable from '@/components/ItemsTable'; import PipelineView from '@/components/PipelineView'; export default function Dashboard() { const [items, setItems] = useState([]); const [stats, setStats] = useState({ total: 0, pending: 0, processing: 0, completed: 0, failed: 0 }); const [view, setView] = useState<'table' | 'pipeline'>('table'); useEffect(() => { fetchItems(); const interval = setInterval(fetchItems, 5000); // Poll every 5 seconds return () => clearInterval(interval); }, []); const fetchItems = async () => { try { const response = await fetch('/api/items'); const data = await response.json(); setItems(data.items); // Calculate stats const newStats = { total: data.items.length, pending: data.items.filter((i: any) => i.status === 'pending').length, processing: data.items.filter((i: any) => i.status === 'processing').length, completed: data.items.filter((i: any) => i.status === 'completed').length, failed: data.items.filter((i: any) => i.status === 'failed').length }; setStats(newStats); } catch (error) { console.error('Failed to fetch items:', error); } }; return ( <div className="p-6"> <div className="mb-6"> <h1 className="text-3xl font-bold">Workflow Dashboard</h1> <p className="text-gray-600 mt-2">Monitor and manage workflow items</p> </div> {/* Stats Cards */} <div className="grid grid-cols-1 md:grid-cols-5 gap-4 mb-6"> <StatsCard title="Total Items" value={stats.total} color="blue" /> <StatsCard title="Pending" value={stats.pending} color="yellow" /> <StatsCard title="Processing" value={stats.processing} color="orange" /> <StatsCard title="Completed" value={stats.completed} color="green" /> <StatsCard title="Failed" value={stats.failed} color="red" /> </div> {/* View Toggle */} <div className="mb-4 flex gap-2"> <button onClick={() => setView('table')} className={\`px-4 py-2 rounded \${ view === 'table' ? 'bg-blue-500 text-white' : 'bg-gray-200 text-gray-700' }\`} > Table View </button> <button onClick={() => setView('pipeline')} className={\`px-4 py-2 rounded \${ view === 'pipeline' ? 'bg-blue-500 text-white' : 'bg-gray-200 text-gray-700' }\`} > Pipeline View </button> </div> {/* Content */} {view === 'table' ? ( <ItemsTable items={items} onRefresh={fetchItems} /> ) : ( <PipelineView items={items} stages={${JSON.stringify(config.stages || ['created', 'processing', 'review', 'completed'])}} /> )} </div> ); } `; await fs.writeFile( path.join(appPath, 'app', 'dashboard', 'page.tsx'), dashboardPage ); // Items API endpoint const itemsApi = `import { NextResponse } from 'next/server'; import { getItems } from '@/lib/db'; export async function GET() { try { const items = await getItems(); return NextResponse.json({ items }); } catch (error: any) { return NextResponse.json({ error: error.message }, { status: 500 }); } } `; await fs.writeFile( path.join(appPath, 'app', 'api', 'items', 'route.ts'), itemsApi ); } private async generateComponents(appPath: string, config: AppConfig): Promise<void> { // Stats Card Component const statsCard = `interface StatsCardProps { title: string; value: number; color: 'blue' | 'yellow' | 'orange' | 'green' | 'red'; } export default function StatsCard({ title, value, color }: StatsCardProps) { const colorClasses = { blue: 'bg-blue-100 text-blue-800 border-blue-200', yellow: 'bg-yellow-100 text-yellow-800 border-yellow-200', orange: 'bg-orange-100 text-orange-800 border-orange-200', green: 'bg-green-100 text-green-800 border-green-200', red: 'bg-red-100 text-red-800 border-red-200' }; return ( <div className={\`p-4 rounded-lg border-2 \${colorClasses[color]}\`}> <h3 className="text-sm font-medium opacity-75">{title}</h3> <p className="text-2xl font-bold mt-1">{value}</p> </div> ); } `; await fs.writeFile(path.join(appPath, 'components', 'StatsCard.tsx'), statsCard); // Items Table Component const itemsTable = `import { useState } from 'react'; import Link from 'next/link'; interface Item { id: string; status: string; stage: string; data: string; created_at: string; updated_at: string; } interface ItemsTableProps { items: Item[]; onRefresh: () => void; } export default function ItemsTable({ items, onRefresh }: ItemsTableProps) { const [filter, setFilter] = useState<string>('all'); const filteredItems = filter === 'all' ? items : items.filter(item => item.status === filter); const getStatusBadge = (status: string) => { const colors = { pending: 'bg-yellow-100 text-yellow-800', processing: 'bg-blue-100 text-blue-800', completed: 'bg-green-100 text-green-800', failed: 'bg-red-100 text-red-800' }; return ( <span className={\`px-2 py-1 rounded text-xs font-medium \${colors[status as keyof typeof colors] || 'bg-gray-100 text-gray-800'}\`}> {status} </span> ); }; return ( <div className="bg-white rounded-lg shadow"> <div className="p-4 border-b flex justify-between items-center"> <div className="flex gap-2"> <select value={filter} onChange={(e) => setFilter(e.target.value)} className="border rounded px-3 py-1" > <option value="all">All Status</option> <option value="pending">Pending</option> <option value="processing">Processing</option> <option value="completed">Completed</option> <option value="failed">Failed</option> </select> </div> <button onClick={onRefresh} className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600" > Refresh </button> </div> <div className="overflow-x-auto"> <table className="w-full"> <thead className="bg-gray-50"> <tr> <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">ID</th> <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Status</th> <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Stage</th> <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Created</th> <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Actions</th> </tr> </thead> <tbody className="divide-y divide-gray-200"> {filteredItems.map(item => ( <tr key={item.id} className="hover:bg-gray-50"> <td className="px-4 py-2 text-sm font-medium"> <Link href={\`/items/\${item.id}\`} className="text-blue-600 hover:underline"> {item.id} </Link> </td> <td className="px-4 py-2">{getStatusBadge(item.status)}</td> <td className="px-4 py-2 text-sm">{item.stage}</td> <td className="px-4 py-2 text-sm text-gray-500"> {new Date(item.created_at).toLocaleString()} </td> <td className="px-4 py-2"> <Link href={\`/items/\${item.id}\`} className="text-sm text-blue-600 hover:underline" > View Details </Link> </td> </tr> ))} </tbody> </table> </div> </div> ); } `; await fs.writeFile(path.join(appPath, 'components', 'ItemsTable.tsx'), itemsTable); // Pipeline View Component const pipelineView = `interface PipelineViewProps { items: any[]; stages: string[]; } export default function PipelineView({ items, stages }: PipelineViewProps) { const getItemsInStage = (stage: string) => { return items.filter(item => item.stage === stage); }; return ( <div className="grid grid-cols-1 md:grid-cols-4 gap-4"> {stages.map(stage => ( <div key={stage} className="bg-white rounded-lg shadow"> <div className="p-4 border-b bg-gray-50"> <h3 className="font-semibold capitalize">{stage}</h3> <p className="text-sm text-gray-500">{getItemsInStage(stage).length} items</p> </div> <div className="p-4 space-y-2 max-h-96 overflow-y-auto"> {getItemsInStage(stage).map(item => ( <div key={item.id} className="p-3 border rounded hover:bg-gray-50 cursor-pointer" > <div className="text-sm font-medium">{item.id}</div> <div className="text-xs text-gray-500 mt-1"> {new Date(item.updated_at).toLocaleString()} </div> </div> ))} </div> </div> ))} </div> ); } `; await fs.writeFile(path.join(appPath, 'components', 'PipelineView.tsx'), pipelineView); } private async generateStyles(appPath: string): Promise<void> { // Main layout const layout = `import './globals.css'; import Link from 'next/link'; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <html lang="en"> <body> <div className="flex h-screen bg-gray-100"> {/* Sidebar */} <div className="w-64 bg-gray-800 text-white"> <div className="p-4"> <h2 className="text-xl font-bold">Workflow Manager</h2> </div> <nav className="mt-4"> <Link href="/dashboard" className="block px-4 py-2 hover:bg-gray-700" > Dashboard </Link> <Link href="/items" className="block px-4 py-2 hover:bg-gray-700" > All Items </Link> <Link href="/executions" className="block px-4 py-2 hover:bg-gray-700" > Executions </Link> <Link href="/settings" className="block px-4 py-2 hover:bg-gray-700" > Settings </Link> </nav> </div> {/* Main Content */} <div className="flex-1 overflow-auto"> {children} </div> </div> </body> </html> ); } `; await fs.writeFile(path.join(appPath, 'app', 'layout.tsx'), layout); // Global CSS const globals = `@tailwind base; @tailwind components; @tailwind utilities; `; await fs.writeFile(path.join(appPath, 'app', 'globals.css'), globals); // Tailwind config const tailwindConfig = `import type { Config } from 'tailwindcss'; const config: Config = { content: [ './pages/**/*.{js,ts,jsx,tsx,mdx}', './components/**/*.{js,ts,jsx,tsx,mdx}', './app/**/*.{js,ts,jsx,tsx,mdx}', ], theme: { extend: {}, }, plugins: [], }; export default config; `; await fs.writeFile(path.join(appPath, 'tailwind.config.ts'), tailwindConfig); // PostCSS config const postcssConfig = `module.exports = { plugins: { tailwindcss: {}, autoprefixer: {}, }, }; `; await fs.writeFile(path.join(appPath, 'postcss.config.js'), postcssConfig); } private async generateEnvFile(appPath: string): Promise<void> { const envExample = `# N8n Integration N8N_WEBHOOK_URL=http://localhost:5678/webhook # App Configuration APP_URL=http://localhost:3000 WORKFLOW_STORAGE_URL=http://localhost:3000 # Database DATABASE_PATH=./data/workflow.db # Workflow Stages (comma-separated) WORKFLOW_STAGES=created,processing,review,completed `; await fs.writeFile(path.join(appPath, '.env.example'), envExample); await fs.writeFile(path.join(appPath, '.env.local'), envExample); } private async generateGitIgnore(appPath: string): Promise<void> { const gitignore = `# Dependencies /node_modules /.pnp .pnp.js # Testing /coverage # Production /build /.next /out # Misc .DS_Store *.pem # Debug npm-debug.log* yarn-debug.log* yarn-error.log* # Local env files .env*.local .env # Vercel .vercel # TypeScript *.tsbuildinfo next-env.d.ts # Database /data/*.db /data/*.db-journal `; await fs.writeFile(path.join(appPath, '.gitignore'), gitignore); } }

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/mckinleymedia/mcflow-mcp'

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