Skip to main content
Glama
startup-validator.ts10.5 kB
/** * Startup Validator for WP Navigator MCP Server * * Validates connection and environment before accepting requests. * * @since 1.2.0 */ import { readFileSync } from 'fs'; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; import type { WPConfig } from './config.js'; import { logger } from './logger.js'; import { detectAgent, getAgentName } from './agent-detection.js'; /** * Get MCP package version from package.json */ function getMcpVersion(): string { try { const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const packagePath = join(__dirname, '..', 'package.json'); const packageJson = JSON.parse(readFileSync(packagePath, 'utf-8')); return packageJson.version || '0.0.0'; } catch { return '0.0.0'; } } /** * Compare semver versions. * Returns: -1 if a < b, 0 if a == b, 1 if a > b */ function compareSemver(a: string, b: string): number { const partsA = a.split('.').map(Number); const partsB = b.split('.').map(Number); for (let i = 0; i < 3; i++) { const partA = partsA[i] || 0; const partB = partsB[i] || 0; if (partA < partB) return -1; if (partA > partB) return 1; } return 0; } interface McpCompat { min_version: string; max_version: string; tested_up_to: string; } interface CompatibilityResult { compatible: boolean; mcpVersion: string; pluginVersion: string; message: string; warning?: string; } /** * Check MCP compatibility with plugin. */ export function checkCompatibility(introspectData: any): CompatibilityResult { const mcpVersion = getMcpVersion(); const pluginVersion = introspectData?.plugin?.version || 'unknown'; const mcpCompat: McpCompat | undefined = introspectData?.plugin?.mcp_compat; // If plugin doesn't provide mcp_compat, assume compatible (backward compat) if (!mcpCompat) { return { compatible: true, mcpVersion, pluginVersion, message: `MCP ${mcpVersion} compatible (plugin does not report mcp_compat)`, }; } const { min_version, max_version, tested_up_to } = mcpCompat; // Check if MCP version is below minimum if (compareSemver(mcpVersion, min_version) < 0) { return { compatible: false, mcpVersion, pluginVersion, message: `MCP ${mcpVersion} below minimum ${min_version}`, warning: `Update MCP: npm install @littlebearapps/wp-navigator-mcp@latest`, }; } // Check if MCP version exceeds maximum if (compareSemver(mcpVersion, max_version) > 0) { return { compatible: false, mcpVersion, pluginVersion, message: `MCP ${mcpVersion} exceeds max ${max_version}`, warning: `Plugin may need updating. Check wpnav.ai for latest version.`, }; } // Check if MCP version exceeds tested version (warning only) if (compareSemver(mcpVersion, tested_up_to) > 0) { return { compatible: true, mcpVersion, pluginVersion, message: `MCP ${mcpVersion} compatible (untested with plugin ${pluginVersion})`, warning: `MCP ${mcpVersion} > tested ${tested_up_to}. Minor issues possible.`, }; } return { compatible: true, mcpVersion, pluginVersion, message: `MCP ${mcpVersion} compatible with plugin ${pluginVersion}`, }; } interface StartupCheckResult { ok: boolean; message: string; details?: any; } interface StartupValidation { allPassed: boolean; checks: { rest: StartupCheckResult; auth: StartupCheckResult; plugin: StartupCheckResult; policy: StartupCheckResult; compat: StartupCheckResult; }; warnings: string[]; } /** * Run all startup validation checks. */ export async function validateStartup( wpRequest: (endpoint: string, options?: RequestInit) => Promise<any>, config: WPConfig ): Promise<StartupValidation> { logger.info('Running startup validation...'); const rest = await checkRestAPI(wpRequest, config); const auth = await checkAuthentication(wpRequest, config); const plugin = await checkPlugin(wpRequest, config); const policy = await checkPolicy(wpRequest, config); // Compatibility check (uses introspect data from policy check) let compat: StartupCheckResult; let compatResult: CompatibilityResult | null = null; if (policy.details) { compatResult = checkCompatibility(policy.details); compat = { ok: compatResult.compatible, message: compatResult.message, details: { mcpVersion: compatResult.mcpVersion, pluginVersion: compatResult.pluginVersion, }, }; } else { compat = { ok: true, message: 'Compatibility: skipped (no introspect data)', }; } const checks = { rest, auth, plugin, policy, compat }; const warnings = collectWarnings(config); // Add compat warning if present if (compatResult?.warning) { warnings.push(compatResult.warning); } // Compat issues are warnings only, don't fail startup const criticalChecks = { rest, auth, plugin, policy }; const allPassed = Object.values(criticalChecks).every(check => check.ok); return { allPassed, checks, warnings }; } /** * Check REST API reachability. */ async function checkRestAPI( wpRequest: (endpoint: string, options?: RequestInit) => Promise<any>, config: WPConfig ): Promise<StartupCheckResult> { try { const response = await wpRequest('/'); return { ok: true, message: 'REST API reachable', details: response, }; } catch (error: any) { return { ok: false, message: `REST API unreachable: ${error.message}`, }; } } /** * Check authentication. */ async function checkAuthentication( wpRequest: (endpoint: string, options?: RequestInit) => Promise<any>, config: WPConfig ): Promise<StartupCheckResult> { try { const data = await wpRequest('/wp/v2/users/me'); return { ok: true, message: `Authenticated as: ${data.name || 'unknown'}`, details: { username: data.slug, roles: data.roles }, }; } catch (error: any) { if (error.message.includes('401')) { return { ok: false, message: 'Authentication failed (401 Unauthorized)', }; } return { ok: false, message: `Auth check failed: ${error.message}`, }; } } /** * Check plugin availability. */ async function checkPlugin( wpRequest: (endpoint: string, options?: RequestInit) => Promise<any>, config: WPConfig ): Promise<StartupCheckResult> { try { // Note: wpnavBase already includes the full URL const endpoint = config.wpnavBase.replace(config.restApi, '') + '/ping'; const data = await wpRequest(endpoint); return { ok: true, message: `Plugin v${data.version || 'unknown'} active`, details: data, }; } catch (error: any) { return { ok: false, message: `Plugin check failed: ${error.message}`, }; } } /** * Check policy configuration (also returns full introspect data for compat check). */ async function checkPolicy( wpRequest: (endpoint: string, options?: RequestInit) => Promise<any>, config: WPConfig ): Promise<StartupCheckResult> { try { const endpoint = config.wpnavIntrospect.replace(config.restApi, ''); const data = await wpRequest(endpoint); const categories = data.policy?.categories || {}; const enabled = Object.keys(categories).filter(k => categories[k]); return { ok: true, message: `Policy: ${enabled.join(', ') || 'None enabled'}`, details: data, // Full introspect data (includes plugin.mcp_compat) }; } catch (error: any) { return { ok: false, message: `Policy check failed: ${error.message}`, }; } } /** * Collect environment warnings. */ function collectWarnings(config: WPConfig): string[] { const warnings: string[] = []; if (!config.toggles.enableWrites) { warnings.push('Writes disabled (read-only mode)'); } if (config.toggles.allowInsecureHttp) { warnings.push('Insecure HTTP allowed (dev mode)'); } if (config.baseUrl.startsWith('http://localhost') || config.baseUrl.startsWith('http://127.0.0.1')) { warnings.push('Local development detected'); } return warnings; } /** * Print startup summary. */ export function printStartupSummary(validation: StartupValidation, config: WPConfig): void { console.log('\n🚀 WP Navigator Pro MCP Server\n'); console.log('━'.repeat(50)); // Connection info console.log(`\n✓ WordPress: ${config.baseUrl}`); console.log(`✓ REST API: ${config.wpnavBase}`); if (validation.checks.auth.ok) { console.log(`✓ ${validation.checks.auth.message}`); } if (validation.checks.plugin.ok) { console.log(`✓ ${validation.checks.plugin.message}`); } if (validation.checks.policy.ok) { console.log(`✓ ${validation.checks.policy.message}`); } // Compatibility status if (validation.checks.compat) { const icon = validation.checks.compat.ok ? '✓' : '⚠️'; console.log(`${icon} ${validation.checks.compat.message}`); } // Warnings if (validation.warnings.length > 0) { console.log('\n⚠️ Warnings:'); validation.warnings.forEach(warning => { console.log(` - ${warning}`); }); } // Status console.log('\n' + '━'.repeat(50)); if (validation.allPassed) { const agentName = getAgentName(); console.log(`\n✅ Ready! Waiting for requests from ${agentName}...\n`); console.log('💡 Tip: Try "Use wpnav_introspect to check your site"\n'); } else { console.log('\n❌ Startup validation failed\n'); Object.entries(validation.checks).forEach(([key, check]) => { if (!check.ok) { console.log(` ✗ ${key}: ${check.message}`); } }); console.log('\n📚 Troubleshooting: https://wpnav.ai/help/connection-errors\n'); process.exit(1); } } /** * Print friendly error with solution. */ export function printFriendlyError(error: Error): void { console.error('\n❌ Connection Error\n'); console.error('━'.repeat(50)); console.error(`\n${error.message}\n`); // Common errors with solutions if (error.message.includes('ECONNREFUSED')) { console.error('💡 Solution:'); console.error(' - Check if WordPress is running'); console.error(' - Verify WP_BASE_URL in your config'); console.error(' - Ensure site is accessible\n'); } else if (error.message.includes('401')) { console.error('💡 Solution:'); console.error(' - Application Password is incorrect'); console.error(' - Regenerate password in WordPress admin'); console.error(' - Check WP_APP_USER and WP_APP_PASS\n'); } else if (error.message.includes('403')) { console.error('💡 Solution:'); console.error(' - WAF blocking detected'); console.error(' - Add /wp-json/wpnav/* to WAF allowlist'); console.error(' - Check security plugin settings\n'); } else { console.error('📚 Troubleshooting: https://wpnav.ai/help\n'); } process.exit(1); }

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/littlebearapps/wp-navigator-mcp'

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