Public Access
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) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,485 @@
|
||||
/**
|
||||
* MCP Server Repository Structure Definition
|
||||
* Standard repository structure for Model Context Protocol (MCP) server projects
|
||||
*
|
||||
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
* 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 |
|
||||
|
||||
|
||||
@@ -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<string, ProjectPluginInterface> Instantiated plugins */
|
||||
|
||||
@@ -0,0 +1,353 @@
|
||||
<?php
|
||||
/**
|
||||
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
*
|
||||
* 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',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"apiPath": "A:/MokoStandards-API",
|
||||
"standardsPath": "A:/MokoStandards",
|
||||
"giteaUrl": "https://git.mokoconsulting.tech",
|
||||
"giteaToken": "your-gitea-api-token"
|
||||
}
|
||||
@@ -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 <hello@mokoconsulting.tech>"
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
*
|
||||
* 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<StandardsConfig> {
|
||||
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<StandardsConfig>;
|
||||
|
||||
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" }`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,453 @@
|
||||
#!/usr/bin/env node
|
||||
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
*
|
||||
* 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<void> {
|
||||
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);
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
*
|
||||
* 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<ExecResult> {
|
||||
const scriptPath = resolve(this.apiPath, 'cli', script);
|
||||
return this.run('php', [scriptPath, ...args]);
|
||||
}
|
||||
|
||||
async runValidate(script: string, args: string[] = []): Promise<ExecResult> {
|
||||
const scriptPath = resolve(this.apiPath, 'validate', script);
|
||||
return this.run('php', [scriptPath, ...args]);
|
||||
}
|
||||
|
||||
private run(command: string, args: string[]): Promise<ExecResult> {
|
||||
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),
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
*
|
||||
* 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;
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
# MCP Server Auto-Release
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
# 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] <github-actions[bot]@users.noreply.github.com>"
|
||||
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
|
||||
@@ -0,0 +1,61 @@
|
||||
# MCP Server Build & Validation
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
# 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
|
||||
@@ -0,0 +1,105 @@
|
||||
# MCP SDK Version Check
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
# 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 }}
|
||||
@@ -0,0 +1,57 @@
|
||||
# MCP Tool Inventory
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
# 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
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user