Skip to main content
Glama

documcp

by tosin2013
test-local-deployment.ts14.4 kB
import { z } from 'zod'; import { promises as fs } from 'fs'; import * as path from 'path'; import { spawn, exec } from 'child_process'; import { promisify } from 'util'; import { MCPToolResponse, formatMCPResponse } from '../types/api.js'; const execAsync = promisify(exec); const inputSchema = z.object({ repositoryPath: z.string().describe('Path to the repository'), ssg: z.enum(['jekyll', 'hugo', 'docusaurus', 'mkdocs', 'eleventy']), port: z.number().optional().default(3000).describe('Port for local server'), timeout: z.number().optional().default(60).describe('Timeout in seconds for build process'), skipBuild: z .boolean() .optional() .default(false) .describe('Skip build step and only start server'), }); interface LocalTestResult { repositoryPath: string; ssg: string; buildSuccess: boolean; buildOutput?: string; buildErrors?: string; serverStarted: boolean; localUrl?: string; port: number; testScript: string; recommendations: string[]; nextSteps: string[]; } interface SSGConfig { buildCommand: string; serveCommand: string; buildDir: string; configFiles: string[]; installCommand?: string; } const SSG_CONFIGS: Record<string, SSGConfig> = { jekyll: { buildCommand: 'bundle exec jekyll build', serveCommand: 'bundle exec jekyll serve', buildDir: '_site', configFiles: ['_config.yml', '_config.yaml'], installCommand: 'bundle install', }, hugo: { buildCommand: 'hugo', serveCommand: 'hugo server', buildDir: 'public', configFiles: ['hugo.toml', 'hugo.yaml', 'hugo.yml', 'config.toml', 'config.yaml', 'config.yml'], }, docusaurus: { buildCommand: 'npm run build', serveCommand: 'npm run serve', buildDir: 'build', configFiles: ['docusaurus.config.js', 'docusaurus.config.ts'], installCommand: 'npm install', }, mkdocs: { buildCommand: 'mkdocs build', serveCommand: 'mkdocs serve', buildDir: 'site', configFiles: ['mkdocs.yml', 'mkdocs.yaml'], installCommand: 'pip install -r requirements.txt', }, eleventy: { buildCommand: 'npx @11ty/eleventy', serveCommand: 'npx @11ty/eleventy --serve', buildDir: '_site', configFiles: ['.eleventy.js', 'eleventy.config.js', '.eleventy.json'], installCommand: 'npm install', }, }; export async function testLocalDeployment(args: unknown): Promise<{ content: any[] }> { const startTime = Date.now(); const { repositoryPath, ssg, port, timeout, skipBuild } = inputSchema.parse(args); try { const config = SSG_CONFIGS[ssg]; if (!config) { throw new Error(`Unsupported SSG: ${ssg}`); } // Change to repository directory process.chdir(repositoryPath); const testResult: LocalTestResult = { repositoryPath, ssg, buildSuccess: false, serverStarted: false, port, testScript: '', recommendations: [], nextSteps: [], }; // Step 1: Check if configuration exists (always check, even if skipBuild) const configExists = await checkConfigurationExists(repositoryPath, config); if (!configExists) { testResult.recommendations.push( `Missing configuration file. Expected one of: ${config.configFiles.join(', ')}`, ); testResult.nextSteps.push('Run generate_config tool to create configuration'); } else { // Always mention which config file was found/expected for test purposes testResult.recommendations.push( `Using ${ssg} configuration: ${config.configFiles.join(' or ')}`, ); } // Step 2: Install dependencies if needed if (config.installCommand && !skipBuild) { try { const { stderr } = await execAsync(config.installCommand, { cwd: repositoryPath, timeout: timeout * 1000, }); if (stderr && !stderr.includes('npm WARN')) { testResult.recommendations.push('Dependency installation warnings detected'); } } catch (error: any) { testResult.recommendations.push(`Dependency installation failed: ${error.message}`); testResult.nextSteps.push('Fix dependency installation issues before testing deployment'); } } // Step 3: Build the site (unless skipped) if (!skipBuild) { try { const { stdout, stderr } = await execAsync(config.buildCommand, { cwd: repositoryPath, timeout: timeout * 1000, }); testResult.buildSuccess = true; testResult.buildOutput = stdout; if (stderr && stderr.trim()) { testResult.buildErrors = stderr; if (stderr.includes('error') || stderr.includes('Error')) { testResult.recommendations.push('Build completed with errors - review build output'); } } // Check if build directory was created const buildDirExists = await checkBuildOutput(repositoryPath, config.buildDir); if (!buildDirExists) { testResult.recommendations.push(`Build directory ${config.buildDir} was not created`); } } catch (error: any) { testResult.buildSuccess = false; testResult.buildErrors = error.message; testResult.recommendations.push('Build failed - fix build errors before deployment'); testResult.nextSteps.push('Review build configuration and resolve errors'); } } else { testResult.buildSuccess = true; // Assume success if skipped } // Step 4: Generate test script testResult.testScript = generateTestScript(ssg, config, port, repositoryPath); // Step 5: Try to start local server (non-blocking) if (testResult.buildSuccess || skipBuild) { const serverResult = await startLocalServer(config, port, repositoryPath, 10); // 10 second timeout for server start testResult.serverStarted = serverResult.started; testResult.localUrl = serverResult.url; if (testResult.serverStarted) { testResult.recommendations.push( 'Local server started successfully - test manually at the provided URL', ); testResult.nextSteps.push('Verify content loads correctly in browser'); testResult.nextSteps.push('Test navigation and responsive design'); } else { testResult.recommendations.push( 'Could not automatically start local server - run manually using the provided script', ); testResult.nextSteps.push( 'Start server manually and verify it works before GitHub deployment', ); } } // Step 6: Generate final recommendations if (testResult.buildSuccess && testResult.serverStarted) { testResult.recommendations.push('Local deployment test successful - ready for GitHub Pages'); testResult.nextSteps.push('Run deploy_pages tool to set up GitHub Actions workflow'); } else if (testResult.buildSuccess && !testResult.serverStarted) { testResult.recommendations.push( 'Build successful but server test incomplete - manual verification needed', ); testResult.nextSteps.push('Test server manually before deploying to GitHub'); } const response: MCPToolResponse<typeof testResult> = { success: true, data: testResult, metadata: { toolVersion: '1.0.0', executionTime: Date.now() - startTime, timestamp: new Date().toISOString(), }, recommendations: [ { type: testResult.buildSuccess ? 'info' : 'warning', title: 'Local Deployment Test Complete', description: `Build ${testResult.buildSuccess ? 'succeeded' : 'failed'}, Server ${ testResult.serverStarted ? 'started' : 'failed to start' }`, }, ], nextSteps: testResult.nextSteps.map((step) => ({ action: step, toolRequired: getRecommendedTool(step), description: step, priority: testResult.buildSuccess ? 'medium' : ('high' as const), })), }; return formatMCPResponse(response); } catch (error) { const errorResponse: MCPToolResponse = { success: false, error: { code: 'LOCAL_TEST_FAILED', message: `Failed to test local deployment: ${error}`, resolution: 'Ensure repository path is valid and SSG is properly configured', }, metadata: { toolVersion: '1.0.0', executionTime: Date.now() - startTime, timestamp: new Date().toISOString(), }, }; return formatMCPResponse(errorResponse); } } async function checkConfigurationExists(repoPath: string, config: SSGConfig): Promise<boolean> { for (const configFile of config.configFiles) { try { await fs.access(path.join(repoPath, configFile)); return true; } catch { // File doesn't exist, continue checking } } return false; } async function checkBuildOutput(repoPath: string, buildDir: string): Promise<boolean> { try { const buildPath = path.join(repoPath, buildDir); const stats = await fs.stat(buildPath); if (stats.isDirectory()) { const files = await fs.readdir(buildPath); return files.length > 0; } } catch { // Directory doesn't exist or can't be read } return false; } async function startLocalServer( config: SSGConfig, port: number, repoPath: string, timeout: number, ): Promise<{ started: boolean; url?: string }> { return new Promise((resolve) => { let serverProcess: any = null; let resolved = false; const cleanup = () => { if (serverProcess && !serverProcess.killed) { try { serverProcess.kill('SIGTERM'); // Force kill if SIGTERM doesn't work after 1 second const forceKillTimeout = setTimeout(() => { if (serverProcess && !serverProcess.killed) { serverProcess.kill('SIGKILL'); } }, 1000); // Clear the timeout if process exits normally serverProcess.on('exit', () => { clearTimeout(forceKillTimeout); }); } catch (error) { // Process may already be dead } } }; const safeResolve = (result: { started: boolean; url?: string }) => { if (!resolved) { resolved = true; cleanup(); resolve(result); } }; const serverTimeout = setTimeout(() => { safeResolve({ started: false }); }, timeout * 1000); try { let command = config.serveCommand; // Modify serve command to use custom port for some SSGs if (config.serveCommand.includes('jekyll serve')) { command = `${config.serveCommand} --port ${port}`; } else if (config.serveCommand.includes('hugo server')) { command = `${config.serveCommand} --port ${port}`; } else if (config.serveCommand.includes('mkdocs serve')) { command = `${config.serveCommand} --dev-addr localhost:${port}`; } else if (config.serveCommand.includes('--serve')) { command = `${config.serveCommand} --port ${port}`; } serverProcess = spawn('sh', ['-c', command], { cwd: repoPath, detached: false, stdio: 'pipe', }); let serverStarted = false; serverProcess.stdout?.on('data', (data: Buffer) => { const output = data.toString(); // Check for server start indicators if ( !serverStarted && (output.includes('Server running') || output.includes('Serving on') || output.includes('Local:') || output.includes('localhost:') || output.includes(`http://127.0.0.1:${port}`) || output.includes(`http://localhost:${port}`)) ) { serverStarted = true; clearTimeout(serverTimeout); safeResolve({ started: true, url: `http://localhost:${port}`, }); } }); serverProcess.stderr?.on('data', (data: Buffer) => { const error = data.toString(); // Some servers output startup info to stderr if ( !serverStarted && (error.includes('Serving on') || error.includes('Local:') || error.includes('localhost:')) ) { serverStarted = true; clearTimeout(serverTimeout); safeResolve({ started: true, url: `http://localhost:${port}`, }); } }); serverProcess.on('error', (_error: Error) => { clearTimeout(serverTimeout); safeResolve({ started: false }); }); serverProcess.on('exit', () => { clearTimeout(serverTimeout); if (!resolved) { safeResolve({ started: false }); } }); } catch (_error) { clearTimeout(serverTimeout); safeResolve({ started: false }); } }); } function generateTestScript( ssg: string, config: SSGConfig, port: number, repoPath: string, ): string { const commands: string[] = [ `# Local Deployment Test Script for ${ssg}`, `# Generated on ${new Date().toISOString()}`, ``, `cd "${repoPath}"`, ``, ]; // Add install command if needed if (config.installCommand) { commands.push(`# Install dependencies`); commands.push(config.installCommand); commands.push(``); } // Add build command commands.push(`# Build the site`); commands.push(config.buildCommand); commands.push(``); // Add serve command with custom port commands.push(`# Start local server`); let serveCommand = config.serveCommand; if (serveCommand.includes('jekyll serve')) { serveCommand = `${serveCommand} --port ${port}`; } else if (serveCommand.includes('hugo server')) { serveCommand = `${serveCommand} --port ${port}`; } else if (serveCommand.includes('mkdocs serve')) { serveCommand = `${serveCommand} --dev-addr localhost:${port}`; } else if (serveCommand.includes('--serve')) { serveCommand = `${serveCommand} --port ${port}`; } commands.push(serveCommand); commands.push(``); commands.push(`# Open in browser:`); commands.push(`# http://localhost:${port}`); return commands.join('\n'); } function getRecommendedTool(step: string): string { if (step.includes('generate_config')) return 'generate_config'; if (step.includes('deploy_pages')) return 'deploy_pages'; if (step.includes('verify_deployment')) return 'verify_deployment'; return 'manual'; }

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/tosin2013/documcp'

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