From 0e82802f0c33f4bf2326409d13701b01a672251e Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Thu, 7 May 2026 15:43:56 -0500 Subject: [PATCH] feat(mcp): add mokostandards MCP server with 24 governance tools Embeds an MCP server in mcp/ that exposes MokoStandards CLI tools as AI assistant tools: platform detection, repo health checks, validation (structure, headers, secrets, changelog, version consistency, enterprise readiness, drift scan), Joomla/Dolibarr-specific checks, definitions browser, policy/guide reader, and release notes generation. Also adds McpServerPlugin, MCP platform detection, and MCP workflow templates. Co-Authored-By: Claude Opus 4.6 (1M context) --- definitions/default/mcp-server.tf | 485 ++++++++++++++++++ docs/api/definitions/default/index.md | 2 + lib/Enterprise/PluginRegistry.php | 2 + lib/Enterprise/Plugins/McpServerPlugin.php | 353 +++++++++++++ mcp/config.example.json | 6 + mcp/package.json | 29 ++ mcp/src/config.ts | 61 +++ mcp/src/index.ts | 453 ++++++++++++++++ mcp/src/runner.ts | 56 ++ mcp/src/types.ts | 31 ++ mcp/tsconfig.json | 19 + .../mcp/mcp-auto-release.yml.template | 278 ++++++++++ .../workflows/mcp/mcp-build-test.yml.template | 61 +++ .../workflows/mcp/mcp-sdk-check.yml.template | 105 ++++ .../mcp/mcp-tool-inventory.yml.template | 57 ++ validate/auto_detect_platform.php | 77 ++- 16 files changed, 2074 insertions(+), 1 deletion(-) create mode 100644 definitions/default/mcp-server.tf create mode 100644 lib/Enterprise/Plugins/McpServerPlugin.php create mode 100644 mcp/config.example.json create mode 100644 mcp/package.json create mode 100644 mcp/src/config.ts create mode 100644 mcp/src/index.ts create mode 100644 mcp/src/runner.ts create mode 100644 mcp/src/types.ts create mode 100644 mcp/tsconfig.json create mode 100644 templates/workflows/mcp/mcp-auto-release.yml.template create mode 100644 templates/workflows/mcp/mcp-build-test.yml.template create mode 100644 templates/workflows/mcp/mcp-sdk-check.yml.template create mode 100644 templates/workflows/mcp/mcp-tool-inventory.yml.template diff --git a/definitions/default/mcp-server.tf b/definitions/default/mcp-server.tf new file mode 100644 index 0000000..cd610aa --- /dev/null +++ b/definitions/default/mcp-server.tf @@ -0,0 +1,485 @@ +/** + * MCP Server Repository Structure Definition + * Standard repository structure for Model Context Protocol (MCP) server projects + * + * Copyright (C) 2026 Moko Consulting + * SPDX-License-Identifier: GPL-3.0-or-later + * Version: 04.06.00 + * Schema Version: 1.0 + */ + +locals { + repository_structure = { + metadata = { + name = "MCP Server" + description = "Standard repository structure for Model Context Protocol (MCP) server projects — TypeScript/Node.js MCP servers that expose external APIs as AI assistant tools" + repository_type = "mcp-server" + platform = "mcp-server" + last_updated = "2026-05-07T00:00:00Z" + maintainer = "Moko Consulting" + version = "04.06.00" + schema_version = "1.0" + } + + root_files = [ + { + name = "README.md" + extension = "md" + description = "Project overview with tool reference table, install, and configuration" + requirement_status = "required" + always_overwrite = false + protected = true + audience = "general" + source_path = "templates/docs/required" + source_filename = "template-README.md" + source_type = "template" + destination_path = "." + destination_filename = "README.md" + create_path = false + template = "templates/docs/required/template-README.md" + }, + { + name = "LICENSE" + extension = "" + description = "License file (GPL-3.0-or-later)" + requirement_status = "required" + audience = "general" + source_path = "templates/licenses" + source_filename = "GPL-3.0" + source_type = "template" + destination_path = "." + destination_filename = "LICENSE" + create_path = false + template = "templates/licenses/GPL-3.0" + }, + { + name = "CHANGELOG.md" + extension = "md" + description = "Version history and changes" + requirement_status = "required" + always_overwrite = false + protected = true + audience = "general" + source_path = "templates/docs/required" + source_filename = "template-CHANGELOG.md" + source_type = "template" + destination_path = "." + destination_filename = "CHANGELOG.md" + create_path = false + template = "templates/docs/required/template-CHANGELOG.md" + }, + { + name = "CONTRIBUTING.md" + extension = "md" + description = "Contribution guidelines" + requirement_status = "required" + always_overwrite = false + protected = true + audience = "contributor" + source_path = "templates/docs/required" + source_filename = "template-CONTRIBUTING.md" + source_type = "template" + destination_path = "." + destination_filename = "CONTRIBUTING.md" + create_path = false + template = "templates/docs/required/template-CONTRIBUTING.md" + }, + { + name = "SECURITY.md" + extension = "md" + description = "Security policy and vulnerability reporting" + requirement_status = "required" + always_overwrite = false + protected = true + audience = "general" + source_path = "templates/docs/required" + source_filename = "template-SECURITY.md" + source_type = "template" + destination_path = "." + destination_filename = "SECURITY.md" + create_path = false + template = "templates/docs/required/template-SECURITY.md" + }, + { + name = "CODE_OF_CONDUCT.md" + extension = "md" + description = "Community code of conduct" + requirement_status = "required" + always_overwrite = false + protected = true + audience = "contributor" + source_path = "templates/docs/extra" + source_filename = "template-CODE_OF_CONDUCT.md" + source_type = "template" + destination_path = "." + destination_filename = "CODE_OF_CONDUCT.md" + create_path = false + template = "templates/docs/extra/template-CODE_OF_CONDUCT.md" + }, + { + name = "package.json" + extension = "json" + description = "Node.js project manifest — @mokoconsulting scoped, MCP SDK + Zod dependencies" + requirement_status = "required" + always_overwrite = false + protected = true + audience = "developer" + }, + { + name = "tsconfig.json" + extension = "json" + description = "TypeScript configuration — ES2022 target, Node16 module, strict mode" + requirement_status = "required" + always_overwrite = false + audience = "developer" + }, + { + name = "config.example.json" + extension = "json" + description = "Example multi-connection configuration file" + requirement_status = "required" + always_overwrite = false + protected = true + audience = "general" + }, + { + name = ".gitignore" + extension = "gitignore" + description = "Git ignore patterns" + requirement_status = "required" + always_overwrite = false + audience = "developer" + }, + { + name = ".gitattributes" + extension = "gitattributes" + description = "Git attributes configuration" + requirement_status = "required" + audience = "developer" + }, + { + name = ".gitmessage" + extension = "gitmessage" + description = "Conventional commit message template" + requirement_status = "required" + always_overwrite = true + audience = "developer" + }, + { + name = "Makefile" + description = "Build automation — install, build, dev, clean, setup, start targets" + requirement_status = "required" + always_overwrite = false + audience = "developer" + } + ] + + directories = [ + { + name = "src" + path = "src" + description = "TypeScript source code" + requirement_status = "required" + purpose = "Contains MCP server entry point, API client, config loader, and type definitions" + files = [ + { + name = "index.ts" + extension = "ts" + description = "MCP server entry point — registers all API tools with McpServer" + requirement_status = "required" + }, + { + name = "client.ts" + extension = "ts" + description = "HTTP client wrapper for the target API (GET/POST/PUT/DELETE)" + requirement_status = "required" + }, + { + name = "config.ts" + extension = "ts" + description = "Configuration loader — reads ~/.{project}.json with multi-connection support" + requirement_status = "required" + }, + { + name = "types.ts" + extension = "ts" + description = "TypeScript interfaces for connection, config, and API response types" + requirement_status = "required" + } + ] + }, + { + name = "scripts" + path = "scripts" + description = "Setup and utility scripts" + requirement_status = "required" + purpose = "Contains interactive setup wizard and repo-specific helpers" + files = [ + { + name = "setup.mjs" + extension = "mjs" + description = "Interactive setup wizard — prompts for API connection details and writes config" + requirement_status = "required" + always_overwrite = false + protected = true + } + ] + }, + { + name = "docs" + path = "docs" + description = "Documentation directory" + requirement_status = "required" + purpose = "Contains project documentation" + files = [ + { + name = "index.md" + extension = "md" + description = "Documentation index" + requirement_status = "suggested" + } + ] + }, + { + name = ".gitea" + path = ".gitea" + description = "Gitea-specific configuration" + requirement_status = "required" + purpose = "Contains Gitea Actions workflows and platform configuration" + files = [ + { + name = ".mokostandards" + description = "MokoStandards platform declaration — must contain 'platform: mcp-server'" + requirement_status = "required" + always_overwrite = false + } + ] + subdirectories = [ + { + name = "workflows" + path = ".gitea/workflows" + description = "Gitea Actions workflows" + requirement_status = "required" + files = [ + { + name = "auto-release.yml" + extension = "yml" + description = "Auto-create release on push to main" + requirement_status = "required" + always_overwrite = true + template = "templates/workflows/shared/auto-release.yml.template" + }, + { + name = "auto-dev-issue.yml" + extension = "yml" + description = "Auto-create tracking issue when a dev/** branch is pushed" + requirement_status = "required" + always_overwrite = true + template = "templates/workflows/shared/auto-dev-issue.yml.template" + }, + { + name = "auto-assign.yml" + extension = "yml" + description = "Auto-assign issues and PRs" + requirement_status = "required" + always_overwrite = true + template = "templates/workflows/shared/auto-assign.yml.template" + }, + { + name = "standards-compliance.yml" + extension = "yml" + description = "MokoStandards compliance validation" + requirement_status = "required" + always_overwrite = true + template = "templates/workflows/shared/standards-compliance.yml.template" + }, + { + name = "codeql-analysis.yml" + extension = "yml" + description = "CodeQL security analysis" + requirement_status = "required" + always_overwrite = true + template = "templates/workflows/shared/codeql-analysis.yml.template" + }, + { + name = "changelog-validation.yml" + extension = "yml" + description = "CHANGELOG validation on PR" + requirement_status = "required" + always_overwrite = true + template = "templates/workflows/shared/changelog-validation.yml.template" + }, + { + name = "sync-version-on-merge.yml" + extension = "yml" + description = "Auto-bump patch version on merge" + requirement_status = "required" + always_overwrite = true + template = "templates/workflows/shared/sync-version-on-merge.yml.template" + }, + { + name = "repository-cleanup.yml" + extension = "yml" + description = "Scheduled cleanup of stale branches and workflow runs" + requirement_status = "required" + always_overwrite = true + template = "templates/workflows/shared/repository-cleanup.yml.template" + }, + { + name = "enterprise-firewall-setup.yml" + extension = "yml" + description = "Enterprise firewall configuration for trusted domain access" + requirement_status = "required" + always_overwrite = true + template = "templates/workflows/shared/enterprise-firewall-setup.yml.template" + }, + { + name = "deploy-dev.yml" + extension = "yml" + description = "Deployment to development server" + requirement_status = "suggested" + always_overwrite = true + template = "templates/workflows/shared/deploy-dev.yml.template" + }, + { + name = "deploy-demo.yml" + extension = "yml" + description = "Deployment to demo server on merge to main" + requirement_status = "suggested" + always_overwrite = true + template = "templates/workflows/shared/deploy-demo.yml.template" + }, + { + name = "copilot-agent.yml" + extension = "yml" + description = "Copilot agent workflow for automated code review" + requirement_status = "optional" + always_overwrite = true + template = "templates/workflows/shared/copilot-agent.yml.template" + }, + { + name = "mcp-build-test.yml" + extension = "yml" + description = "MCP server build validation — TypeScript compile, dist verification, tool count" + requirement_status = "required" + always_overwrite = true + template = "templates/workflows/mcp/mcp-build-test.yml.template" + }, + { + name = "mcp-sdk-check.yml" + extension = "yml" + description = "Weekly check for MCP SDK and Zod updates — creates issue when new version available" + requirement_status = "required" + always_overwrite = true + template = "templates/workflows/mcp/mcp-sdk-check.yml.template" + }, + { + name = "mcp-tool-inventory.yml" + extension = "yml" + description = "Generate tool inventory report on push to main" + requirement_status = "suggested" + always_overwrite = true + template = "templates/workflows/mcp/mcp-tool-inventory.yml.template" + } + ] + } + ] + }, + { + name = "dist" + path = "dist" + description = "Compiled JavaScript output (generated)" + requirement_status = "not-allowed" + purpose = "Generated directory that should not be committed" + }, + { + name = "node_modules" + path = "node_modules" + description = "Node.js dependencies (generated)" + requirement_status = "not-allowed" + purpose = "Generated directory that should not be committed" + } + ] + + repository_requirements = { + secrets = [ + { + name = "GH_TOKEN" + description = "Org-level Gitea PAT — configure in org Actions secrets" + required = true + scope = "organisation" + used_in = "Gitea Actions workflows" + } + ] + + variables = [ + { + name = "NODE_VERSION" + description = "Node.js version for CI/CD" + default_value = "20" + required = false + scope = "repository" + } + ] + + branch_protections = [ + { + branch_pattern = "main" + require_pull_request = true + required_approvals = 1 + require_code_owner_review = false + dismiss_stale_reviews = true + require_status_checks = true + required_status_checks = ["ci"] + enforce_admins = false + restrict_pushes = true + } + ] + + repository_settings = { + has_issues = true + has_projects = true + has_wiki = false + has_discussions = false + allow_merge_commit = true + allow_squash_merge = true + allow_rebase_merge = false + delete_branch_on_merge = true + allow_auto_merge = false + } + + labels = [ + { + name = "bug" + color = "d73a4a" + description = "Something isn't working" + }, + { + name = "enhancement" + color = "a2eeef" + description = "New feature or request" + }, + { + name = "documentation" + color = "0075ca" + description = "Improvements or additions to documentation" + }, + { + name = "security" + color = "ee0701" + description = "Security vulnerability or concern" + }, + { + name = "new-tool" + color = "5319e7" + description = "New MCP tool/endpoint to add" + }, + { + name = "api-change" + color = "fbca04" + description = "Upstream API changed — tool needs update" + } + ] + } + } +} diff --git a/docs/api/definitions/default/index.md b/docs/api/definitions/default/index.md index 7725187..7e730a5 100644 --- a/docs/api/definitions/default/index.md +++ b/docs/api/definitions/default/index.md @@ -14,6 +14,7 @@ This directory contains base platform-specific definition files that serve as te | **generic-repository.tf** | Generic | Alternative generic repository structure with minimal requirements | | **standards-repository.tf** | Standards | Structure for MokoStandards organizational repository with api/, templates/, docs/, logs/ | | **waas-component.tf** | Joomla | Structure for Joomla/WaaS components, modules, plugins with manifest validation | +| **mcp-server.tf** | MCP Server | Structure for MCP (Model Context Protocol) server projects — TypeScript/Node.js servers exposing REST APIs as AI tools | ## File Format @@ -117,6 +118,7 @@ The auto-detection script maps platforms to definition files: | `nodejs` | nodejs-repository.tf (future) | | `python` | python-repository.tf (future) | | `terraform` | terraform-repository.tf (future) | +| `mcp-server` | mcp-server.tf | | `standards` | standards-repository.tf | | `generic` | default-repository.tf | diff --git a/lib/Enterprise/PluginRegistry.php b/lib/Enterprise/PluginRegistry.php index 6e10a63..5d65228 100644 --- a/lib/Enterprise/PluginRegistry.php +++ b/lib/Enterprise/PluginRegistry.php @@ -14,6 +14,7 @@ use MokoEnterprise\Plugins\TerraformPlugin; use MokoEnterprise\Plugins\WordPressPlugin; use MokoEnterprise\Plugins\MobilePlugin; use MokoEnterprise\Plugins\ApiPlugin; +use MokoEnterprise\Plugins\McpServerPlugin; /** * Plugin Registry - Central registry for all project type plugins @@ -37,6 +38,7 @@ class PluginRegistry 'wordpress' => WordPressPlugin::class, 'mobile' => MobilePlugin::class, 'api' => ApiPlugin::class, + 'mcp-server' => McpServerPlugin::class, ]; /** @var array Instantiated plugins */ diff --git a/lib/Enterprise/Plugins/McpServerPlugin.php b/lib/Enterprise/Plugins/McpServerPlugin.php new file mode 100644 index 0000000..aaf61ce --- /dev/null +++ b/lib/Enterprise/Plugins/McpServerPlugin.php @@ -0,0 +1,353 @@ + + * + * 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/MokoStandards-API + * PATH: /lib/Enterprise/Plugins/McpServerPlugin.php + * VERSION: 04.06.00 + * 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', + ]; + } +} diff --git a/mcp/config.example.json b/mcp/config.example.json new file mode 100644 index 0000000..b05ea4f --- /dev/null +++ b/mcp/config.example.json @@ -0,0 +1,6 @@ +{ + "apiPath": "A:/MokoStandards-API", + "standardsPath": "A:/MokoStandards", + "giteaUrl": "https://git.mokoconsulting.tech", + "giteaToken": "your-gitea-api-token" +} diff --git a/mcp/package.json b/mcp/package.json new file mode 100644 index 0000000..143a5fb --- /dev/null +++ b/mcp/package.json @@ -0,0 +1,29 @@ +{ + "name": "@mokoconsulting/mokostandards-mcp", + "version": "1.0.0", + "description": "MCP server for MokoStandards governance — validation, compliance, platform detection, definitions browser", + "type": "module", + "main": "dist/index.js", + "bin": { + "mokostandards-mcp": "dist/index.js" + }, + "scripts": { + "build": "tsc", + "dev": "tsc --watch", + "start": "node dist/index.js", + "clean": "rm -rf dist/" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.12.1", + "zod": "^3.24.4" + }, + "devDependencies": { + "@types/node": "^22.15.3", + "typescript": "^5.8.3" + }, + "engines": { + "node": ">=20.0.0" + }, + "license": "GPL-3.0-or-later", + "author": "Moko Consulting " +} diff --git a/mcp/src/config.ts b/mcp/src/config.ts new file mode 100644 index 0000000..1c36d96 --- /dev/null +++ b/mcp/src/config.ts @@ -0,0 +1,61 @@ +/* Copyright (C) 2026 Moko Consulting + * + * This file is part of a Moko Consulting project. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * FILE INFORMATION + * DEFGROUP: mokostandards-mcp.Config + * INGROUP: MokoStandards-API + * REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API + * PATH: /mcp/src/config.ts + * VERSION: 01.00.00 + * BRIEF: Configuration loader for MokoStandards MCP server + */ + +import { readFile } from 'node:fs/promises'; +import { resolve, dirname } from 'node:path'; +import { homedir } from 'node:os'; +import { fileURLToPath } from 'node:url'; +import type { StandardsConfig } from './types.js'; + +const CONFIG_FILENAME = '.mokostandards-mcp.json'; + +export async function loadConfig(): Promise { + const config_path = process.env.MOKOSTANDARDS_MCP_CONFIG + ? resolve(process.env.MOKOSTANDARDS_MCP_CONFIG) + : resolve(homedir(), CONFIG_FILENAME); + + try { + const raw = await readFile(config_path, 'utf-8'); + const parsed = JSON.parse(raw) as Partial; + + if (!parsed.apiPath) { + throw new Error('apiPath is required — path to MokoStandards-API root'); + } + + return { + apiPath: resolve(parsed.apiPath), + standardsPath: parsed.standardsPath ? resolve(parsed.standardsPath) : undefined, + giteaUrl: parsed.giteaUrl, + giteaToken: parsed.giteaToken, + }; + } catch (err) { + // Auto-detect: if running from within MokoStandards-API/mcp/dist/, resolve parent + const scriptDir = dirname(fileURLToPath(import.meta.url)); + const autoApiPath = resolve(scriptDir, '..', '..'); + + try { + await readFile(resolve(autoApiPath, 'definitions', 'default', 'generic.tf'), 'utf-8'); + return { apiPath: autoApiPath }; + } catch { + // Can't auto-detect either + } + + const message = err instanceof Error ? err.message : String(err); + throw new Error( + `Failed to load config from ${config_path}: ${message}\n` + + `Create ${config_path} with { "apiPath": "/path/to/MokoStandards-API" }`, + ); + } +} diff --git a/mcp/src/index.ts b/mcp/src/index.ts new file mode 100644 index 0000000..2cf80a2 --- /dev/null +++ b/mcp/src/index.ts @@ -0,0 +1,453 @@ +#!/usr/bin/env node +/* Copyright (C) 2026 Moko Consulting + * + * This file is part of a Moko Consulting project. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * FILE INFORMATION + * DEFGROUP: mokostandards-mcp.Server + * INGROUP: MokoStandards-API + * REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API + * PATH: /mcp/src/index.ts + * VERSION: 01.00.00 + * BRIEF: MCP server entry point — exposes MokoStandards governance tools + */ + +import { readFile, readdir } from 'node:fs/promises'; +import { resolve, basename } from 'node:path'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { z } from 'zod'; +import { loadConfig } from './config.js'; +import { StandardsRunner } from './runner.js'; +import type { StandardsConfig } from './types.js'; + +let config: StandardsConfig; +let runner: StandardsRunner; + +function textResult(text: string): { content: Array<{ type: 'text'; text: string }> } { + return { content: [{ type: 'text' as const, text }] }; +} + +const server = new McpServer({ + name: 'mokostandards-mcp', + version: '1.0.0', +}); + +// ── Platform Detection ────────────────────────────────────────────────── + +server.tool( + 'standards_detect_platform', + 'Auto-detect the platform type of a repository (joomla, dolibarr, mcp-server, generic, etc.)', + { + repo_path: z.string().describe('Path to the repository to analyze'), + }, + async ({ repo_path }) => { + const result = await runner.runValidate('auto_detect_platform.php', [ + '--repo-path', repo_path, + '--schema-dir', resolve(config.apiPath, 'definitions', 'default'), + '--json', + ]); + return textResult(result.stdout || result.stderr); + }, +); + +// ── Version Management ────────────────────────────────────────────────── + +server.tool( + 'standards_version_read', + 'Read the current version from a repository README.md', + { + repo_path: z.string().describe('Path to the repository'), + }, + async ({ repo_path }) => { + const result = await runner.runCli('version_read.php', ['--path', repo_path]); + return textResult(result.stdout.trim() || `Error: ${result.stderr}`); + }, +); + +server.tool( + 'standards_version_bump', + 'Bump the version number in a repository (patch, minor, or major)', + { + repo_path: z.string().describe('Path to the repository'), + level: z.enum(['patch', 'minor', 'major']).describe('Version bump level'), + }, + async ({ repo_path, level }) => { + const result = await runner.runCli('version_bump.php', ['--path', repo_path, '--level', level]); + return textResult(result.stdout || result.stderr); + }, +); + +// ── Validation Tools ──────────────────────────────────────────────────── + +server.tool( + 'standards_check_repo_health', + 'Run a comprehensive health check on a repository — returns score, issues, and compliance level', + { + repo_path: z.string().describe('Path to the repository'), + }, + async ({ repo_path }) => { + const result = await runner.runValidate('check_repo_health.php', [ + '--path', repo_path, '--json', + ]); + return textResult(result.stdout || result.stderr); + }, +); + +server.tool( + 'standards_check_structure', + 'Validate repository file/directory structure against its platform definition', + { + repo_path: z.string().describe('Path to the repository'), + }, + async ({ repo_path }) => { + const result = await runner.runValidate('check_structure.php', ['--path', repo_path]); + return textResult(result.stdout || result.stderr); + }, +); + +server.tool( + 'standards_check_license_headers', + 'Check that all source files have proper license/copyright headers', + { + repo_path: z.string().describe('Path to the repository'), + }, + async ({ repo_path }) => { + const result = await runner.runValidate('check_license_headers.php', ['--path', repo_path]); + return textResult(result.stdout || result.stderr); + }, +); + +server.tool( + 'standards_check_version_consistency', + 'Verify version numbers are consistent across README, CHANGELOG, package.json, composer.json, manifests', + { + repo_path: z.string().describe('Path to the repository'), + }, + async ({ repo_path }) => { + const result = await runner.runValidate('check_version_consistency.php', ['--path', repo_path]); + return textResult(result.stdout || result.stderr); + }, +); + +server.tool( + 'standards_check_changelog', + 'Validate CHANGELOG.md format and content', + { + repo_path: z.string().describe('Path to the repository'), + }, + async ({ repo_path }) => { + const result = await runner.runValidate('check_changelog.php', ['--path', repo_path]); + return textResult(result.stdout || result.stderr); + }, +); + +server.tool( + 'standards_check_no_secrets', + 'Scan a repository for accidentally committed secrets, credentials, or API keys', + { + repo_path: z.string().describe('Path to the repository'), + }, + async ({ repo_path }) => { + const result = await runner.runValidate('check_no_secrets.php', ['--path', repo_path]); + return textResult(result.stdout || result.stderr); + }, +); + +server.tool( + 'standards_check_php_syntax', + 'Check PHP syntax across all PHP files in a repository', + { + repo_path: z.string().describe('Path to the repository'), + }, + async ({ repo_path }) => { + const result = await runner.runValidate('check_php_syntax.php', ['--path', repo_path]); + return textResult(result.stdout || result.stderr); + }, +); + +server.tool( + 'standards_check_tabs', + 'Check for consistent indentation (tabs vs spaces) across source files', + { + repo_path: z.string().describe('Path to the repository'), + }, + async ({ repo_path }) => { + const result = await runner.runValidate('check_tabs.php', ['--path', repo_path]); + return textResult(result.stdout || result.stderr); + }, +); + +server.tool( + 'standards_check_enterprise_readiness', + 'Check if a repository meets enterprise-readiness criteria (CI/CD, security, documentation)', + { + repo_path: z.string().describe('Path to the repository'), + }, + async ({ repo_path }) => { + const result = await runner.runValidate('check_enterprise_readiness.php', ['--path', repo_path]); + return textResult(result.stdout || result.stderr); + }, +); + +server.tool( + 'standards_scan_drift', + 'Scan for configuration drift — files that have diverged from the standards definition', + { + repo_path: z.string().describe('Path to the repository'), + }, + async ({ repo_path }) => { + const result = await runner.runValidate('scan_drift.php', ['--path', repo_path]); + return textResult(result.stdout || result.stderr); + }, +); + +// ── Joomla-Specific Validation ────────────────────────────────────────── + +server.tool( + 'standards_check_joomla_manifest', + 'Validate a Joomla extension manifest XML file', + { + repo_path: z.string().describe('Path to the Joomla extension repository'), + }, + async ({ repo_path }) => { + const result = await runner.runValidate('check_joomla_manifest.php', ['--path', repo_path]); + return textResult(result.stdout || result.stderr); + }, +); + +server.tool( + 'standards_check_joomla_language', + 'Validate Joomla language file structure and consistency', + { + repo_path: z.string().describe('Path to the Joomla extension repository'), + }, + async ({ repo_path }) => { + const result = await runner.runValidate('check_language_structure.php', ['--path', repo_path]); + return textResult(result.stdout || result.stderr); + }, +); + +// ── Dolibarr-Specific Validation ──────────────────────────────────────── + +server.tool( + 'standards_check_dolibarr_module', + 'Validate a Dolibarr module structure and descriptor', + { + repo_path: z.string().describe('Path to the Dolibarr module repository'), + }, + async ({ repo_path }) => { + const result = await runner.runValidate('check_dolibarr_module.php', ['--path', repo_path]); + return textResult(result.stdout || result.stderr); + }, +); + +// ── Release Management ────────────────────────────────────────────────── + +server.tool( + 'standards_release_notes', + 'Generate release notes from CHANGELOG.md for a specific version', + { + repo_path: z.string().describe('Path to the repository'), + version: z.string().optional().describe('Version to generate notes for (defaults to current)'), + }, + async ({ repo_path, version }) => { + const args = ['--path', repo_path]; + if (version) args.push('--version', version); + const result = await runner.runCli('release_notes.php', args); + return textResult(result.stdout || result.stderr); + }, +); + +// ── Definitions Browser ───────────────────────────────────────────────── + +server.tool( + 'standards_list_platforms', + 'List all available platform definitions (joomla, dolibarr, mcp-server, etc.)', + {}, + async () => { + const defDir = resolve(config.apiPath, 'definitions', 'default'); + try { + const files = await readdir(defDir); + const defs = files.filter(f => f.endsWith('.tf')).map(f => f.replace('.tf', '')); + return textResult(`Available platform definitions:\n${defs.map(d => ` - ${d}`).join('\n')}`); + } catch { + return textResult(`Error: Could not read definitions from ${defDir}`); + } + }, +); + +server.tool( + 'standards_get_platform_definition', + 'Get the full platform definition (required files, directories, requirements) for a platform type', + { + platform: z.string().describe('Platform name (e.g. "mcp-server", "joomla", "dolibarr", "generic")'), + }, + async ({ platform }) => { + const defPath = resolve(config.apiPath, 'definitions', 'default', `${platform}.tf`); + try { + const content = await readFile(defPath, 'utf-8'); + return textResult(content); + } catch { + return textResult(`Error: No definition found for platform "${platform}" at ${defPath}`); + } + }, +); + +server.tool( + 'standards_list_repos', + 'List all repositories that have been synced with MokoStandards (from sync definitions)', + {}, + async () => { + const syncDir = resolve(config.apiPath, 'definitions', 'sync'); + try { + const files = await readdir(syncDir); + const repos = files + .filter(f => f.endsWith('.def.tf')) + .map(f => f.replace('.def.tf', '')); + + const lines: string[] = []; + for (const repo of repos) { + const content = await readFile(resolve(syncDir, `${repo}.def.tf`), 'utf-8'); + const platform = content.match(/detected_platform\s*=\s*"([^"]+)"/)?.[1] ?? 'unknown'; + const desc = content.match(/description\s*=\s*"([^"]+)"/)?.[1] ?? ''; + lines.push(` ${repo} [${platform}] — ${desc}`); + } + + return textResult(`Synced repositories (${repos.length}):\n${lines.join('\n')}`); + } catch (err) { + return textResult(`Error reading sync definitions: ${err}`); + } + }, +); + +server.tool( + 'standards_get_repo_sync', + 'Get the sync tracking record for a specific repository', + { + repo: z.string().describe('Repository name (e.g. "joomla-api-mcp", "MokoCassiopeia")'), + }, + async ({ repo }) => { + const syncPath = resolve(config.apiPath, 'definitions', 'sync', `${repo}.def.tf`); + try { + const content = await readFile(syncPath, 'utf-8'); + return textResult(content); + } catch { + return textResult(`Error: No sync record found for "${repo}"`); + } + }, +); + +// ── Documentation Query ───────────────────────────────────────────────── + +server.tool( + 'standards_list_policies', + 'List available MokoStandards policy documents', + {}, + async () => { + const docsPath = config.standardsPath + ? resolve(config.standardsPath, 'docs', 'policy') + : resolve(config.apiPath, '..', 'MokoStandards', 'docs', 'policy'); + try { + const files = await readdir(docsPath); + const docs = files.filter(f => f.endsWith('.md')).map(f => ` - ${f}`); + return textResult(`Policy documents:\n${docs.join('\n')}`); + } catch { + return textResult(`Could not list policies. Set standardsPath in config to the MokoStandards root.`); + } + }, +); + +server.tool( + 'standards_read_policy', + 'Read a specific MokoStandards policy document', + { + filename: z.string().describe('Policy filename (e.g. "core-structure.md", "file-header-standards.md")'), + }, + async ({ filename }) => { + const safe = basename(filename); + const docsPath = config.standardsPath + ? resolve(config.standardsPath, 'docs', 'policy') + : resolve(config.apiPath, '..', 'MokoStandards', 'docs', 'policy'); + try { + const content = await readFile(resolve(docsPath, safe), 'utf-8'); + return textResult(content); + } catch { + return textResult(`Error: Could not read policy "${safe}" from ${docsPath}`); + } + }, +); + +server.tool( + 'standards_read_guide', + 'Read a specific MokoStandards guide document', + { + path: z.string().describe('Relative path within docs/guide/ (e.g. "validation/auto-detection.md")'), + }, + async ({ path }) => { + const docsPath = config.standardsPath + ? resolve(config.standardsPath, 'docs', 'guide') + : resolve(config.apiPath, '..', 'MokoStandards', 'docs', 'guide'); + try { + const content = await readFile(resolve(docsPath, path), 'utf-8'); + return textResult(content); + } catch { + return textResult(`Error: Could not read guide "${path}" from ${docsPath}`); + } + }, +); + +// ── Server Info ───────────────────────────────────────────────────────── + +server.tool( + 'standards_info', + 'Get MokoStandards server info — API path, standards version, available tools', + {}, + async () => { + let version = 'unknown'; + try { + const readme = await readFile(resolve(config.apiPath, '..', 'MokoStandards', 'README.md'), 'utf-8'); + const match = readme.match(/VERSION:\s*([\d.]+)/); + if (match) version = match[1]; + } catch { /* ignore */ } + + const syncDir = resolve(config.apiPath, 'definitions', 'sync'); + let repoCount = 0; + try { + const files = await readdir(syncDir); + repoCount = files.filter(f => f.endsWith('.def.tf')).length; + } catch { /* ignore */ } + + const defDir = resolve(config.apiPath, 'definitions', 'default'); + let platformCount = 0; + try { + const files = await readdir(defDir); + platformCount = files.filter(f => f.endsWith('.tf')).length; + } catch { /* ignore */ } + + return textResult([ + `MokoStandards MCP Server`, + ` Standards version: ${version}`, + ` API path: ${config.apiPath}`, + ` Standards path: ${config.standardsPath ?? 'auto-detect'}`, + ` Platform definitions: ${platformCount}`, + ` Synced repositories: ${repoCount}`, + ].join('\n')); + }, +); + +// ── Start Server ──────────────────────────────────────────────────────── + +async function main(): Promise { + config = await loadConfig(); + runner = new StandardsRunner(config); + const transport = new StdioServerTransport(); + await server.connect(transport); +} + +main().catch((err) => { + process.stderr.write(`Fatal: ${err}\n`); + process.exit(1); +}); diff --git a/mcp/src/runner.ts b/mcp/src/runner.ts new file mode 100644 index 0000000..f23386d --- /dev/null +++ b/mcp/src/runner.ts @@ -0,0 +1,56 @@ +/* Copyright (C) 2026 Moko Consulting + * + * This file is part of a Moko Consulting project. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * FILE INFORMATION + * DEFGROUP: mokostandards-mcp.Runner + * INGROUP: MokoStandards-API + * REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API + * PATH: /mcp/src/runner.ts + * VERSION: 01.00.00 + * BRIEF: PHP CLI command runner for MokoStandards tools — uses execFile (no shell injection) + */ + +import { execFile as nodeExecFile } from 'node:child_process'; +import { resolve } from 'node:path'; +import type { StandardsConfig, ExecResult } from './types.js'; + +const TIMEOUT_MS = 60_000; + +/** + * Runs MokoStandards PHP CLI tools via execFile (safe, no shell). + * All arguments are passed as array elements — never interpolated into a shell string. + */ +export class StandardsRunner { + private readonly apiPath: string; + + constructor(config: StandardsConfig) { + this.apiPath = config.apiPath; + } + + async runCli(script: string, args: string[] = []): Promise { + const scriptPath = resolve(this.apiPath, 'cli', script); + return this.run('php', [scriptPath, ...args]); + } + + async runValidate(script: string, args: string[] = []): Promise { + const scriptPath = resolve(this.apiPath, 'validate', script); + return this.run('php', [scriptPath, ...args]); + } + + private run(command: string, args: string[]): Promise { + return new Promise((resolvePromise) => { + // execFile is used intentionally — it does NOT spawn a shell, + // so arguments cannot be injected. This is the safe alternative to exec(). + nodeExecFile(command, args, { timeout: TIMEOUT_MS, maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => { + resolvePromise({ + stdout: stdout?.toString() ?? '', + stderr: stderr?.toString() ?? '', + exitCode: err && 'code' in err ? (err as { code: number }).code : (err ? 1 : 0), + }); + }); + }); + } +} diff --git a/mcp/src/types.ts b/mcp/src/types.ts new file mode 100644 index 0000000..dd2bee5 --- /dev/null +++ b/mcp/src/types.ts @@ -0,0 +1,31 @@ +/* Copyright (C) 2026 Moko Consulting + * + * This file is part of a Moko Consulting project. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * FILE INFORMATION + * DEFGROUP: mokostandards-mcp.Types + * INGROUP: MokoStandards-API + * REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API + * PATH: /mcp/src/types.ts + * VERSION: 01.00.00 + * BRIEF: TypeScript type definitions for MokoStandards MCP server + */ + +export interface StandardsConfig { + /** Path to MokoStandards-API root (contains cli/, validate/, definitions/) */ + apiPath: string; + /** Path to MokoStandards root (contains docs/) */ + standardsPath?: string; + /** Gitea API base URL */ + giteaUrl?: string; + /** Gitea API token (for repo queries) */ + giteaToken?: string; +} + +export interface ExecResult { + stdout: string; + stderr: string; + exitCode: number; +} diff --git a/mcp/tsconfig.json b/mcp/tsconfig.json new file mode 100644 index 0000000..0dd168c --- /dev/null +++ b/mcp/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/templates/workflows/mcp/mcp-auto-release.yml.template b/templates/workflows/mcp/mcp-auto-release.yml.template new file mode 100644 index 0000000..74daa33 --- /dev/null +++ b/templates/workflows/mcp/mcp-auto-release.yml.template @@ -0,0 +1,278 @@ +# MCP Server Auto-Release +# Copyright (C) 2026 Moko Consulting +# SPDX-License-Identifier: GPL-3.0-or-later +# +# MCP-specific release pipeline that builds TypeScript, runs validation, +# attaches the compiled dist/ as a release artifact, and creates a GitHub +# Release with tool inventory in the release notes. +# +# This replaces the generic auto-release.yml for MCP server repos. + +name: MCP Release + +on: + push: + branches: + - main + paths: + - 'src/**' + - 'package.json' + - 'tsconfig.json' + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +permissions: + contents: write + issues: write + +jobs: + build-and-release: + name: Build, Validate & Release + runs-on: ubuntu-latest + if: >- + !contains(github.event.head_commit.message, '[skip ci]') && + github.actor != 'github-actions[bot]' + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + token: ${{ secrets.GH_TOKEN || github.token }} + fetch-depth: 0 + + # ── Build ──────────────────────────────────────────────────────── + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install dependencies + run: npm ci + + - name: TypeScript compile check + run: npx tsc --noEmit + + - name: Build + run: npm run build + + - name: Verify dist output + run: | + for f in index.js client.js config.js types.js; do + test -f "dist/${f}" || (echo "ERROR: dist/${f} not found" && exit 1) + done + echo "✓ All dist files present" + + # ── Tool Inventory ─────────────────────────────────────────────── + - name: Generate tool inventory + id: tools + run: | + TOOL_COUNT=$(grep -c "server\.tool(" src/index.ts || echo "0") + echo "count=${TOOL_COUNT}" >> "$GITHUB_OUTPUT" + + # Extract tool names + TOOL_LIST=$(grep -oE "'[a-z_]+'" src/index.ts | head -100 | tr -d "'" | sort -u) + echo "Tools registered: ${TOOL_COUNT}" + + # Generate inventory for release notes + echo "## Tool Inventory (${TOOL_COUNT} tools)" > /tmp/tool-inventory.md + echo "" >> /tmp/tool-inventory.md + grep -B0 -A1 "server\.tool(" src/index.ts | grep -oE "'[^']+'" | while IFS= read -r name; do + read -r desc 2>/dev/null || true + CLEAN_NAME=$(echo "$name" | tr -d "'") + CLEAN_DESC=$(echo "$desc" | tr -d "'" | sed 's/,$//') + if [ -n "$CLEAN_NAME" ] && [ -n "$CLEAN_DESC" ]; then + echo "- \`${CLEAN_NAME}\` — ${CLEAN_DESC}" >> /tmp/tool-inventory.md + fi + done + + # ── Version ────────────────────────────────────────────────────── + - name: Setup MokoStandards tools + env: + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' + run: | + git clone --depth 1 --branch version/04 --quiet \ + "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ + /tmp/mokostandards + cd /tmp/mokostandards + composer install --no-dev --no-interaction --quiet + + - name: Read version from README.md + id: version + run: | + VERSION=$(php /tmp/mokostandards/api/cli/version_read.php --path . 2>/dev/null) + if [ -z "$VERSION" ]; then + echo "No VERSION in README.md — skipping release" + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + MAJOR=$(echo "$VERSION" | awk -F. '{print $1}') + MINOR=$(echo "$VERSION" | awk -F. '{printf "%s.%s", $1, $2}') + PATCH=$(echo "$VERSION" | awk -F. '{print $3}') + + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "branch=version/${MAJOR}" >> "$GITHUB_OUTPUT" + echo "major=$MAJOR" >> "$GITHUB_OUTPUT" + echo "minor=$MINOR" >> "$GITHUB_OUTPUT" + echo "release_tag=v${MAJOR}" >> "$GITHUB_OUTPUT" + + if [ "$PATCH" = "00" ]; then + echo "skip=true" >> "$GITHUB_OUTPUT" + else + echo "skip=false" >> "$GITHUB_OUTPUT" + if [ "$PATCH" = "01" ]; then + echo "is_first=true" >> "$GITHUB_OUTPUT" + else + echo "is_first=false" >> "$GITHUB_OUTPUT" + fi + fi + + - name: Check if already released + if: steps.version.outputs.skip != 'true' + id: check + run: | + TAG="${{ steps.version.outputs.release_tag }}" + TAG_EXISTS=false + git rev-parse "$TAG" >/dev/null 2>&1 && TAG_EXISTS=true + echo "tag_exists=$TAG_EXISTS" >> "$GITHUB_OUTPUT" + + # ── Release Artifact ───────────────────────────────────────────── + - name: Package dist + if: steps.version.outputs.skip != 'true' + run: | + VERSION="${{ steps.version.outputs.version }}" + REPO_NAME="${{ github.event.repository.name }}" + tar -czf "/tmp/${REPO_NAME}-${VERSION}.tar.gz" -C dist . + echo "artifact=/tmp/${REPO_NAME}-${VERSION}.tar.gz" >> "$GITHUB_OUTPUT" + + # ── Version Updates ────────────────────────────────────────────── + - name: Set platform version + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.tag_exists != 'true' + run: | + VERSION="${{ steps.version.outputs.version }}" + php /tmp/mokostandards/api/cli/version_set_platform.php \ + --path . --version "$VERSION" --branch main + + - name: Update version badges + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.tag_exists != 'true' + run: | + VERSION="${{ steps.version.outputs.version }}" + find . -name "*.md" ! -path "./.git/*" ! -path "./vendor/*" | while read -r f; do + if grep -q '\[VERSION:' "$f" 2>/dev/null; then + sed -i "s/\[VERSION:[[:space:]]*[0-9]\{2\}\.[0-9]\{2\}\.[0-9]\{2\}\]/[VERSION: ${VERSION}]/" "$f" + fi + done + + - name: Commit release changes + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.tag_exists != 'true' + run: | + if git diff --quiet && git diff --cached --quiet; then + echo "No changes to commit" + exit 0 + fi + VERSION="${{ steps.version.outputs.version }}" + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + git add -A + git commit -m "chore(release): build ${VERSION} [skip ci]" \ + --author="github-actions[bot] " + git push + + # ── Version Branch ─────────────────────────────────────────────── + - name: Archive version branch + if: steps.check.outputs.tag_exists != 'true' + run: | + BRANCH="${{ steps.version.outputs.branch }}" + git push origin HEAD:"$BRANCH" --force + echo "Updated archive branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY + + # ── Tag & Release ──────────────────────────────────────────────── + - name: Create git tag + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.tag_exists != 'true' && + steps.version.outputs.is_first == 'true' + run: | + TAG="${{ steps.version.outputs.release_tag }}" + if ! git rev-parse "$TAG" >/dev/null 2>&1; then + git tag "$TAG" + git push origin "$TAG" + fi + + - name: GitHub Release + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.tag_exists != 'true' + env: + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + run: | + VERSION="${{ steps.version.outputs.version }}" + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + MAJOR="${{ steps.version.outputs.major }}" + BRANCH="${{ steps.version.outputs.branch }}" + TOOL_COUNT="${{ steps.tools.outputs.count }}" + REPO_NAME="${{ github.event.repository.name }}" + + # Build release notes + NOTES=$(php /tmp/mokostandards/api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null) + [ -z "$NOTES" ] && NOTES="Release ${VERSION}" + + { + echo "$NOTES" + echo "" + echo "---" + echo "" + echo "### MCP Server Info" + echo "- **Tools registered**: ${TOOL_COUNT}" + echo "- **Node.js**: 20+" + echo "- **MCP SDK**: $(node -p \"require('./package.json').dependencies['@modelcontextprotocol/sdk']\" 2>/dev/null || echo 'unknown')" + echo "" + cat /tmp/tool-inventory.md 2>/dev/null || true + } > /tmp/release_notes.md + + EXISTING=$(gh release view "$RELEASE_TAG" --json tagName -q .tagName 2>/dev/null || true) + + ARTIFACT="/tmp/${REPO_NAME}-${VERSION}.tar.gz" + + if [ -z "$EXISTING" ]; then + gh release create "$RELEASE_TAG" \ + --title "v${MAJOR} (latest: ${VERSION})" \ + --notes-file /tmp/release_notes.md \ + --target "$BRANCH" \ + "$ARTIFACT" + echo "Release created: ${RELEASE_TAG} (${VERSION})" >> $GITHUB_STEP_SUMMARY + else + gh release edit "$RELEASE_TAG" \ + --title "v${MAJOR} (latest: ${VERSION})" \ + --notes-file /tmp/release_notes.md + gh release upload "$RELEASE_TAG" "$ARTIFACT" --clobber 2>/dev/null || true + echo "Release updated: ${RELEASE_TAG} -> ${VERSION}" >> $GITHUB_STEP_SUMMARY + fi + + # ── Summary ────────────────────────────────────────────────────── + - name: Pipeline Summary + if: always() + run: | + VERSION="${{ steps.version.outputs.version }}" + TOOL_COUNT="${{ steps.tools.outputs.count }}" + if [ "${{ steps.version.outputs.skip }}" = "true" ]; then + echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "## MCP Release Complete" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Detail | Value |" >> $GITHUB_STEP_SUMMARY + echo "|--------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Tools | ${TOOL_COUNT} |" >> $GITHUB_STEP_SUMMARY + echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Tag | \`${{ steps.version.outputs.release_tag }}\` |" >> $GITHUB_STEP_SUMMARY + fi diff --git a/templates/workflows/mcp/mcp-build-test.yml.template b/templates/workflows/mcp/mcp-build-test.yml.template new file mode 100644 index 0000000..cb631c7 --- /dev/null +++ b/templates/workflows/mcp/mcp-build-test.yml.template @@ -0,0 +1,61 @@ +# MCP Server Build & Validation +# Copyright (C) 2026 Moko Consulting +# SPDX-License-Identifier: GPL-3.0-or-later +# +# Builds the MCP server, validates TypeScript compilation, and checks +# that tools are properly registered with valid Zod schemas. + +name: MCP Build & Validate + +on: + push: + branches: [main, dev/**] + paths: ['src/**', 'package.json', 'tsconfig.json'] + pull_request: + branches: [main] + paths: ['src/**', 'package.json', 'tsconfig.json'] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [20, 22] + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - name: Install dependencies + run: npm ci + + - name: TypeScript compile + run: npx tsc --noEmit + + - name: Build + run: npm run build + + - name: Verify dist output exists + run: | + test -f dist/index.js || (echo "ERROR: dist/index.js not found" && exit 1) + test -f dist/client.js || (echo "ERROR: dist/client.js not found" && exit 1) + test -f dist/config.js || (echo "ERROR: dist/config.js not found" && exit 1) + test -f dist/types.js || (echo "ERROR: dist/types.js not found" && exit 1) + echo "✓ All required dist files present" + + - name: Verify shebang in index.js + run: | + head -1 dist/index.js | grep -q "#!/usr/bin/env node" || echo "WARNING: Missing shebang in dist/index.js" + + - name: Count registered tools + run: | + TOOL_COUNT=$(grep -c "server\.tool(" src/index.ts || true) + echo "Registered tools: ${TOOL_COUNT}" + if [ "${TOOL_COUNT}" -eq 0 ]; then + echo "ERROR: No tools registered in src/index.ts" + exit 1 + fi diff --git a/templates/workflows/mcp/mcp-sdk-check.yml.template b/templates/workflows/mcp/mcp-sdk-check.yml.template new file mode 100644 index 0000000..b926cd3 --- /dev/null +++ b/templates/workflows/mcp/mcp-sdk-check.yml.template @@ -0,0 +1,105 @@ +# MCP SDK Version Check +# Copyright (C) 2026 Moko Consulting +# SPDX-License-Identifier: GPL-3.0-or-later +# +# Weekly check for MCP SDK updates. Creates an issue when a new version +# of @modelcontextprotocol/sdk is available. + +name: MCP SDK Version Check + +on: + schedule: + - cron: '0 9 * * 1' # Every Monday at 9am UTC + workflow_dispatch: + +jobs: + check-sdk: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Check for SDK updates + id: sdk-check + run: | + CURRENT=$(node -p "require('./package.json').dependencies['@modelcontextprotocol/sdk']" | sed 's/[\^~]//') + LATEST=$(npm view @modelcontextprotocol/sdk version 2>/dev/null || echo "unknown") + + echo "current=${CURRENT}" >> $GITHUB_OUTPUT + echo "latest=${LATEST}" >> $GITHUB_OUTPUT + + if [ "${CURRENT}" != "${LATEST}" ] && [ "${LATEST}" != "unknown" ]; then + echo "update_available=true" >> $GITHUB_OUTPUT + echo "MCP SDK update available: ${CURRENT} → ${LATEST}" + else + echo "update_available=false" >> $GITHUB_OUTPUT + echo "MCP SDK is up to date: ${CURRENT}" + fi + + - name: Check for Zod updates + id: zod-check + run: | + CURRENT=$(node -p "require('./package.json').dependencies['zod']" | sed 's/[\^~]//') + LATEST=$(npm view zod version 2>/dev/null || echo "unknown") + + echo "current=${CURRENT}" >> $GITHUB_OUTPUT + echo "latest=${LATEST}" >> $GITHUB_OUTPUT + + if [ "${CURRENT}" != "${LATEST}" ] && [ "${LATEST}" != "unknown" ]; then + echo "update_available=true" >> $GITHUB_OUTPUT + else + echo "update_available=false" >> $GITHUB_OUTPUT + fi + + - name: Create update issue + if: steps.sdk-check.outputs.update_available == 'true' + uses: actions/github-script@v7 + with: + script: | + const title = `chore(deps): update @modelcontextprotocol/sdk ${process.env.CURRENT} → ${process.env.LATEST}`; + const body = [ + '## MCP SDK Update Available', + '', + `| Package | Current | Latest |`, + `|---------|---------|--------|`, + `| @modelcontextprotocol/sdk | ${process.env.CURRENT} | ${process.env.LATEST} |`, + `| zod | ${process.env.ZOD_CURRENT} | ${process.env.ZOD_LATEST} |`, + '', + '### Steps', + '1. Update package.json', + '2. Run `npm install`', + '3. Run `npm run build` to verify compilation', + '4. Test all tools against target API', + '', + '### Changelog', + `https://github.com/modelcontextprotocol/typescript-sdk/releases`, + ].join('\n'); + + // Check for existing open issue + const existing = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + labels: 'api-change', + }); + + const alreadyExists = existing.data.some(i => i.title.includes('@modelcontextprotocol/sdk')); + if (!alreadyExists) { + await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title, + body, + labels: ['api-change', 'chore'], + }); + } + env: + CURRENT: ${{ steps.sdk-check.outputs.current }} + LATEST: ${{ steps.sdk-check.outputs.latest }} + ZOD_CURRENT: ${{ steps.zod-check.outputs.current }} + ZOD_LATEST: ${{ steps.zod-check.outputs.latest }} diff --git a/templates/workflows/mcp/mcp-tool-inventory.yml.template b/templates/workflows/mcp/mcp-tool-inventory.yml.template new file mode 100644 index 0000000..f935b0c --- /dev/null +++ b/templates/workflows/mcp/mcp-tool-inventory.yml.template @@ -0,0 +1,57 @@ +# MCP Tool Inventory +# Copyright (C) 2026 Moko Consulting +# SPDX-License-Identifier: GPL-3.0-or-later +# +# Generates a tool inventory report on each push to main. +# Extracts tool names, descriptions, and parameter counts from src/index.ts. + +name: MCP Tool Inventory + +on: + push: + branches: [main] + paths: ['src/index.ts'] + workflow_dispatch: + +jobs: + inventory: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Generate tool inventory + run: | + echo "# MCP Tool Inventory" > TOOLS.md + echo "" >> TOOLS.md + echo "Auto-generated from \`src/index.ts\` on $(date -u +%Y-%m-%dT%H:%M:%SZ)" >> TOOLS.md + echo "" >> TOOLS.md + + # Count tools + TOOL_COUNT=$(grep -c "server\.tool(" src/index.ts || true) + echo "**Total tools: ${TOOL_COUNT}**" >> TOOLS.md + echo "" >> TOOLS.md + + # Extract tool names and descriptions + echo "| Tool | Description |" >> TOOLS.md + echo "|------|-------------|" >> TOOLS.md + + grep -A1 "server\.tool(" src/index.ts | grep -E "^\s*'" | while read -r line; do + TOOL_NAME=$(echo "$line" | sed "s/.*'\([^']*\)'.*/\1/") + # Get next line for description + DESC=$(grep -A2 "'${TOOL_NAME}'" src/index.ts | grep -E "^\s*'" | tail -1 | sed "s/.*'\([^']*\)'.*/\1/" || echo "") + echo "| \`${TOOL_NAME}\` | ${DESC} |" >> TOOLS.md + done + + echo "" >> TOOLS.md + echo "---" >> TOOLS.md + echo "*Generated by MCP Tool Inventory workflow*" >> TOOLS.md + + cat TOOLS.md + + - name: Upload inventory artifact + uses: actions/upload-artifact@v4 + with: + name: tool-inventory + path: TOOLS.md + retention-days: 90 diff --git a/validate/auto_detect_platform.php b/validate/auto_detect_platform.php index 49d04af..256627d 100755 --- a/validate/auto_detect_platform.php +++ b/validate/auto_detect_platform.php @@ -52,6 +52,7 @@ class AutoDetectPlatform extends CLIApp 'wordpress' => ['score' => 0, 'indicators' => []], 'mobile' => ['score' => 0, 'indicators' => []], 'api' => ['score' => 0, 'indicators' => []], + 'mcp-server' => ['score' => 0, 'indicators' => []], 'documentation' => ['score' => 0, 'indicators' => []], 'generic' => ['score' => 0, 'indicators' => []], ]; @@ -131,7 +132,8 @@ class AutoDetectPlatform extends CLIApp $this->detectWordPress($repoPath); $this->detectMobile($repoPath); $this->detectAPI($repoPath); - + $this->detectMcpServer($repoPath); + // Determine platform $this->determinePlatform(); } @@ -584,6 +586,78 @@ class AutoDetectPlatform extends CLIApp ]; } + private function detectMcpServer(string $repoPath): void + { + $score = 0; + $indicators = []; + + // Check for MCP SDK in package.json + if (file_exists("{$repoPath}/package.json")) { + $content = @file_get_contents("{$repoPath}/package.json"); + if ($content && strpos($content, '@modelcontextprotocol/sdk') !== false) { + $score += 0.5; + $indicators[] = "Found @modelcontextprotocol/sdk in package.json"; + } + } + + // Check for MCP server entry point with McpServer usage + if (file_exists("{$repoPath}/src/index.ts")) { + $content = @file_get_contents("{$repoPath}/src/index.ts"); + if ($content) { + if (strpos($content, 'McpServer') !== false) { + $score += 0.3; + $indicators[] = "Found McpServer import in src/index.ts"; + } + if (strpos($content, 'server.tool(') !== false) { + $score += 0.1; + $toolCount = substr_count($content, 'server.tool('); + $indicators[] = "Found {$toolCount} tool registrations in src/index.ts"; + } + if (strpos($content, 'StdioServerTransport') !== false) { + $score += 0.1; + $indicators[] = "Found StdioServerTransport (stdio MCP transport)"; + } + } + } + + // Check for the standard 4-file MCP structure + $mcpFiles = ['src/index.ts', 'src/client.ts', 'src/config.ts', 'src/types.ts']; + $foundCount = 0; + foreach ($mcpFiles as $file) { + if (file_exists("{$repoPath}/{$file}")) { + $foundCount++; + } + } + if ($foundCount === 4) { + $score += 0.1; + $indicators[] = "Found standard MCP 4-file src/ structure"; + } + + // Check for setup wizard + if (file_exists("{$repoPath}/scripts/setup.mjs")) { + $score += 0.05; + $indicators[] = "Found interactive setup wizard"; + } + + // Check for .mokostandards platform declaration + $mokoFiles = ["{$repoPath}/.gitea/.mokostandards", "{$repoPath}/.github/.mokostandards"]; + foreach ($mokoFiles as $mokoFile) { + if (file_exists($mokoFile)) { + $content = @file_get_contents($mokoFile); + if ($content && stripos($content, 'mcp-server') !== false) { + $score += 0.2; + $indicators[] = "Found explicit mcp-server platform declaration"; + break; + } + } + } + + $this->detectionResults['mcp-server'] = [ + 'score' => min(1.0, $score), + 'indicators' => $indicators, + ]; + } + private function determinePlatform(): void { // Find platform with highest score above threshold @@ -611,6 +685,7 @@ class AutoDetectPlatform extends CLIApp 'wordpress' => 'wordpress-repository.tf', 'mobile' => 'mobile-app-repository.tf', 'api' => 'api-repository.tf', + 'mcp-server' => 'mcp-server.tf', 'documentation' => 'documentation-repository.tf', 'standards' => 'standards-repository.tf', 'generic' => 'default-repository.tf',