* * This file is part of a Moko Consulting project. * * SPDX-License-Identifier: GPL-3.0-or-later * * FILE INFORMATION * DEFGROUP: MokoStandards.Enterprise.Plugins * INGROUP: MokoStandards * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /lib/Enterprise/Plugins/McpServerPlugin.php * BRIEF: Enterprise plugin for MCP (Model Context Protocol) server projects */ declare(strict_types=1); namespace MokoEnterprise\Plugins; use MokoEnterprise\AbstractProjectPlugin; /** * MCP Server Project Plugin * * Provides validation, metrics, and management capabilities for * Model Context Protocol server projects — TypeScript/Node.js servers * that expose external APIs as AI assistant tools. */ class McpServerPlugin extends AbstractProjectPlugin { /** * {@inheritdoc} */ public function getProjectType(): string { return 'mcp-server'; } /** * {@inheritdoc} */ public function getPluginName(): string { return 'MCP Server Enterprise Plugin'; } /** * {@inheritdoc} */ public function validateProject(array $config, string $projectPath): array { $errors = []; $warnings = []; // Check for required source files $requiredSrc = ['src/index.ts', 'src/client.ts', 'src/config.ts', 'src/types.ts']; foreach ($requiredSrc as $file) { if (!file_exists("{$projectPath}/{$file}")) { $errors[] = "Missing required source file: {$file}"; } } // Check for package.json with MCP SDK dependency if (file_exists("{$projectPath}/package.json")) { $content = @file_get_contents("{$projectPath}/package.json"); if ($content) { if (strpos($content, '@modelcontextprotocol/sdk') === false) { $errors[] = 'package.json missing @modelcontextprotocol/sdk dependency'; } if (strpos($content, 'zod') === false) { $warnings[] = 'package.json missing zod dependency (recommended for tool parameter validation)'; } } } else { $errors[] = 'Missing package.json'; } // Check for tsconfig.json if (!file_exists("{$projectPath}/tsconfig.json")) { $errors[] = 'Missing tsconfig.json'; } // Check for setup wizard if (!file_exists("{$projectPath}/scripts/setup.mjs")) { $warnings[] = 'Missing scripts/setup.mjs — interactive setup wizard recommended'; } // Check for config example if (!file_exists("{$projectPath}/config.example.json")) { $warnings[] = 'Missing config.example.json — example configuration recommended'; } // Check for shebang in index.ts if (file_exists("{$projectPath}/src/index.ts")) { $content = @file_get_contents("{$projectPath}/src/index.ts"); if ($content && strpos($content, '#!/usr/bin/env node') === false) { $warnings[] = 'src/index.ts should start with #!/usr/bin/env node shebang'; } } // Check for McpServer usage if (file_exists("{$projectPath}/src/index.ts")) { $content = @file_get_contents("{$projectPath}/src/index.ts"); if ($content && strpos($content, 'McpServer') === false) { $errors[] = 'src/index.ts must import and use McpServer from @modelcontextprotocol/sdk'; } } // Check for StdioServerTransport if (file_exists("{$projectPath}/src/index.ts")) { $content = @file_get_contents("{$projectPath}/src/index.ts"); if ($content && strpos($content, 'StdioServerTransport') === false) { $warnings[] = 'src/index.ts should use StdioServerTransport for Claude Code compatibility'; } } $this->log( 'MCP server project validation completed', 'info', ['errors' => count($errors), 'warnings' => count($warnings)] ); return [ 'valid' => empty($errors), 'errors' => $errors, 'warnings' => $warnings, ]; } /** * {@inheritdoc} */ public function collectMetrics(string $projectPath, array $config): array { $metrics = [ 'has_mcp_sdk' => false, 'has_zod' => false, 'has_setup_wizard' => file_exists("{$projectPath}/scripts/setup.mjs"), 'has_config_example' => file_exists("{$projectPath}/config.example.json"), 'tool_count' => 0, 'has_connection_management' => false, 'has_raw_api_passthrough' => false, ]; // Parse package.json for dependencies if (file_exists("{$projectPath}/package.json")) { $content = @file_get_contents("{$projectPath}/package.json"); if ($content) { $metrics['has_mcp_sdk'] = strpos($content, '@modelcontextprotocol/sdk') !== false; $metrics['has_zod'] = strpos($content, 'zod') !== false; } } // Count registered tools in index.ts if (file_exists("{$projectPath}/src/index.ts")) { $content = @file_get_contents("{$projectPath}/src/index.ts"); if ($content) { $metrics['tool_count'] = substr_count($content, 'server.tool('); $metrics['has_connection_management'] = strpos($content, 'list_connections') !== false; $metrics['has_raw_api_passthrough'] = strpos($content, 'api_request') !== false; } } // Count total TypeScript lines $tsFiles = $this->findFiles($projectPath, '**/*.ts'); $totalLines = 0; foreach ($tsFiles as $file) { if (is_file($file)) { $totalLines += count(file($file)); } } $metrics['total_lines'] = $totalLines; $this->recordMetric('mcp-server', 'tool_count', $metrics['tool_count']); $this->recordMetric('mcp-server', 'total_lines', $totalLines); $this->log('Collected MCP server metrics', 'info', $metrics); return $metrics; } /** * {@inheritdoc} */ public function healthCheck(string $projectPath, array $config): array { $issues = []; $score = 100; // Check for required source files $requiredSrc = ['src/index.ts', 'src/client.ts', 'src/config.ts', 'src/types.ts']; foreach ($requiredSrc as $file) { if (!file_exists("{$projectPath}/{$file}")) { $issues[] = [ 'severity' => 'critical', 'message' => "Missing required file: {$file}", ]; $score -= 20; } } // Check for MCP SDK if (file_exists("{$projectPath}/package.json")) { $content = @file_get_contents("{$projectPath}/package.json"); if ($content && strpos($content, '@modelcontextprotocol/sdk') === false) { $issues[] = [ 'severity' => 'critical', 'message' => 'Missing @modelcontextprotocol/sdk dependency', ]; $score -= 25; } } // Check for at least one registered tool if (file_exists("{$projectPath}/src/index.ts")) { $content = @file_get_contents("{$projectPath}/src/index.ts"); if ($content) { $toolCount = substr_count($content, 'server.tool('); if ($toolCount === 0) { $issues[] = [ 'severity' => 'critical', 'message' => 'No MCP tools registered in src/index.ts', ]; $score -= 25; } elseif ($toolCount < 5) { $issues[] = [ 'severity' => 'info', 'message' => "Only {$toolCount} tools registered — consider adding more coverage", ]; } } } // Check for README if (!$this->fileExists($projectPath, 'README.md')) { $issues[] = ['severity' => 'warning', 'message' => 'Missing README.md']; $score -= 5; } // Check for setup wizard if (!file_exists("{$projectPath}/scripts/setup.mjs")) { $issues[] = ['severity' => 'warning', 'message' => 'Missing interactive setup wizard']; $score -= 10; } // Check for config example if (!file_exists("{$projectPath}/config.example.json")) { $issues[] = ['severity' => 'warning', 'message' => 'Missing config.example.json']; $score -= 5; } $score = max(0, $score); $this->log('MCP server health check completed', 'info', [ 'score' => $score, 'issues_count' => count($issues), ]); return [ 'healthy' => $score >= 70, 'score' => $score, 'issues' => $issues, ]; } /** * {@inheritdoc} */ public function getRequiredFiles(): array { return [ 'src/index.ts — MCP server entry point with tool registrations', 'src/client.ts — HTTP client for target API', 'src/config.ts — Multi-connection config loader', 'src/types.ts — TypeScript interfaces', 'package.json — with @modelcontextprotocol/sdk and zod', 'tsconfig.json — TypeScript configuration', ]; } /** * {@inheritdoc} */ public function getRecommendedFiles(): array { return [ 'README.md — with tool reference table', 'config.example.json — example multi-connection config', 'scripts/setup.mjs — interactive setup wizard', 'docs/API.md — full tool parameter documentation', 'docs/ARCHITECTURE.md — component overview and design decisions', 'docs/INSTALLATION.md — prerequisites and setup guide', 'Makefile — build automation', '.mcp.json — MCP server registration (gitignored)', ]; } /** * {@inheritdoc} */ public function getConfigSchema(): array { return [ 'type' => 'object', 'properties' => [ 'target_api' => [ 'type' => 'string', 'description' => 'Name of the target API (e.g. "Dolibarr", "Joomla")', ], 'auth_method' => [ 'type' => 'string', 'enum' => ['bearer', 'api-key-header', 'basic', 'oauth2'], 'description' => 'Authentication mechanism used by the target API', ], 'api_prefix' => [ 'type' => 'string', 'description' => 'API path prefix (e.g. "/api/index.php", "/api/v1")', ], 'transport' => [ 'type' => 'string', 'enum' => ['stdio', 'sse', 'http'], 'description' => 'MCP transport type (default: stdio)', ], ], 'required' => ['target_api'], ]; } /** * {@inheritdoc} */ public function getBestPractices(): array { return [ 'Use Zod schemas for all tool parameter validation', 'Include a raw API passthrough tool for uncovered endpoints', 'Support multiple named connections (staging, production, dev)', 'Include an interactive setup wizard (scripts/setup.mjs)', 'Use node:https (not fetch) for self-signed cert support', 'Normalize error responses in formatResponse() helper', 'Group tools by resource type with section comments', 'Follow naming convention: prefix_resource_action (snake_case)', 'Include a list_connections tool for debugging', 'Document all tools with parameter tables in docs/API.md', 'Store config in home directory (~/.project-name.json)', 'Support config path override via environment variable', 'Use StdioServerTransport for Claude Code compatibility', 'Include config.example.json with multi-connection example', ]; } }