Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d5d581883 | |||
| bd5f676e0a | |||
| 847263dd86 | |||
| 6e540f64c4 | |||
| cf02738930 | |||
| 455d4c8a19 | |||
| 8286d493b9 | |||
| b740152d67 | |||
| a59091e348 | |||
| ee3d58c20f |
@@ -0,0 +1,49 @@
|
|||||||
|
# mcp_mokobackup
|
||||||
|
|
||||||
|
MCP server for database and file backups across Dolibarr, Joomla/Akeeba, Gitea, and file-based environments.
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Package** | `@mokoconsulting/backup-mcp` |
|
||||||
|
| **Entry** | `dist/index.js` |
|
||||||
|
| **Config** | `~/.mcp_mokobackup.json` (override: `BACKUP_MCP_CONFIG` env var) |
|
||||||
|
| **Language** | TypeScript |
|
||||||
|
| **Branch** | develop on `dev`, merge to `main` (protected) |
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install # Install dependencies
|
||||||
|
npm run build # Compile TypeScript → dist/
|
||||||
|
npm run dev # Watch mode
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── index.ts # MCP server entry, tool registration
|
||||||
|
├── config.ts # Loads ~/.mcp_mokobackup.json, resolves targets
|
||||||
|
├── client.ts # Backup execution logic
|
||||||
|
├── akeeba.ts # Akeeba Backup API integration (Joomla sites)
|
||||||
|
├── mokobackup.ts # MokoJoomBackup REST API integration
|
||||||
|
└── types.ts # BackupConfig, BackupTarget types
|
||||||
|
```
|
||||||
|
|
||||||
|
- Config defines **targets** — each target has a type (akeeba, dolibarr, mysql, files, gitea-db, gitea-files)
|
||||||
|
- Client-specific targets go in client repo configs, not global
|
||||||
|
- Dolibarr backups read `conf.php` via SSH to get DB credentials
|
||||||
|
|
||||||
|
## Config
|
||||||
|
|
||||||
|
Default config at `~/.mcp_mokobackup.json`. Client repos override via `BACKUP_MCP_CONFIG` env var pointing to their own config file (e.g. `A:/client-clarksvillefurs/.mcp_mokobackup.json`).
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- **Never commit** `.claude/`, `.mcp.json`, `TODO.md`, `*.min.css`/`*.min.js`
|
||||||
|
- **Attribution**: `Authored-by: Moko Consulting`
|
||||||
|
- **Workflow directory**: `.mokogitea/` (not `.gitea/` or `.github/`)
|
||||||
|
- **Wiki**: documentation lives in the Gitea wiki, not `docs/` files
|
||||||
|
- **Standards**: [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)
|
||||||
@@ -0,0 +1,236 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: moko-platform.CI
|
||||||
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
||||||
|
# PATH: /templates/workflows/universal/pr-check.yml.template
|
||||||
|
# VERSION: 05.00.00
|
||||||
|
# BRIEF: PR gate — branch policy + code validation before merge
|
||||||
|
|
||||||
|
name: "Universal: PR Check"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize, reopened, edited]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# ── Branch Policy ──────────────────────────────────────────────────────
|
||||||
|
branch-policy:
|
||||||
|
name: Branch Policy
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check branch merge target
|
||||||
|
run: |
|
||||||
|
HEAD="${{ github.head_ref }}"
|
||||||
|
BASE="${{ github.base_ref }}"
|
||||||
|
|
||||||
|
echo "PR: ${HEAD} → ${BASE}"
|
||||||
|
|
||||||
|
ALLOWED=true
|
||||||
|
REASON=""
|
||||||
|
|
||||||
|
case "$HEAD" in
|
||||||
|
feature/*|feat/*)
|
||||||
|
if [ "$BASE" != "dev" ]; then
|
||||||
|
ALLOWED=false
|
||||||
|
REASON="Feature branches must target 'dev', not '${BASE}'"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
fix/*|bugfix/*)
|
||||||
|
if [ "$BASE" != "dev" ]; then
|
||||||
|
ALLOWED=false
|
||||||
|
REASON="Fix branches must target 'dev', not '${BASE}'"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
patch/*)
|
||||||
|
if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then
|
||||||
|
ALLOWED=false
|
||||||
|
REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
hotfix/*)
|
||||||
|
if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
|
||||||
|
ALLOWED=false
|
||||||
|
REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
rc)
|
||||||
|
if [ "$BASE" != "main" ]; then
|
||||||
|
ALLOWED=false
|
||||||
|
REASON="RC branch can only merge into 'main', not '${BASE}'"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
dev)
|
||||||
|
if [ "$BASE" != "main" ]; then
|
||||||
|
ALLOWED=false
|
||||||
|
REASON="Dev branch can only merge into 'main', not '${BASE}'"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ "$ALLOWED" = false ]; then
|
||||||
|
echo "::error::${REASON}"
|
||||||
|
echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "${REASON}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Branch policy: OK (${HEAD} → ${BASE})"
|
||||||
|
echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# ── Code Validation ────────────────────────────────────────────────────
|
||||||
|
validate:
|
||||||
|
name: Validate PR
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Detect platform
|
||||||
|
id: platform
|
||||||
|
run: |
|
||||||
|
# Read platform from XML manifest (<platform> tag) or plain text fallback
|
||||||
|
PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1)
|
||||||
|
[ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]')
|
||||||
|
[ -z "$PLATFORM" ] && PLATFORM="generic"
|
||||||
|
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
|
||||||
|
run: |
|
||||||
|
if ! command -v php &> /dev/null; then
|
||||||
|
sudo apt-get update -qq
|
||||||
|
sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: PHP syntax check
|
||||||
|
if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
|
||||||
|
run: |
|
||||||
|
ERRORS=0
|
||||||
|
while IFS= read -r -d '' file; do
|
||||||
|
if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0)
|
||||||
|
echo "PHP lint: ${ERRORS} error(s)"
|
||||||
|
[ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; }
|
||||||
|
|
||||||
|
- name: Validate platform manifest
|
||||||
|
run: |
|
||||||
|
PLATFORM="${{ steps.platform.outputs.platform }}"
|
||||||
|
case "$PLATFORM" in
|
||||||
|
joomla)
|
||||||
|
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
||||||
|
if [ -z "$MANIFEST" ]; then
|
||||||
|
echo "::warning::No Joomla manifest found (WaaS site)"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "Manifest: ${MANIFEST}"
|
||||||
|
if command -v php &> /dev/null; then
|
||||||
|
php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$MANIFEST'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::Manifest XML is malformed"; exit 1; }
|
||||||
|
fi
|
||||||
|
for ELEMENT in name version description; do
|
||||||
|
grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; }
|
||||||
|
done
|
||||||
|
echo "Joomla manifest valid"
|
||||||
|
;;
|
||||||
|
dolibarr)
|
||||||
|
MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1)
|
||||||
|
if [ -z "$MOD_FILE" ]; then
|
||||||
|
echo "::error::No mod*.class.php found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Dolibarr module: ${MOD_FILE}"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Generic platform — no manifest validation"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
- name: Check update stream format
|
||||||
|
run: |
|
||||||
|
PLATFORM="${{ steps.platform.outputs.platform }}"
|
||||||
|
case "$PLATFORM" in
|
||||||
|
joomla)
|
||||||
|
if [ -f "updates.xml" ]; then
|
||||||
|
if command -v php &> /dev/null; then
|
||||||
|
php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('updates.xml'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::updates.xml is malformed"; exit 1; }
|
||||||
|
fi
|
||||||
|
echo "updates.xml valid"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
dolibarr)
|
||||||
|
[ -f "update.txt" ] && echo "update.txt present" || echo "::warning::No update.txt"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
- name: Check changelog has unreleased entry
|
||||||
|
run: |
|
||||||
|
if [ ! -f "CHANGELOG.md" ]; then
|
||||||
|
echo "::warning::No CHANGELOG.md found"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
# Check for content under [Unreleased] section
|
||||||
|
if ! grep -q "## \[Unreleased\]" CHANGELOG.md; then
|
||||||
|
echo "::error::CHANGELOG.md missing [Unreleased] section"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# Check there's at least one entry (Added/Changed/Fixed/Removed) under Unreleased
|
||||||
|
UNRELEASED_CONTENT=$(sed -n '/## \[Unreleased\]/,/## \[/p' CHANGELOG.md | grep -cE '^\s*-\s' || true)
|
||||||
|
if [ "$UNRELEASED_CONTENT" -eq 0 ]; then
|
||||||
|
echo "::error::CHANGELOG.md [Unreleased] section has no entries. Add a changelog entry describing your changes."
|
||||||
|
echo "## Changelog Check: Failed" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "The \`[Unreleased]\` section in CHANGELOG.md has no entries." >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Add a line like \`- Description of your change\` under a heading (\`### Added\`, \`### Changed\`, \`### Fixed\`, etc.)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Changelog: ${UNRELEASED_CONTENT} entry/entries in [Unreleased]"
|
||||||
|
|
||||||
|
- name: Verify package source
|
||||||
|
run: |
|
||||||
|
SOURCE_DIR="src"
|
||||||
|
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
||||||
|
if [ ! -d "$SOURCE_DIR" ]; then
|
||||||
|
echo "::warning::No src/ or htdocs/ directory"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l)
|
||||||
|
echo "Source: ${FILE_COUNT} files"
|
||||||
|
[ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; }
|
||||||
|
|
||||||
|
# ── Pre-Release RC Build ─────────────────────────────────────────────────
|
||||||
|
pre-release:
|
||||||
|
name: Build RC Package
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [branch-policy, validate]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Trigger RC pre-release
|
||||||
|
env:
|
||||||
|
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
BRANCH: ${{ github.head_ref }}
|
||||||
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
run: |
|
||||||
|
curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
|
||||||
|
echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## [1.0] — 2026-05-08
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Initial release
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Contributing
|
||||||
|
|
||||||
|
See [standards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki).
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# backup-mcp
|
||||||
|
|
||||||
|
MCP server for database and file backups across Dolibarr and Joomla environments
|
||||||
|
|
||||||
|
  
|
||||||
|
|
||||||
|
|
||||||
|
Model Context Protocol server for database dumps, file backups, and Akeeba Backup integration on Joomla sites.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Type** | MCP Server |
|
||||||
|
| **Language** | Node.js |
|
||||||
|
| **Tools** | 11 tools (6 SSH-based + 5 Akeeba API) |
|
||||||
|
| **License** | GPL-3.0-or-later |
|
||||||
|
| **Platform** | [Gitea](https://git.mokoconsulting.tech/MokoConsulting/backup-mcp) (primary) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
backup-mcp provides two backup strategies through a single MCP server:
|
||||||
|
|
||||||
|
| Strategy | Method | Tools |
|
||||||
|
|----------|--------|-------|
|
||||||
|
| **SSH Backups** | MySQL/PostgreSQL dumps and tar archives via SSH | `backup_database`, `backup_files`, `backup_list`, `backup_prune`, `backup_status`, `backup_list_targets` |
|
||||||
|
| **Akeeba Backups** | Joomla Web Services API (`/api/index.php/v1/akeebabackup/*`) | `akeeba_backup`, `akeeba_list`, `akeeba_download`, `akeeba_delete`, `akeeba_profiles` |
|
||||||
|
|
||||||
|
Each client repo has its own `.backup-mcp.json` scoped via the `BACKUP_MCP_CONFIG` env var in `.mcp.json`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Wiki Pages
|
||||||
|
|
||||||
|
### Reference
|
||||||
|
|
||||||
|
- [Tools Reference](https://git.mokoconsulting.tech/MokoConsulting/backup-mcp/wiki/Tools-Reference) -- all 11 tools with descriptions
|
||||||
|
- [Akeeba Integration](https://git.mokoconsulting.tech/MokoConsulting/backup-mcp/wiki/Akeeba-Integration) -- Akeeba Backup Pro setup, requirements, per-client workspace config
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Wikis
|
||||||
|
|
||||||
|
| Repo | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| [ssh-mcp](https://git.mokoconsulting.tech/MokoConsulting/ssh-mcp/wiki) | SSH server management (used for SSH-based backups) |
|
||||||
|
| [joomla-api-mcp](https://git.mokoconsulting.tech/MokoConsulting/joomla-api-mcp/wiki) | Joomla Web Services API MCP |
|
||||||
|
| [deploy-mcp](https://git.mokoconsulting.tech/MokoConsulting/deploy-mcp/wiki) | Git-based deployment MCP |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **[MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki)** -- central standards hub for all Moko Consulting projects.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Full documentation is available on the [Wiki](https://git.mokoconsulting.tech/MokoConsulting/backup-mcp/wiki).
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
See the wiki for development guidelines and contribution instructions.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the GNU General Public License v3.0 or later -- see the [LICENSE](LICENSE) file.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*[Moko Consulting](https://mokoconsulting.tech) -- [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Security
|
||||||
|
|
||||||
|
Report to hello@mokoconsulting.tech.
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"defaultTarget": "dolibarr-db",
|
||||||
|
"targets": {
|
||||||
|
"dolibarr-db": {
|
||||||
|
"name": "dolibarr",
|
||||||
|
"type": "mysql",
|
||||||
|
"sshHost": "crm.mokoconsulting.tech",
|
||||||
|
"sshUser": "mokoconsulting",
|
||||||
|
"sshKeyPath": "~/.ssh/id_ed25519",
|
||||||
|
"database": "dolibarr",
|
||||||
|
"dbUser": "dolibarr",
|
||||||
|
"dbPassword": "your-db-password",
|
||||||
|
"localBackupDir": "~/backups/dolibarr"
|
||||||
|
},
|
||||||
|
"joomla-db": {
|
||||||
|
"name": "joomla",
|
||||||
|
"type": "mysql",
|
||||||
|
"sshHost": "waas.mokoconsulting.tech",
|
||||||
|
"sshUser": "mokoconsulting",
|
||||||
|
"sshKeyPath": "~/.ssh/id_ed25519",
|
||||||
|
"database": "joomla",
|
||||||
|
"dbUser": "joomla",
|
||||||
|
"dbPassword": "your-db-password",
|
||||||
|
"remotePaths": ["/var/www/html/images", "/var/www/html/media"],
|
||||||
|
"localBackupDir": "~/backups/joomla"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "@mokoconsulting/backup-mcp",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "MCP server for database and file backups across Dolibarr and Joomla environments",
|
||||||
|
"type": "module",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"bin": { "backup-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,129 @@
|
|||||||
|
import * as https from 'node:https';
|
||||||
|
import * as http from 'node:http';
|
||||||
|
import { mkdirSync } from 'node:fs';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import type { BackupTarget, BackupResult, AkeebaBackupRecord } from './types.js';
|
||||||
|
|
||||||
|
const TIMEOUT_MS = 300_000; // 5 min for backup operations
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Akeeba Backup client using Joomla Web Services API
|
||||||
|
* Endpoint: /api/index.php/v1/akeebabackup/*
|
||||||
|
* Auth: Bearer token (Joomla API token)
|
||||||
|
*/
|
||||||
|
export class AkeebaClient {
|
||||||
|
private readonly target: BackupTarget;
|
||||||
|
private readonly baseUrl: string;
|
||||||
|
private readonly headers: Record<string, string>;
|
||||||
|
|
||||||
|
constructor(target: BackupTarget) {
|
||||||
|
this.target = target;
|
||||||
|
const site = (target.siteUrl ?? '').replace(/\/+$/, '');
|
||||||
|
this.baseUrl = `${site}/api/index.php/v1/akeebabackup`;
|
||||||
|
this.headers = {
|
||||||
|
'Authorization': `Bearer ${target.secretWord}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/vnd.api+json',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private request(url: string, method: string, body?: unknown): Promise<{ status: number; data: unknown }> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const parsed = new URL(url);
|
||||||
|
const mod = parsed.protocol === 'https:' ? https : http;
|
||||||
|
const payload = body ? JSON.stringify(body) : undefined;
|
||||||
|
const opts: http.RequestOptions = {
|
||||||
|
hostname: parsed.hostname,
|
||||||
|
port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
|
||||||
|
path: parsed.pathname + parsed.search,
|
||||||
|
method,
|
||||||
|
headers: { ...this.headers, ...(payload ? { 'Content-Length': Buffer.byteLength(payload) } : {}) },
|
||||||
|
timeout: TIMEOUT_MS,
|
||||||
|
};
|
||||||
|
|
||||||
|
const req = mod.request(opts, (res) => {
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
res.on('data', (c: Buffer) => chunks.push(c));
|
||||||
|
res.on('end', () => {
|
||||||
|
const raw = Buffer.concat(chunks).toString();
|
||||||
|
let data: unknown;
|
||||||
|
try { data = JSON.parse(raw); } catch { data = raw; }
|
||||||
|
resolve({ status: res.statusCode ?? 0, data });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
req.on('error', reject);
|
||||||
|
req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
|
||||||
|
if (payload) req.write(payload);
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async startBackup(profileId?: number, description?: string): Promise<BackupResult> {
|
||||||
|
try {
|
||||||
|
const profile = profileId ?? this.target.profileId ?? 1;
|
||||||
|
const desc = description ?? `MCP backup ${new Date().toISOString()}`;
|
||||||
|
const res = await this.request(`${this.baseUrl}/backup`, 'POST', {
|
||||||
|
profile: profile,
|
||||||
|
description: desc,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status >= 400) {
|
||||||
|
return { success: false, message: `Akeeba backup failed: ${JSON.stringify(res.data)}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, message: `Akeeba backup started (profile ${profile}): ${desc}` };
|
||||||
|
} catch (err) {
|
||||||
|
return { success: false, message: `Akeeba backup failed: ${err}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async listBackups(limit = 20): Promise<AkeebaBackupRecord[]> {
|
||||||
|
try {
|
||||||
|
const res = await this.request(`${this.baseUrl}/backups?page[limit]=${limit}`, 'GET');
|
||||||
|
if (res.status >= 400) return [];
|
||||||
|
const body = res.data as { data?: Array<{ attributes: AkeebaBackupRecord }> };
|
||||||
|
return (body.data ?? []).map(d => d.attributes);
|
||||||
|
} catch { return []; }
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteBackup(id: string): Promise<BackupResult> {
|
||||||
|
try {
|
||||||
|
const res = await this.request(`${this.baseUrl}/backup/${id}`, 'DELETE');
|
||||||
|
if (res.status >= 400) return { success: false, message: `Delete failed: ${JSON.stringify(res.data)}` };
|
||||||
|
return { success: true, message: `Deleted Akeeba backup ${id}` };
|
||||||
|
} catch (err) {
|
||||||
|
return { success: false, message: `Delete failed: ${err}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async downloadBackup(id: string): Promise<BackupResult> {
|
||||||
|
try {
|
||||||
|
mkdirSync(this.target.localBackupDir, { recursive: true });
|
||||||
|
const res = await this.request(`${this.baseUrl}/backup/${id}/download`, 'GET');
|
||||||
|
|
||||||
|
if (res.status >= 400) {
|
||||||
|
return { success: false, message: `Download failed: ${JSON.stringify(res.data)}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
const filename = `${this.target.name}-akeeba-${id}-${new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)}.jpa`;
|
||||||
|
const localFile = join(this.target.localBackupDir, filename);
|
||||||
|
const fs = await import('node:fs/promises');
|
||||||
|
|
||||||
|
if (typeof res.data === 'string') {
|
||||||
|
const buffer = Buffer.from(res.data, 'base64');
|
||||||
|
await fs.writeFile(localFile, buffer);
|
||||||
|
return { success: true, message: `Downloaded backup ${id}`, filePath: localFile, sizeBytes: buffer.length };
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.writeFile(localFile, JSON.stringify(res.data));
|
||||||
|
return { success: true, message: `Downloaded backup ${id}`, filePath: localFile };
|
||||||
|
} catch (err) {
|
||||||
|
return { success: false, message: `Download failed: ${err}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getProfiles(): Promise<unknown> {
|
||||||
|
const res = await this.request(`${this.baseUrl}/profiles`, 'GET');
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
// Uses execFile (safe, no shell interpolation) for all SSH/command execution
|
||||||
|
import { execFile as execFileCb } from 'node:child_process';
|
||||||
|
import { promisify } from 'node:util';
|
||||||
|
import { mkdirSync, readdirSync, statSync } from 'node:fs';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import type { BackupTarget, BackupResult } from './types.js';
|
||||||
|
|
||||||
|
const execFile = promisify(execFileCb);
|
||||||
|
const TIMEOUT = 300_000; // 5 min for large backups
|
||||||
|
|
||||||
|
export class BackupClient {
|
||||||
|
private readonly target: BackupTarget;
|
||||||
|
|
||||||
|
constructor(target: BackupTarget) {
|
||||||
|
this.target = target;
|
||||||
|
mkdirSync(target.localBackupDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
private sshArgs(cmd: string): string[] {
|
||||||
|
const args = ['-o', 'StrictHostKeyChecking=accept-new', '-o', 'BatchMode=yes'];
|
||||||
|
if (this.target.sshPort) args.push('-p', String(this.target.sshPort));
|
||||||
|
if (this.target.sshKeyPath) args.push('-i', this.target.sshKeyPath);
|
||||||
|
args.push(`${this.target.sshUser}@${this.target.sshHost}`, cmd);
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
private timestamp(): string {
|
||||||
|
return new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
||||||
|
}
|
||||||
|
|
||||||
|
async dumpDatabase(): Promise<BackupResult> {
|
||||||
|
const ts = this.timestamp();
|
||||||
|
const filename = `${this.target.name}-db-${ts}.sql.gz`;
|
||||||
|
const localFile = join(this.target.localBackupDir, filename);
|
||||||
|
const dumpCmd = this.target.type === 'mysql'
|
||||||
|
? `mysqldump -u ${this.target.dbUser} -p'${this.target.dbPassword}' ${this.target.database} | gzip`
|
||||||
|
: `PGPASSWORD='${this.target.dbPassword}' pg_dump -U ${this.target.dbUser} ${this.target.database} | gzip`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { stdout } = await execFile('ssh', this.sshArgs(dumpCmd), { timeout: TIMEOUT, maxBuffer: 500 * 1024 * 1024 });
|
||||||
|
const fs = await import('node:fs/promises');
|
||||||
|
await fs.writeFile(localFile, stdout, 'binary');
|
||||||
|
const stat = statSync(localFile);
|
||||||
|
return { success: true, message: `Database backup: ${filename}`, filePath: localFile, sizeBytes: stat.size };
|
||||||
|
} catch (err) {
|
||||||
|
return { success: false, message: `Database backup failed: ${err}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async backupFiles(): Promise<BackupResult> {
|
||||||
|
const ts = this.timestamp();
|
||||||
|
const filename = `${this.target.name}-files-${ts}.tar.gz`;
|
||||||
|
const localFile = join(this.target.localBackupDir, filename);
|
||||||
|
const paths = (this.target.remotePaths ?? []).join(' ');
|
||||||
|
const tarCmd = `tar czf - ${paths}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { stdout } = await execFile('ssh', this.sshArgs(tarCmd), { timeout: TIMEOUT, maxBuffer: 500 * 1024 * 1024 });
|
||||||
|
const fs = await import('node:fs/promises');
|
||||||
|
await fs.writeFile(localFile, stdout, 'binary');
|
||||||
|
const stat = statSync(localFile);
|
||||||
|
return { success: true, message: `File backup: ${filename}`, filePath: localFile, sizeBytes: stat.size };
|
||||||
|
} catch (err) {
|
||||||
|
return { success: false, message: `File backup failed: ${err}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
listBackups(): { name: string; size: number; date: Date }[] {
|
||||||
|
try {
|
||||||
|
return readdirSync(this.target.localBackupDir)
|
||||||
|
.filter(f => f.startsWith(this.target.name))
|
||||||
|
.map(f => {
|
||||||
|
const stat = statSync(join(this.target.localBackupDir, f));
|
||||||
|
return { name: f, size: stat.size, date: stat.mtime };
|
||||||
|
})
|
||||||
|
.sort((a, b) => b.date.getTime() - a.date.getTime());
|
||||||
|
} catch { return []; }
|
||||||
|
}
|
||||||
|
|
||||||
|
async parseDolibarrConf(): Promise<{ dbHost: string; dbName: string; dbUser: string; dbPass: string; dataRoot: string }> {
|
||||||
|
const confPath = this.target.confPath ?? '/htdocs/conf/conf.php';
|
||||||
|
const cmd = `cat ${confPath}`;
|
||||||
|
const { stdout } = await execFile('ssh', this.sshArgs(cmd), { timeout: TIMEOUT });
|
||||||
|
const get = (key: string) => {
|
||||||
|
const m = stdout.match(new RegExp(`\\$${key}\\s*=\\s*['"]([^'"]*)`));
|
||||||
|
if (!m) throw new Error(`Could not find $${key} in ${confPath}`);
|
||||||
|
return m[1];
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
dbHost: get('dolibarr_main_db_host'),
|
||||||
|
dbName: get('dolibarr_main_db_name'),
|
||||||
|
dbUser: get('dolibarr_main_db_user'),
|
||||||
|
dbPass: get('dolibarr_main_db_pass'),
|
||||||
|
dataRoot: get('dolibarr_main_data_root'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async dumpDolibarr(): Promise<BackupResult> {
|
||||||
|
const ts = this.timestamp();
|
||||||
|
const conf = await this.parseDolibarrConf();
|
||||||
|
|
||||||
|
// 1. Database dump
|
||||||
|
const dbFilename = `${this.target.name}-db-${ts}.sql.gz`;
|
||||||
|
const dbLocalFile = join(this.target.localBackupDir, dbFilename);
|
||||||
|
const dumpCmd = `mysqldump -h ${conf.dbHost} -u ${conf.dbUser} -p'${conf.dbPass}' ${conf.dbName} | gzip`;
|
||||||
|
try {
|
||||||
|
const { stdout: dbOut } = await execFile('ssh', this.sshArgs(dumpCmd), { timeout: TIMEOUT, maxBuffer: 500 * 1024 * 1024 });
|
||||||
|
const fs = await import('node:fs/promises');
|
||||||
|
await fs.writeFile(dbLocalFile, dbOut, 'binary');
|
||||||
|
|
||||||
|
// 2. Documents + custom directories
|
||||||
|
const filesFilename = `${this.target.name}-files-${ts}.tar.gz`;
|
||||||
|
const filesLocalFile = join(this.target.localBackupDir, filesFilename);
|
||||||
|
const customDir = conf.dataRoot.replace(/\/documents\/?$/, '/custom');
|
||||||
|
const tarCmd = `tar czf - ${conf.dataRoot} ${customDir} 2>/dev/null`;
|
||||||
|
const { stdout: tarOut } = await execFile('ssh', this.sshArgs(tarCmd), { timeout: TIMEOUT, maxBuffer: 500 * 1024 * 1024 });
|
||||||
|
await fs.writeFile(filesLocalFile, tarOut, 'binary');
|
||||||
|
|
||||||
|
const dbStat = statSync(dbLocalFile);
|
||||||
|
const filesStat = statSync(filesLocalFile);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `Dolibarr backup complete:\n DB: ${dbFilename} (${(dbStat.size / 1024 / 1024).toFixed(1)} MB)\n Files: ${filesFilename} (${(filesStat.size / 1024 / 1024).toFixed(1)} MB)`,
|
||||||
|
filePath: dbLocalFile,
|
||||||
|
sizeBytes: dbStat.size + filesStat.size,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
return { success: false, message: `Dolibarr backup failed: ${err}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async pruneBackups(olderThanDays: number): Promise<BackupResult> {
|
||||||
|
const cutoff = Date.now() - olderThanDays * 86400000;
|
||||||
|
const fs = await import('node:fs/promises');
|
||||||
|
const backups = this.listBackups().filter(b => b.date.getTime() < cutoff);
|
||||||
|
for (const b of backups) {
|
||||||
|
await fs.unlink(join(this.target.localBackupDir, b.name));
|
||||||
|
}
|
||||||
|
return { success: true, message: `Pruned ${backups.length} backups older than ${olderThanDays} days` };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { readFile } from 'node:fs/promises';
|
||||||
|
import { resolve } from 'node:path';
|
||||||
|
import { homedir } from 'node:os';
|
||||||
|
import type { BackupConfig, BackupTarget } from './types.js';
|
||||||
|
|
||||||
|
const CONFIG_FILENAME = '.mcp_mokobackup.json';
|
||||||
|
|
||||||
|
export async function loadConfig(): Promise<BackupConfig> {
|
||||||
|
const configPath = process.env.BACKUP_MCP_CONFIG
|
||||||
|
? resolve(process.env.BACKUP_MCP_CONFIG)
|
||||||
|
: resolve(homedir(), CONFIG_FILENAME);
|
||||||
|
|
||||||
|
const raw = await readFile(configPath, 'utf-8');
|
||||||
|
const parsed = JSON.parse(raw) as Partial<BackupConfig>;
|
||||||
|
if (!parsed.targets || Object.keys(parsed.targets).length === 0) {
|
||||||
|
throw new Error(`No targets in ${configPath}`);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
targets: parsed.targets,
|
||||||
|
defaultTarget: parsed.defaultTarget ?? Object.keys(parsed.targets)[0],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTarget(config: BackupConfig, name?: string): BackupTarget {
|
||||||
|
const key = name ?? config.defaultTarget;
|
||||||
|
const target = config.targets[key];
|
||||||
|
if (!target) throw new Error(`Target "${key}" not found. Available: ${Object.keys(config.targets).join(', ')}`);
|
||||||
|
return target;
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||||
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { loadConfig, getTarget } from './config.js';
|
||||||
|
import { BackupClient } from './client.js';
|
||||||
|
import { AkeebaClient } from './akeeba.js';
|
||||||
|
import { MokoBackupClient } from './mokobackup.js';
|
||||||
|
import type { BackupConfig } from './types.js';
|
||||||
|
|
||||||
|
let config: BackupConfig;
|
||||||
|
|
||||||
|
function clientFor(t?: string): BackupClient { return new BackupClient(getTarget(config, t)); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the appropriate Joomla backup client based on target type.
|
||||||
|
* MokoBackup and Akeeba have the same interface — auto-detect which to use.
|
||||||
|
*/
|
||||||
|
function joomlaBackupFor(t?: string): AkeebaClient | MokoBackupClient {
|
||||||
|
const target = getTarget(config, t);
|
||||||
|
if (target.type === 'mokobackup') return new MokoBackupClient(target);
|
||||||
|
return new AkeebaClient(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep legacy function for backwards compatibility
|
||||||
|
function akeebaFor(t?: string): AkeebaClient | MokoBackupClient { return joomlaBackupFor(t); }
|
||||||
|
function text(s: string) { return { content: [{ type: 'text' as const, text: s }] }; }
|
||||||
|
|
||||||
|
const T = { target: z.string().optional().describe('Backup target name') };
|
||||||
|
|
||||||
|
const server = new McpServer({ name: 'backup-mcp', version: '1.0.0' });
|
||||||
|
|
||||||
|
// ── SSH-based backups ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
server.tool('backup_database', 'Dump database (MySQL/PostgreSQL) to local backup via SSH', { ...T },
|
||||||
|
async ({ target }) => { const r = await clientFor(target).dumpDatabase(); return text(JSON.stringify(r, null, 2)); });
|
||||||
|
|
||||||
|
server.tool('backup_files', 'Backup remote directories to local tar.gz via SSH', { ...T },
|
||||||
|
async ({ target }) => { const r = await clientFor(target).backupFiles(); return text(JSON.stringify(r, null, 2)); });
|
||||||
|
|
||||||
|
server.tool('backup_list', 'List available local backups with sizes and dates', { ...T },
|
||||||
|
async ({ target }) => {
|
||||||
|
const backups = clientFor(target).listBackups();
|
||||||
|
if (backups.length === 0) return text('No backups found');
|
||||||
|
return text(backups.map(b => `${b.name} ${(b.size / 1024 / 1024).toFixed(1)} MB ${b.date.toISOString()}`).join('\n'));
|
||||||
|
});
|
||||||
|
|
||||||
|
server.tool('backup_prune', 'Delete local backups older than specified days', {
|
||||||
|
...T, days: z.number().describe('Delete backups older than this many days'),
|
||||||
|
}, async ({ target, days }) => { const r = await clientFor(target).pruneBackups(days); return text(r.message); });
|
||||||
|
|
||||||
|
server.tool('backup_status', 'Show backup disk usage and last backup info', { ...T },
|
||||||
|
async ({ target }) => {
|
||||||
|
const backups = clientFor(target).listBackups();
|
||||||
|
const totalMB = backups.reduce((s, b) => s + b.size, 0) / 1024 / 1024;
|
||||||
|
const last = backups[0];
|
||||||
|
return text(`Backups: ${backups.length}\nTotal size: ${totalMB.toFixed(1)} MB\nLast backup: ${last ? `${last.name} (${last.date.toISOString()})` : 'none'}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Akeeba Backup (Joomla sites) ─────────────────────────────────────
|
||||||
|
|
||||||
|
server.tool('akeeba_backup', 'Start a backup on a Joomla site (Akeeba or MokoBackup — auto-detected by target type)', {
|
||||||
|
...T, description: z.string().optional().describe('Backup description'),
|
||||||
|
}, async ({ target, description }) => {
|
||||||
|
const r = await akeebaFor(target).startBackup(undefined, description);
|
||||||
|
return text(JSON.stringify(r, null, 2));
|
||||||
|
});
|
||||||
|
|
||||||
|
server.tool('akeeba_list', 'List backup records on a Joomla site (Akeeba or MokoBackup)', {
|
||||||
|
...T, limit: z.number().optional().describe('Number of records (default 20)'),
|
||||||
|
}, async ({ target, limit }) => {
|
||||||
|
const records = await akeebaFor(target).listBackups(limit ?? 20);
|
||||||
|
if (records.length === 0) return text('No backups found');
|
||||||
|
return text(records.map(r =>
|
||||||
|
`#${r.id} ${r.status} ${r.description} ${r.backupstart} ${r.archivename} ${(r.total_size / 1024 / 1024).toFixed(1)} MB`
|
||||||
|
).join('\n'));
|
||||||
|
});
|
||||||
|
|
||||||
|
server.tool('akeeba_download', 'Download a backup archive from a Joomla site (Akeeba or MokoBackup)', {
|
||||||
|
...T, backup_id: z.string().describe('Backup record ID'),
|
||||||
|
}, async ({ target, backup_id }) => {
|
||||||
|
const r = await akeebaFor(target).downloadBackup(backup_id);
|
||||||
|
return text(JSON.stringify(r, null, 2));
|
||||||
|
});
|
||||||
|
|
||||||
|
server.tool('akeeba_delete', 'Delete a backup record from a Joomla site (Akeeba or MokoBackup)', {
|
||||||
|
...T, backup_id: z.string().describe('Backup record ID'),
|
||||||
|
}, async ({ target, backup_id }) => {
|
||||||
|
const r = await akeebaFor(target).deleteBackup(backup_id);
|
||||||
|
return text(r.message);
|
||||||
|
});
|
||||||
|
|
||||||
|
server.tool('akeeba_profiles', 'List backup profiles on a Joomla site (Akeeba or MokoBackup)', { ...T },
|
||||||
|
async ({ target }) => {
|
||||||
|
const profiles = await akeebaFor(target).getProfiles();
|
||||||
|
return text(JSON.stringify(profiles, null, 2));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// ── Dolibarr Backup ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
server.tool('dolibarr_backup', 'Backup a Dolibarr instance (DB + documents + custom) by reading conf.php via SSH', {
|
||||||
|
...T,
|
||||||
|
}, async ({ target }) => {
|
||||||
|
const r = await clientFor(target).dumpDolibarr();
|
||||||
|
return text(JSON.stringify(r, null, 2));
|
||||||
|
});
|
||||||
|
|
||||||
|
server.tool('dolibarr_conf', 'Read and display Dolibarr database settings from conf.php via SSH (no passwords shown)', {
|
||||||
|
...T,
|
||||||
|
}, async ({ target }) => {
|
||||||
|
const conf = await clientFor(target).parseDolibarrConf();
|
||||||
|
return text(`DB Host: ${conf.dbHost}\nDB Name: ${conf.dbName}\nDB User: ${conf.dbUser}\nData Root: ${conf.dataRoot}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── General ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
server.tool('backup_list_targets', 'List all configured backup targets', {},
|
||||||
|
async () => text(Object.entries(config.targets).map(([k, v]) => {
|
||||||
|
const loc = (v.type === 'akeeba' || v.type === 'mokobackup') ? v.siteUrl : `${v.sshUser}@${v.sshHost}`;
|
||||||
|
return `${k}${k === config.defaultTarget ? ' (default)' : ''}: ${v.type} @ ${loc} → ${v.localBackupDir}`;
|
||||||
|
}).join('\n')));
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
config = await loadConfig();
|
||||||
|
await server.connect(new StdioServerTransport());
|
||||||
|
}
|
||||||
|
main().catch(err => { console.error(err); process.exit(1); });
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
import * as https from 'node:https';
|
||||||
|
import * as http from 'node:http';
|
||||||
|
import { mkdirSync } from 'node:fs';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import type { BackupTarget, BackupResult, AkeebaBackupRecord } from './types.js';
|
||||||
|
|
||||||
|
const TIMEOUT_MS = 300_000; // 5 min for backup operations
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MokoJoomBackup client using Joomla Web Services API
|
||||||
|
* Endpoint: /api/index.php/v1/mokobackup/*
|
||||||
|
* Auth: Bearer token (Joomla API token)
|
||||||
|
*
|
||||||
|
* Wire-compatible with AkeebaClient — same interface, different base URL.
|
||||||
|
* The existing akeeba_* MCP tools work with both backends.
|
||||||
|
*/
|
||||||
|
export class MokoBackupClient {
|
||||||
|
private readonly target: BackupTarget;
|
||||||
|
private readonly baseUrl: string;
|
||||||
|
private readonly headers: Record<string, string>;
|
||||||
|
|
||||||
|
constructor(target: BackupTarget) {
|
||||||
|
this.target = target;
|
||||||
|
const site = (target.siteUrl ?? '').replace(/\/+$/, '');
|
||||||
|
this.baseUrl = `${site}/api/index.php/v1/mokobackup`;
|
||||||
|
this.headers = {
|
||||||
|
'Authorization': `Bearer ${target.secretWord}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/vnd.api+json',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private request(url: string, method: string, body?: unknown): Promise<{ status: number; data: unknown }> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const parsed = new URL(url);
|
||||||
|
const mod = parsed.protocol === 'https:' ? https : http;
|
||||||
|
const payload = body ? JSON.stringify(body) : undefined;
|
||||||
|
const opts: http.RequestOptions = {
|
||||||
|
hostname: parsed.hostname,
|
||||||
|
port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
|
||||||
|
path: parsed.pathname + parsed.search,
|
||||||
|
method,
|
||||||
|
headers: { ...this.headers, ...(payload ? { 'Content-Length': Buffer.byteLength(payload) } : {}) },
|
||||||
|
timeout: TIMEOUT_MS,
|
||||||
|
};
|
||||||
|
|
||||||
|
const req = mod.request(opts, (res) => {
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
res.on('data', (c: Buffer) => chunks.push(c));
|
||||||
|
res.on('end', () => {
|
||||||
|
const raw = Buffer.concat(chunks).toString();
|
||||||
|
let data: unknown;
|
||||||
|
try { data = JSON.parse(raw); } catch { data = raw; }
|
||||||
|
resolve({ status: res.statusCode ?? 0, data });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
req.on('error', reject);
|
||||||
|
req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
|
||||||
|
if (payload) req.write(payload);
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async startBackup(profileId?: number, description?: string): Promise<BackupResult> {
|
||||||
|
try {
|
||||||
|
const profile = profileId ?? this.target.profileId ?? 1;
|
||||||
|
const desc = description ?? `MCP backup ${new Date().toISOString()}`;
|
||||||
|
const res = await this.request(`${this.baseUrl}/backup`, 'POST', {
|
||||||
|
profile: profile,
|
||||||
|
description: desc,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status >= 400) {
|
||||||
|
return { success: false, message: `MokoBackup failed: ${JSON.stringify(res.data)}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
// MokoBackup returns {data: {success, message, record_id}}
|
||||||
|
const body = res.data as { data?: { success: boolean; message: string } };
|
||||||
|
const msg = body?.data?.message ?? `Backup started (profile ${profile}): ${desc}`;
|
||||||
|
|
||||||
|
return { success: true, message: msg };
|
||||||
|
} catch (err) {
|
||||||
|
return { success: false, message: `MokoBackup failed: ${err}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async listBackups(limit = 20): Promise<AkeebaBackupRecord[]> {
|
||||||
|
try {
|
||||||
|
const res = await this.request(`${this.baseUrl}/backups?page[limit]=${limit}`, 'GET');
|
||||||
|
if (res.status >= 400) return [];
|
||||||
|
const body = res.data as { data?: Array<{ attributes: AkeebaBackupRecord }> };
|
||||||
|
return (body.data ?? []).map(d => d.attributes);
|
||||||
|
} catch { return []; }
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteBackup(id: string): Promise<BackupResult> {
|
||||||
|
try {
|
||||||
|
const res = await this.request(`${this.baseUrl}/backup/${id}`, 'DELETE');
|
||||||
|
if (res.status >= 400) return { success: false, message: `Delete failed: ${JSON.stringify(res.data)}` };
|
||||||
|
return { success: true, message: `Deleted MokoBackup record ${id}` };
|
||||||
|
} catch (err) {
|
||||||
|
return { success: false, message: `Delete failed: ${err}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async downloadBackup(id: string): Promise<BackupResult> {
|
||||||
|
try {
|
||||||
|
mkdirSync(this.target.localBackupDir, { recursive: true });
|
||||||
|
const res = await this.request(`${this.baseUrl}/backup/${id}/download`, 'GET');
|
||||||
|
|
||||||
|
if (res.status >= 400) {
|
||||||
|
return { success: false, message: `Download failed: ${JSON.stringify(res.data)}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
const filename = `${this.target.name}-mokobackup-${id}-${new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)}.zip`;
|
||||||
|
const localFile = join(this.target.localBackupDir, filename);
|
||||||
|
const fs = await import('node:fs/promises');
|
||||||
|
|
||||||
|
if (typeof res.data === 'string') {
|
||||||
|
const buffer = Buffer.from(res.data, 'base64');
|
||||||
|
await fs.writeFile(localFile, buffer);
|
||||||
|
return { success: true, message: `Downloaded backup ${id}`, filePath: localFile, sizeBytes: buffer.length };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle JSON-wrapped response
|
||||||
|
const body = res.data as { data?: string };
|
||||||
|
if (body?.data && typeof body.data === 'string') {
|
||||||
|
const buffer = Buffer.from(body.data, 'base64');
|
||||||
|
await fs.writeFile(localFile, buffer);
|
||||||
|
return { success: true, message: `Downloaded backup ${id}`, filePath: localFile, sizeBytes: buffer.length };
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.writeFile(localFile, JSON.stringify(res.data));
|
||||||
|
return { success: true, message: `Downloaded backup ${id}`, filePath: localFile };
|
||||||
|
} catch (err) {
|
||||||
|
return { success: false, message: `Download failed: ${err}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getProfiles(): Promise<unknown> {
|
||||||
|
const res = await this.request(`${this.baseUrl}/profiles`, 'GET');
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
export interface BackupTarget {
|
||||||
|
name: string;
|
||||||
|
type: 'mysql' | 'postgres' | 'files' | 'akeeba' | 'mokobackup' | 'dolibarr';
|
||||||
|
sshHost?: string;
|
||||||
|
sshPort?: number;
|
||||||
|
sshUser?: string;
|
||||||
|
sshKeyPath?: string;
|
||||||
|
database?: string;
|
||||||
|
dbUser?: string;
|
||||||
|
dbPassword?: string;
|
||||||
|
remotePaths?: string[];
|
||||||
|
localBackupDir: string;
|
||||||
|
/** Akeeba/MokoBackup: site base URL (e.g. https://clarksvillefurs.com) */
|
||||||
|
siteUrl?: string;
|
||||||
|
/** Akeeba/MokoBackup: Joomla API token (Bearer auth) */
|
||||||
|
secretWord?: string;
|
||||||
|
/** Akeeba/MokoBackup: backup profile ID (default 1) */
|
||||||
|
profileId?: number;
|
||||||
|
/** Dolibarr-specific: path to conf/conf.php on the remote server */
|
||||||
|
confPath?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BackupConfig {
|
||||||
|
targets: Record<string, BackupTarget>;
|
||||||
|
defaultTarget: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BackupResult {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
filePath?: string;
|
||||||
|
sizeBytes?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AkeebaBackupRecord {
|
||||||
|
id: string;
|
||||||
|
description: string;
|
||||||
|
status: string;
|
||||||
|
origin: string;
|
||||||
|
type: string;
|
||||||
|
profile_id: number;
|
||||||
|
archivename: string;
|
||||||
|
absolute_path: string;
|
||||||
|
multipart: number;
|
||||||
|
tag: string;
|
||||||
|
backupstart: string;
|
||||||
|
backupend: string;
|
||||||
|
filesexist: number;
|
||||||
|
remote_filename: string;
|
||||||
|
total_size: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AkeebaApiResponse {
|
||||||
|
body: {
|
||||||
|
status: number;
|
||||||
|
data: unknown;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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,44 @@
|
|||||||
|
# mcp_mokocrm
|
||||||
|
|
||||||
|
MCP server for Dolibarr ERP/CRM REST API operations — third parties, invoices, proposals, projects, tasks, contacts, and business management.
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Package** | `@mokoconsulting/mcp-mokocrm-api` |
|
||||||
|
| **Entry** | `dist/index.js` |
|
||||||
|
| **Config** | `~/.mcp_mokocrm.json` (override: `DOLIBARR_API_MCP_CONFIG` env var) |
|
||||||
|
| **Language** | TypeScript |
|
||||||
|
| **Branch** | develop on `dev`, merge to `main` (protected) |
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install # Install dependencies
|
||||||
|
npm run build # Compile TypeScript → dist/
|
||||||
|
npm run dev # Watch mode
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── index.ts # MCP server entry, tool registration
|
||||||
|
├── config.ts # Loads ~/.mcp_mokocrm.json, resolves connections
|
||||||
|
├── client.ts # Dolibarr REST API client wrapper
|
||||||
|
├── tools/ # Individual tool implementations
|
||||||
|
└── types.ts # DolibarrConfig, DolibarrConnection types
|
||||||
|
```
|
||||||
|
|
||||||
|
- Config defines **connections** — each is a Dolibarr instance with URL + API key
|
||||||
|
- Default connections: production (crm.mokoconsulting.tech), dev (crm.dev.mokoconsulting.tech)
|
||||||
|
- No demo environment for CRM
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- **Never commit** `.claude/`, `.mcp.json`, `TODO.md`, `*.min.css`/`*.min.js`
|
||||||
|
- **Attribution**: `Authored-by: Moko Consulting`
|
||||||
|
- **Workflow directory**: `.mokogitea/` (not `.gitea/` or `.github/`)
|
||||||
|
- **Wiki**: documentation lives in the Gitea wiki, not `docs/` files
|
||||||
|
- **Standards**: [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
<!-- 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
|
||||||
|
|
||||||
|
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License (./LICENSE).
|
||||||
|
|
||||||
|
# FILE INFORMATION
|
||||||
|
DEFGROUP: dolibarr-api-mcp.Documentation
|
||||||
|
INGROUP: dolibarr-api-mcp
|
||||||
|
REPO: https://git.mokoconsulting.tech/MokoConsulting/dolibarr-api-mcp
|
||||||
|
VERSION: 0.0.0
|
||||||
|
PATH: ./CHANGELOG.md
|
||||||
|
BRIEF: Version history and change log
|
||||||
|
-->
|
||||||
|
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.0] - 2026-05-07
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Initial MCP server with Dolibarr REST API tools
|
||||||
|
- Third party management (list, get, create, update, delete)
|
||||||
|
- Invoice management (list, get, create, add lines, validate, set paid)
|
||||||
|
- Commercial proposal management (list, get, create, add lines, validate, close)
|
||||||
|
- Customer order management (list, get, create, validate)
|
||||||
|
- Product and service catalog (list, get, create, update, stock levels)
|
||||||
|
- Contact/address management (list, get)
|
||||||
|
- Project management (list, get, create)
|
||||||
|
- Task management (list, get)
|
||||||
|
- User management (list, get)
|
||||||
|
- Category management (list by type)
|
||||||
|
- Bank account listing
|
||||||
|
- Supplier invoice listing
|
||||||
|
- Supplier order listing
|
||||||
|
- Warehouse listing
|
||||||
|
- Company setup and system status endpoints
|
||||||
|
- Raw API passthrough for any Dolibarr endpoint
|
||||||
|
- Multi-connection support with named connections
|
||||||
|
- Interactive setup wizard (`npm run setup`)
|
||||||
|
- SQL filter builder (`buildSqlFilter`, `searchFilter`) for safe query construction
|
||||||
|
- Full documentation: README, INSTALLATION, ARCHITECTURE, API reference
|
||||||
|
- MokoStandards-compliant project structure
|
||||||
|
- 12 Gitea Actions CI/CD workflows
|
||||||
|
|
||||||
|
## Revision History
|
||||||
|
|
||||||
|
| Date | Version | Author | Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 2026-05-07 | 0.0.1 | jmiller | Initial release |
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
<!-- 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
|
||||||
|
|
||||||
|
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License (./LICENSE).
|
||||||
|
|
||||||
|
# FILE INFORMATION
|
||||||
|
DEFGROUP: dolibarr-api-mcp.Documentation
|
||||||
|
INGROUP: dolibarr-api-mcp
|
||||||
|
REPO: https://git.mokoconsulting.tech/MokoConsulting/dolibarr-api-mcp
|
||||||
|
VERSION: 01.00.00
|
||||||
|
PATH: ./CONTRIBUTING.md
|
||||||
|
BRIEF: Contribution guidelines for the project
|
||||||
|
-->
|
||||||
|
|
||||||
|
# Contributing to dolibarr-api-mcp
|
||||||
|
|
||||||
|
We appreciate your interest in contributing to this project! This document provides guidelines for contributing.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Code of Conduct](#code-of-conduct)
|
||||||
|
- [Getting Started](#getting-started)
|
||||||
|
- [How to Contribute](#how-to-contribute)
|
||||||
|
- [Development Workflow](#development-workflow)
|
||||||
|
- [Commit Messages](#commit-messages)
|
||||||
|
- [Pull Request Process](#pull-request-process)
|
||||||
|
|
||||||
|
## Code of Conduct
|
||||||
|
|
||||||
|
This project adheres to the Contributor Covenant Code of Conduct. By participating, you are expected to uphold this code. Please report unacceptable behavior to hello@mokoconsulting.tech.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Clone your fork locally
|
||||||
|
3. Install dependencies: `npm install`
|
||||||
|
4. Build: `npm run build`
|
||||||
|
5. Create a new branch for your work
|
||||||
|
|
||||||
|
## How to Contribute
|
||||||
|
|
||||||
|
### Reporting Bugs
|
||||||
|
|
||||||
|
- Use the Gitea issue tracker
|
||||||
|
- Describe the bug clearly with steps to reproduce
|
||||||
|
- Include the Dolibarr version you're connecting to
|
||||||
|
- Include relevant logs or error messages
|
||||||
|
|
||||||
|
### Adding New Tools
|
||||||
|
|
||||||
|
If you want to add support for a Dolibarr API endpoint not yet covered:
|
||||||
|
|
||||||
|
1. Check the [Dolibarr API Explorer](https://your-dolibarr.com/api/index.php/explorer) for endpoint details
|
||||||
|
2. Add the tool registration in `src/index.ts` following the existing patterns
|
||||||
|
3. Update `docs/API.md` with the new tool's parameter table
|
||||||
|
4. Update `README.md` tool listing
|
||||||
|
5. Update `CHANGELOG.md`
|
||||||
|
|
||||||
|
### Contributing Code
|
||||||
|
|
||||||
|
- Pick an issue or create one
|
||||||
|
- Fork the repository and create a branch
|
||||||
|
- Make your changes following the project conventions
|
||||||
|
- Submit a pull request
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
1. Ensure your fork is up to date with the main repository
|
||||||
|
2. Create a feature branch from `main`
|
||||||
|
3. Make your changes
|
||||||
|
4. Test against a Dolibarr instance (use `npm run setup` to configure a dev connection)
|
||||||
|
5. Build with `npm run build` to catch TypeScript errors
|
||||||
|
6. Commit your changes with clear messages
|
||||||
|
7. Push to your fork
|
||||||
|
8. Create a pull request
|
||||||
|
|
||||||
|
## Commit Messages
|
||||||
|
|
||||||
|
Follow the conventional commit format:
|
||||||
|
|
||||||
|
```
|
||||||
|
<type>(<scope>): <subject>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
```
|
||||||
|
|
||||||
|
Types: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`, `ci`, `build`, `perf`, `revert`
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```
|
||||||
|
feat(tools): add shipment management tools
|
||||||
|
|
||||||
|
Add dolibarr_shipments_list, dolibarr_shipment_get, and
|
||||||
|
dolibarr_shipment_validate tools for the /shipments API endpoint.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pull Request Process
|
||||||
|
|
||||||
|
1. Update documentation for any new tools
|
||||||
|
2. Follow the project's coding style and conventions
|
||||||
|
3. Ensure `npm run build` succeeds without errors
|
||||||
|
4. Update the CHANGELOG.md with your changes
|
||||||
|
5. Request review from maintainers
|
||||||
|
6. Address any feedback promptly
|
||||||
|
7. Once approved, your PR will be merged
|
||||||
|
|
||||||
|
## Style Guidelines
|
||||||
|
|
||||||
|
- Use tabs for indentation
|
||||||
|
- All source files must include the Moko Consulting copyright header
|
||||||
|
- Use `snake_case` for local variables (matching Dolibarr API field names)
|
||||||
|
- Use Zod for all tool parameter validation
|
||||||
|
- Follow the `formatResponse()` pattern for consistent error handling
|
||||||
|
|
||||||
|
## Infrastructure Standards
|
||||||
|
|
||||||
|
All repositories in the MokoConsulting org follow these conventions:
|
||||||
|
|
||||||
|
### Release Tags
|
||||||
|
|
||||||
|
Every repo maintains 5 standard release channel tags:
|
||||||
|
|
||||||
|
- `development` - Active development builds
|
||||||
|
- `alpha` - Early internal testing
|
||||||
|
- `beta` - Broader testing / client UAT
|
||||||
|
- `release-candidate` - Final QA before production
|
||||||
|
- `stable` - Production release
|
||||||
|
|
||||||
|
### Branch Protection
|
||||||
|
|
||||||
|
- `main` is protected; only `jmiller` can push directly
|
||||||
|
- All other contributors must use pull requests
|
||||||
|
- PRs are automatically reviewed by Claude Code
|
||||||
|
|
||||||
|
### CI/CD
|
||||||
|
|
||||||
|
- Gitea Actions runs all CI workflows
|
||||||
|
- Workflows live in `.gitea/workflows/`
|
||||||
|
|
||||||
|
### Secrets
|
||||||
|
|
||||||
|
All repos have `GA_TOKEN` and `GH_TOKEN` as Actions secrets for API access.
|
||||||
|
|
||||||
|
## Questions?
|
||||||
|
|
||||||
|
If you have questions about contributing, feel free to open an issue or contact the maintainers at hello@mokoconsulting.tech.
|
||||||
|
|
||||||
|
## Revision History
|
||||||
|
|
||||||
|
| Date | Version | Author | Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 2026-05-07 | 0.0.1 | jmiller | Initial contributing guidelines |
|
||||||
@@ -0,0 +1,370 @@
|
|||||||
|
# dolibarr-api-mcp
|
||||||
|
|
||||||
|
MCP server for Dolibarr ERP/CRM REST API operations
|
||||||
|
|
||||||
|
    
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Language** | TypeScript |
|
||||||
|
| **License** | GPL-3.0-or-later |
|
||||||
|
| **Platform** | [Gitea](https://git.mokoconsulting.tech/MokoConsulting/dolibarr-api-mcp) |
|
||||||
|
| **Node.js** | >= 20.0.0 |
|
||||||
|
| **MCP SDK** | @modelcontextprotocol/sdk ^1.12.1 |
|
||||||
|
|
||||||
|
A [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) server that bridges AI assistants (Claude Code, Cursor, etc.) with Dolibarr's built-in REST API. Manage invoices, proposals, orders, products, third parties, projects, and more -- directly from your AI assistant.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone https://git.mokoconsulting.tech/MokoConsulting/dolibarr-api-mcp.git
|
||||||
|
cd dolibarr-api-mcp
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
npm run setup
|
||||||
|
```
|
||||||
|
|
||||||
|
The interactive setup wizard will prompt for your Dolibarr instance URL, API key, and TLS settings.
|
||||||
|
|
||||||
|
Register with Claude Code (`~/.claude.json`):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"dolibarr-api": {
|
||||||
|
"type": "stdio",
|
||||||
|
"command": "node",
|
||||||
|
"args": ["/path/to/dolibarr-api-mcp/dist/index.js"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify with: `dolibarr_status` -- returns the Dolibarr version and server info.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tools (85)
|
||||||
|
|
||||||
|
Every tool accepts an optional `connection` parameter to target a specific named Dolibarr instance (defaults to the configured default).
|
||||||
|
|
||||||
|
### Third Parties (5)
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|---|---|
|
||||||
|
| `dolibarr_thirdparties_list` | List/search third parties with pagination and SQL filters |
|
||||||
|
| `dolibarr_thirdparty_get` | Get a third party by ID |
|
||||||
|
| `dolibarr_thirdparty_create` | Create a new third party (customer, supplier, or prospect) |
|
||||||
|
| `dolibarr_thirdparty_update` | Update an existing third party |
|
||||||
|
| `dolibarr_thirdparty_delete` | Delete a third party |
|
||||||
|
|
||||||
|
### Contacts (5)
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|---|---|
|
||||||
|
| `dolibarr_contacts_list` | List/search contacts with pagination |
|
||||||
|
| `dolibarr_contact_get` | Get a contact by ID |
|
||||||
|
| `dolibarr_contact_create` | Create a new contact linked to a third party |
|
||||||
|
| `dolibarr_contact_update` | Update an existing contact |
|
||||||
|
| `dolibarr_contact_delete` | Delete a contact |
|
||||||
|
|
||||||
|
### Invoices (7)
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|---|---|
|
||||||
|
| `dolibarr_invoices_list` | List/search invoices with status and date filters |
|
||||||
|
| `dolibarr_invoice_get` | Get an invoice by ID |
|
||||||
|
| `dolibarr_invoice_create` | Create a new invoice for a third party |
|
||||||
|
| `dolibarr_invoice_add_line` | Add a line item to an invoice |
|
||||||
|
| `dolibarr_invoice_validate` | Validate a draft invoice |
|
||||||
|
| `dolibarr_invoice_set_paid` | Mark an invoice as paid |
|
||||||
|
| `dolibarr_invoice_add_payment` | Record a payment against an invoice |
|
||||||
|
| `dolibarr_invoice_payments` | List payments for an invoice |
|
||||||
|
|
||||||
|
### Proposals / Quotes (7)
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|---|---|
|
||||||
|
| `dolibarr_proposals_list` | List/search proposals with pagination |
|
||||||
|
| `dolibarr_proposal_get` | Get a proposal by ID |
|
||||||
|
| `dolibarr_proposal_create` | Create a new proposal for a third party |
|
||||||
|
| `dolibarr_proposal_add_line` | Add a line item to a proposal |
|
||||||
|
| `dolibarr_proposal_validate` | Validate a draft proposal |
|
||||||
|
| `dolibarr_proposal_close` | Close a proposal (signed or refused) |
|
||||||
|
|
||||||
|
### Orders (5)
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|---|---|
|
||||||
|
| `dolibarr_orders_list` | List/search orders with pagination |
|
||||||
|
| `dolibarr_order_get` | Get an order by ID |
|
||||||
|
| `dolibarr_order_create` | Create a new order for a third party |
|
||||||
|
| `dolibarr_order_add_line` | Add a line item to an order |
|
||||||
|
| `dolibarr_order_validate` | Validate a draft order |
|
||||||
|
|
||||||
|
### Products & Services (5)
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|---|---|
|
||||||
|
| `dolibarr_products_list` | List/search products and services |
|
||||||
|
| `dolibarr_product_get` | Get a product by ID |
|
||||||
|
| `dolibarr_product_create` | Create a new product or service |
|
||||||
|
| `dolibarr_product_update` | Update an existing product |
|
||||||
|
| `dolibarr_product_stock` | Get stock levels for a product |
|
||||||
|
|
||||||
|
### Projects (4)
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|---|---|
|
||||||
|
| `dolibarr_projects_list` | List/search projects |
|
||||||
|
| `dolibarr_project_get` | Get a project by ID |
|
||||||
|
| `dolibarr_project_create` | Create a new project |
|
||||||
|
| `dolibarr_project_update` | Update an existing project |
|
||||||
|
|
||||||
|
### Tasks (6)
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|---|---|
|
||||||
|
| `dolibarr_tasks_list` | List tasks (optionally filtered by project) |
|
||||||
|
| `dolibarr_task_get` | Get a task by ID |
|
||||||
|
| `dolibarr_task_create` | Create a new task within a project |
|
||||||
|
| `dolibarr_task_update` | Update an existing task |
|
||||||
|
| `dolibarr_task_timespent_list` | List time entries for a task |
|
||||||
|
| `dolibarr_task_timespent_add` | Add a time entry to a task |
|
||||||
|
|
||||||
|
### Contracts (4)
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|---|---|
|
||||||
|
| `dolibarr_contracts_list` | List/search contracts |
|
||||||
|
| `dolibarr_contract_get` | Get a contract by ID |
|
||||||
|
| `dolibarr_contract_create` | Create a new contract |
|
||||||
|
| `dolibarr_contract_validate` | Validate a draft contract |
|
||||||
|
|
||||||
|
### Shipments (5)
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|---|---|
|
||||||
|
| `dolibarr_shipments_list` | List/search shipments |
|
||||||
|
| `dolibarr_shipment_get` | Get a shipment by ID |
|
||||||
|
| `dolibarr_shipment_create` | Create a new shipment from an order |
|
||||||
|
| `dolibarr_shipment_validate` | Validate a draft shipment |
|
||||||
|
| `dolibarr_shipment_close` | Close a shipment |
|
||||||
|
|
||||||
|
### Agenda Events (4)
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|---|---|
|
||||||
|
| `dolibarr_agendaevents_list` | List/search agenda events |
|
||||||
|
| `dolibarr_agendaevent_get` | Get an agenda event by ID |
|
||||||
|
| `dolibarr_agendaevent_create` | Create a new agenda event |
|
||||||
|
| `dolibarr_agendaevent_update` | Update an existing agenda event |
|
||||||
|
|
||||||
|
### Tickets (3)
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|---|---|
|
||||||
|
| `dolibarr_tickets_list` | List/search tickets |
|
||||||
|
| `dolibarr_ticket_get` | Get a ticket by ID |
|
||||||
|
| `dolibarr_ticket_create` | Create a new support ticket |
|
||||||
|
|
||||||
|
### Members (2)
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|---|---|
|
||||||
|
| `dolibarr_members_list` | List/search members (foundation/association module) |
|
||||||
|
| `dolibarr_member_get` | Get a member by ID |
|
||||||
|
|
||||||
|
### Users (3)
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|---|---|
|
||||||
|
| `dolibarr_users_list` | List Dolibarr users |
|
||||||
|
| `dolibarr_user_get` | Get a user by ID |
|
||||||
|
| `dolibarr_user_create` | Create a new Dolibarr user |
|
||||||
|
|
||||||
|
### Expense Reports (3)
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|---|---|
|
||||||
|
| `dolibarr_expensereports_list` | List/search expense reports |
|
||||||
|
| `dolibarr_expensereport_get` | Get an expense report by ID |
|
||||||
|
| `dolibarr_expensereport_create` | Create a new expense report |
|
||||||
|
|
||||||
|
### Interventions (2)
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|---|---|
|
||||||
|
| `dolibarr_interventions_list` | List/search interventions |
|
||||||
|
| `dolibarr_intervention_get` | Get an intervention by ID |
|
||||||
|
|
||||||
|
### Documents (3)
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|---|---|
|
||||||
|
| `dolibarr_documents_list` | List documents attached to a module element |
|
||||||
|
| `dolibarr_document_download` | Download a document file |
|
||||||
|
| `dolibarr_document_builddoc` | Generate a PDF document for an element |
|
||||||
|
|
||||||
|
### Stock & Warehouses (3)
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|---|---|
|
||||||
|
| `dolibarr_warehouses_list` | List warehouses |
|
||||||
|
| `dolibarr_stockmovements_list` | List stock movements |
|
||||||
|
| `dolibarr_stockmovement_create` | Create a stock movement |
|
||||||
|
|
||||||
|
### Bank Accounts (2)
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|---|---|
|
||||||
|
| `dolibarr_bankaccounts_list` | List bank accounts |
|
||||||
|
| `dolibarr_bankaccount_lines` | List transaction lines for a bank account |
|
||||||
|
|
||||||
|
### Categories (3)
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|---|---|
|
||||||
|
| `dolibarr_categories_list` | List categories by type |
|
||||||
|
| `dolibarr_category_get` | Get a category by ID |
|
||||||
|
| `dolibarr_category_create` | Create a new category |
|
||||||
|
|
||||||
|
### Supplier Invoices & Orders (2)
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|---|---|
|
||||||
|
| `dolibarr_supplier_invoices_list` | List supplier (vendor) invoices |
|
||||||
|
| `dolibarr_supplier_orders_list` | List supplier (vendor) orders |
|
||||||
|
|
||||||
|
### Setup & System (5)
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|---|---|
|
||||||
|
| `dolibarr_status` | Check Dolibarr instance status and version |
|
||||||
|
| `dolibarr_setup_company` | Get company/organization setup info |
|
||||||
|
| `dolibarr_setup_modules` | List enabled Dolibarr modules |
|
||||||
|
| `dolibarr_setup_dictionary` | Query Dolibarr dictionary tables (countries, currencies, etc.) |
|
||||||
|
| `dolibarr_list_connections` | List all configured Dolibarr connections |
|
||||||
|
|
||||||
|
### Generic (1)
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|---|---|
|
||||||
|
| `dolibarr_api_request` | Make a raw API request to any Dolibarr endpoint |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The config file is stored at `~/.dolibarr-api-mcp.json` (or set `DOLIBARR_API_MCP_CONFIG` for a custom path):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"defaultConnection": "production",
|
||||||
|
"connections": {
|
||||||
|
"production": {
|
||||||
|
"baseUrl": "https://erp.example.com",
|
||||||
|
"apiKey": "your-api-key"
|
||||||
|
},
|
||||||
|
"staging": {
|
||||||
|
"baseUrl": "https://erp-staging.example.com",
|
||||||
|
"apiKey": "your-staging-key",
|
||||||
|
"insecure": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Required | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `defaultConnection` | Yes | Name of the default connection |
|
||||||
|
| `connections` | Yes | Map of named connections |
|
||||||
|
| `baseUrl` | Yes | Dolibarr instance URL (no trailing slash) |
|
||||||
|
| `apiKey` | Yes | Dolibarr API key (`DOLAPIKEY` header auth) |
|
||||||
|
| `insecure` | No | Set `true` to skip TLS verification (self-signed certs) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
AI Assistant <--> MCP (stdio) <--> DolibarrClient <--> Dolibarr REST API
|
||||||
|
/api/index.php
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Transport**: stdio (standard input/output)
|
||||||
|
- **Auth**: `DOLAPIKEY` HTTP header (Dolibarr's native per-user API key)
|
||||||
|
- **HTTP**: Uses `node:https`/`node:http` (not `fetch`) for reliable self-signed TLS support on Node.js 24+
|
||||||
|
- **Validation**: Zod schemas for all tool inputs
|
||||||
|
- **Filtering**: `buildSqlFilter()` helper for Dolibarr's `sqlfilters` parameter with injection-safe escaping
|
||||||
|
|
||||||
|
### Source Layout
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `src/index.ts` | Server entry point -- registers all MCP tools with Zod schemas |
|
||||||
|
| `src/client.ts` | `DolibarrClient` HTTP class (GET/POST/PUT/DELETE) |
|
||||||
|
| `src/config.ts` | Configuration loader for multi-instance connections |
|
||||||
|
| `src/types.ts` | TypeScript interfaces (`DolibarrConnection`, `DolibarrConfig`, `ApiResponse`) |
|
||||||
|
| `scripts/setup.mjs` | Interactive setup wizard for creating the config file |
|
||||||
|
| `config.example.json` | Example configuration with multiple connections |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
**List all customers:**
|
||||||
|
```
|
||||||
|
dolibarr_thirdparties_list with search="acme", limit=10
|
||||||
|
```
|
||||||
|
|
||||||
|
**Create an invoice and add a line:**
|
||||||
|
```
|
||||||
|
dolibarr_invoice_create with socid=42
|
||||||
|
dolibarr_invoice_add_line with id=<invoice_id>, desc="Consulting services", subprice=150.00, qty=8
|
||||||
|
dolibarr_invoice_validate with id=<invoice_id>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Check a specific Dolibarr instance:**
|
||||||
|
```
|
||||||
|
dolibarr_status with connection="staging"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Raw API request for unsupported endpoints:**
|
||||||
|
```
|
||||||
|
dolibarr_api_request with method="GET", endpoint="/categories", params={"type": "product"}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Guides
|
||||||
|
|
||||||
|
| Page | Description |
|
||||||
|
|---|---|
|
||||||
|
| [INSTALLATION](https://git.mokoconsulting.tech/MokoConsulting/dolibarr-api-mcp/wiki/INSTALLATION) | Prerequisites, install steps, Claude Code registration, troubleshooting |
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
| Page | Description |
|
||||||
|
|---|---|
|
||||||
|
| [ARCHITECTURE](https://git.mokoconsulting.tech/MokoConsulting/dolibarr-api-mcp/wiki/ARCHITECTURE) | Component overview, design decisions, data flow, API module coverage |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Full documentation is available on the [Wiki](https://git.mokoconsulting.tech/MokoConsulting/dolibarr-api-mcp/wiki).
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
See [CONTRIBUTING.md](CONTRIBUTING.md) for development guidelines and contribution instructions.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the GNU General Public License v3.0 or later -- see the [LICENSE](LICENSE) file.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*[Moko Consulting](https://mokoconsulting.tech) -- [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
<!--
|
||||||
|
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: dolibarr-api-mcp.Documentation
|
||||||
|
INGROUP: dolibarr-api-mcp
|
||||||
|
REPO: https://git.mokoconsulting.tech/MokoConsulting/dolibarr-api-mcp
|
||||||
|
PATH: /SECURITY.md
|
||||||
|
VERSION: 01.00.00
|
||||||
|
BRIEF: Security vulnerability reporting and handling policy
|
||||||
|
-->
|
||||||
|
|
||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Purpose and Scope
|
||||||
|
|
||||||
|
This document defines the security vulnerability reporting, response, and disclosure policy for dolibarr-api-mcp and all repositories governed by MokoStandards.
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
| Version | Supported |
|
||||||
|
| ------- | ------------------ |
|
||||||
|
| 1.x.x | :white_check_mark: |
|
||||||
|
| < 1.0 | :x: |
|
||||||
|
|
||||||
|
Only the current major version receives security updates.
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
Report security vulnerabilities via Gitea issue (preferred):
|
||||||
|
https://git.mokoconsulting.tech/MokoConsulting/dolibarr-api-mcp/issues/new?template=security.yaml
|
||||||
|
|
||||||
|
Or email: hello@mokoconsulting.tech
|
||||||
|
|
||||||
|
### Where to Report
|
||||||
|
|
||||||
|
**DO NOT** create public issues for security vulnerabilities.
|
||||||
|
|
||||||
|
Report security vulnerabilities privately to:
|
||||||
|
|
||||||
|
**Email**: `hello@mokoconsulting.tech`
|
||||||
|
|
||||||
|
**Subject Line**: `[SECURITY] Brief Description`
|
||||||
|
|
||||||
|
### What to Include
|
||||||
|
|
||||||
|
1. **Description**: Clear explanation of the vulnerability
|
||||||
|
2. **Impact**: Potential security impact and severity assessment
|
||||||
|
3. **Affected Versions**: Which versions are vulnerable
|
||||||
|
4. **Reproduction Steps**: Detailed steps to reproduce the issue
|
||||||
|
5. **Proof of Concept**: Code or demonstration (if applicable)
|
||||||
|
6. **Suggested Fix**: Proposed remediation (if known)
|
||||||
|
|
||||||
|
### Response Timeline
|
||||||
|
|
||||||
|
* **Initial Response**: Within 3 business days
|
||||||
|
* **Assessment Complete**: Within 7 business days
|
||||||
|
* **Fix Timeline**: Depends on severity (see below)
|
||||||
|
|
||||||
|
## Severity Classification
|
||||||
|
|
||||||
|
### Critical
|
||||||
|
* API key exposure or leakage
|
||||||
|
* Remote code execution via API parameters
|
||||||
|
* Authentication bypass
|
||||||
|
* **Fix Timeline**: 7 days
|
||||||
|
|
||||||
|
### High
|
||||||
|
* SQL injection via sqlfilters parameter
|
||||||
|
* Unauthorized access to Dolibarr data
|
||||||
|
* **Fix Timeline**: 14 days
|
||||||
|
|
||||||
|
### Medium
|
||||||
|
* Information disclosure (limited scope)
|
||||||
|
* Configuration file exposure
|
||||||
|
* **Fix Timeline**: 30 days
|
||||||
|
|
||||||
|
### Low
|
||||||
|
* Security best practice violations
|
||||||
|
* Minor information leaks
|
||||||
|
* **Fix Timeline**: 60 days or next release
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
### API Key Storage
|
||||||
|
|
||||||
|
- API keys are stored in `~/.dolibarr-api-mcp.json` with user-only file permissions
|
||||||
|
- Never commit API keys to version control
|
||||||
|
- The `.gitignore` excludes `.mcp.json` and environment files
|
||||||
|
|
||||||
|
### SQL Filter Safety
|
||||||
|
|
||||||
|
- The `buildSqlFilter()` helper escapes single quotes to prevent SQL injection via the `sqlfilters` parameter
|
||||||
|
- All user-provided search terms are wrapped with the helper before being sent to Dolibarr
|
||||||
|
|
||||||
|
### TLS Verification
|
||||||
|
|
||||||
|
- The `insecure` connection option disables TLS certificate verification
|
||||||
|
- This should only be used for local development with self-signed certificates
|
||||||
|
- Production connections should always use valid TLS certificates
|
||||||
|
|
||||||
|
## Attribution and Recognition
|
||||||
|
|
||||||
|
We acknowledge and appreciate responsible disclosure. With your permission, we will credit you in security advisories and release notes.
|
||||||
|
|
||||||
|
## Revision History
|
||||||
|
|
||||||
|
| Date | Version | Author | Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 2026-05-07 | 0.0.1 | jmiller | Initial security policy |
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"defaultConnection": "production",
|
||||||
|
"connections": {
|
||||||
|
"local-dev": {
|
||||||
|
"baseUrl": "https://localhost:8080",
|
||||||
|
"apiKey": "your-dolibarr-api-key-here",
|
||||||
|
"insecure": true
|
||||||
|
},
|
||||||
|
"production": {
|
||||||
|
"baseUrl": "https://erp.example.com",
|
||||||
|
"apiKey": "your-production-api-key"
|
||||||
|
},
|
||||||
|
"staging": {
|
||||||
|
"baseUrl": "https://erp-staging.example.com",
|
||||||
|
"apiKey": "your-staging-api-key"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"name": "@mokoconsulting/mcp-mokocrm-api",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "MCP server for Dolibarr ERP/CRM REST API operations",
|
||||||
|
"type": "module",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"bin": {
|
||||||
|
"dolibarr-api-mcp": "dist/index.js"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"dev": "tsc --watch",
|
||||||
|
"start": "node dist/index.js",
|
||||||
|
"lint": "eslint src/",
|
||||||
|
"setup": "node scripts/setup.mjs",
|
||||||
|
"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>",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://git.mokoconsulting.tech/MokoConsulting/dolibarr-api-mcp.git"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
#!/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: dolibarr-api-mcp.Scripts
|
||||||
|
* INGROUP: dolibarr-api-mcp
|
||||||
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/dolibarr-api-mcp
|
||||||
|
* PATH: /scripts/setup.mjs
|
||||||
|
* VERSION: 01.00.00
|
||||||
|
* BRIEF: Interactive setup — prompts for Dolibarr API connection details and writes config
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createInterface } from 'node:readline/promises';
|
||||||
|
import { readFile, writeFile } from 'node:fs/promises';
|
||||||
|
import { resolve } from 'node:path';
|
||||||
|
import { homedir } from 'node:os';
|
||||||
|
|
||||||
|
const CONFIG_PATH = resolve(homedir(), '.dolibarr-api-mcp.json');
|
||||||
|
|
||||||
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
||||||
|
|
||||||
|
async function prompt(question, defaultValue) {
|
||||||
|
const suffix = defaultValue ? ` [${defaultValue}]` : '';
|
||||||
|
const answer = await rl.question(`${question}${suffix}: `);
|
||||||
|
return answer.trim() || defaultValue || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function promptRequired(question) {
|
||||||
|
let answer = '';
|
||||||
|
while (!answer) {
|
||||||
|
answer = (await rl.question(`${question}: `)).trim();
|
||||||
|
if (!answer) {
|
||||||
|
console.log(' This field is required.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return answer;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log('');
|
||||||
|
console.log('=== dolibarr-api-mcp Setup ===');
|
||||||
|
console.log('');
|
||||||
|
console.log('This will create your configuration file at:');
|
||||||
|
console.log(` ${CONFIG_PATH}`);
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// Check for existing config
|
||||||
|
let existing = null;
|
||||||
|
try {
|
||||||
|
const raw = await readFile(CONFIG_PATH, 'utf-8');
|
||||||
|
existing = JSON.parse(raw);
|
||||||
|
console.log('Existing config found. You can add a new connection or overwrite.');
|
||||||
|
console.log(` Current connections: ${Object.keys(existing.connections).join(', ')}`);
|
||||||
|
console.log('');
|
||||||
|
} catch {
|
||||||
|
// No existing config
|
||||||
|
}
|
||||||
|
|
||||||
|
const connectionName = await prompt('Connection name', 'production');
|
||||||
|
const baseUrl = await promptRequired('Dolibarr URL (e.g. https://erp.example.com)');
|
||||||
|
const apiKey = await promptRequired('Dolibarr API key (from user settings or Setup > Security)');
|
||||||
|
|
||||||
|
const cleanUrl = baseUrl.replace(/\/+$/, '');
|
||||||
|
|
||||||
|
const insecureAnswer = await prompt('Skip TLS verification for self-signed certs? (y/N)', 'N');
|
||||||
|
const insecure = insecureAnswer.toLowerCase() === 'y';
|
||||||
|
|
||||||
|
const connection = { baseUrl: cleanUrl, apiKey };
|
||||||
|
if (insecure) {
|
||||||
|
connection.insecure = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let config;
|
||||||
|
if (existing) {
|
||||||
|
config = existing;
|
||||||
|
config.connections[connectionName] = connection;
|
||||||
|
const setDefault = await prompt(`Set "${connectionName}" as default connection? (y/N)`, 'N');
|
||||||
|
if (setDefault.toLowerCase() === 'y') {
|
||||||
|
config.defaultConnection = connectionName;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
config = {
|
||||||
|
defaultConnection: connectionName,
|
||||||
|
connections: {
|
||||||
|
[connectionName]: connection,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await writeFile(CONFIG_PATH, JSON.stringify(config, null, '\t') + '\n', 'utf-8');
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
console.log(`Config written to ${CONFIG_PATH}`);
|
||||||
|
console.log(` Connection "${connectionName}" configured for ${cleanUrl}`);
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
const addAnother = await prompt('Add another connection? (y/N)', 'N');
|
||||||
|
if (addAnother.toLowerCase() === 'y') {
|
||||||
|
rl.close();
|
||||||
|
// Re-run to add another
|
||||||
|
const { execFileSync } = await import('node:child_process');
|
||||||
|
execFileSync('node', [new URL(import.meta.url).pathname], { stdio: 'inherit' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Setup complete. You can now use the MCP server.');
|
||||||
|
console.log('');
|
||||||
|
rl.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error(`Setup failed: ${err.message}`);
|
||||||
|
rl.close();
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
/* 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: dolibarr-api-mcp.Client
|
||||||
|
* INGROUP: dolibarr-api-mcp
|
||||||
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/dolibarr-api-mcp
|
||||||
|
* PATH: /src/client.ts
|
||||||
|
* VERSION: 01.00.00
|
||||||
|
* BRIEF: HTTP client for Dolibarr REST API
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as https from 'node:https';
|
||||||
|
import * as http from 'node:http';
|
||||||
|
import type { DolibarrConnection, ApiResponse } from './types.js';
|
||||||
|
|
||||||
|
const API_PREFIX = '/api/index.php';
|
||||||
|
const TIMEOUT_MS = 30_000;
|
||||||
|
|
||||||
|
export class DolibarrClient {
|
||||||
|
private readonly base_url: string;
|
||||||
|
private readonly headers: Record<string, string>;
|
||||||
|
private readonly insecure: boolean;
|
||||||
|
|
||||||
|
constructor(conn: DolibarrConnection) {
|
||||||
|
this.base_url = conn.baseUrl.replace(/\/+$/, '') + API_PREFIX;
|
||||||
|
this.headers = {
|
||||||
|
'DOLAPIKEY': conn.apiKey,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
};
|
||||||
|
this.insecure = conn.insecure ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(endpoint: string, params?: Record<string, string>): Promise<ApiResponse> {
|
||||||
|
const url = this.buildUrl(endpoint, params);
|
||||||
|
return this.request(url, 'GET');
|
||||||
|
}
|
||||||
|
|
||||||
|
async post(endpoint: string, body?: unknown): Promise<ApiResponse> {
|
||||||
|
const url = this.buildUrl(endpoint);
|
||||||
|
return this.request(url, 'POST', body);
|
||||||
|
}
|
||||||
|
|
||||||
|
async put(endpoint: string, body: unknown): Promise<ApiResponse> {
|
||||||
|
const url = this.buildUrl(endpoint);
|
||||||
|
return this.request(url, 'PUT', body);
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(endpoint: string): Promise<ApiResponse> {
|
||||||
|
const url = this.buildUrl(endpoint);
|
||||||
|
return this.request(url, 'DELETE');
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildUrl(endpoint: string, params?: Record<string, string>): string {
|
||||||
|
const path = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
|
||||||
|
const url = new URL(`${this.base_url}${path}`);
|
||||||
|
if (params) {
|
||||||
|
for (const [key, value] of Object.entries(params)) {
|
||||||
|
url.searchParams.set(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private request(url: string, method: string, body?: unknown): Promise<ApiResponse> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const parsed = new URL(url);
|
||||||
|
const is_https = parsed.protocol === 'https:';
|
||||||
|
const transport = is_https ? https : http;
|
||||||
|
|
||||||
|
const options: https.RequestOptions = {
|
||||||
|
hostname: parsed.hostname,
|
||||||
|
port: parsed.port || (is_https ? 443 : 80),
|
||||||
|
path: parsed.pathname + parsed.search,
|
||||||
|
method,
|
||||||
|
headers: { ...this.headers },
|
||||||
|
timeout: TIMEOUT_MS,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.insecure && is_https) {
|
||||||
|
options.rejectUnauthorized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = body !== undefined ? JSON.stringify(body) : undefined;
|
||||||
|
if (payload) {
|
||||||
|
(options.headers as Record<string, string>)['Content-Length'] = Buffer.byteLength(payload).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
const req = transport.request(options, (res) => {
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
res.on('data', (chunk: Buffer) => chunks.push(chunk));
|
||||||
|
res.on('end', () => {
|
||||||
|
const raw = Buffer.concat(chunks).toString('utf-8');
|
||||||
|
let data: unknown;
|
||||||
|
|
||||||
|
try {
|
||||||
|
data = JSON.parse(raw);
|
||||||
|
} catch {
|
||||||
|
data = raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve({ status: res.statusCode ?? 0, data });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', (err) => reject(err));
|
||||||
|
req.on('timeout', () => {
|
||||||
|
req.destroy();
|
||||||
|
reject(new Error('Request timed out'));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (payload) {
|
||||||
|
req.write(payload);
|
||||||
|
}
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
/* 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: dolibarr-api-mcp.Config
|
||||||
|
* INGROUP: dolibarr-api-mcp
|
||||||
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/dolibarr-api-mcp
|
||||||
|
* PATH: /src/config.ts
|
||||||
|
* VERSION: 01.00.00
|
||||||
|
* BRIEF: Configuration loader for Dolibarr API MCP connections
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { readFile } from 'node:fs/promises';
|
||||||
|
import { resolve } from 'node:path';
|
||||||
|
import { homedir } from 'node:os';
|
||||||
|
import type { DolibarrConfig, DolibarrConnection } from './types.js';
|
||||||
|
|
||||||
|
const CONFIG_FILENAME = '.mcp_mokocrm.json';
|
||||||
|
|
||||||
|
export async function loadConfig(): Promise<DolibarrConfig> {
|
||||||
|
const config_path = process.env.DOLIBARR_API_MCP_CONFIG
|
||||||
|
? resolve(process.env.DOLIBARR_API_MCP_CONFIG)
|
||||||
|
: resolve(homedir(), CONFIG_FILENAME);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const raw = await readFile(config_path, 'utf-8');
|
||||||
|
const parsed = JSON.parse(raw) as Partial<DolibarrConfig>;
|
||||||
|
|
||||||
|
if (!parsed.connections || Object.keys(parsed.connections).length === 0) {
|
||||||
|
throw new Error('No connections defined in config');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
connections: parsed.connections,
|
||||||
|
defaultConnection: parsed.defaultConnection ?? Object.keys(parsed.connections)[0],
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
throw new Error(
|
||||||
|
`Failed to load config from ${config_path}: ${message}\n` +
|
||||||
|
`Create ${config_path} — see config.example.json for format.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getConnection(config: DolibarrConfig, name?: string): DolibarrConnection {
|
||||||
|
const key = name ?? config.defaultConnection;
|
||||||
|
const conn = config.connections[key];
|
||||||
|
if (!conn) {
|
||||||
|
throw new Error(
|
||||||
|
`Connection "${key}" not found. Available: ${Object.keys(config.connections).join(', ')}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return conn;
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -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: dolibarr-api-mcp.Types
|
||||||
|
* INGROUP: dolibarr-api-mcp
|
||||||
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/dolibarr-api-mcp
|
||||||
|
* PATH: /src/types.ts
|
||||||
|
* VERSION: 01.00.00
|
||||||
|
* BRIEF: TypeScript type definitions for Dolibarr API MCP server
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface DolibarrConnection {
|
||||||
|
baseUrl: string;
|
||||||
|
apiKey: string;
|
||||||
|
/** Skip TLS certificate verification (self-signed certs) */
|
||||||
|
insecure?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DolibarrConfig {
|
||||||
|
connections: Record<string, DolibarrConnection>;
|
||||||
|
defaultConnection: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiResponse {
|
||||||
|
status: number;
|
||||||
|
data: unknown;
|
||||||
|
}
|
||||||
@@ -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,42 @@
|
|||||||
|
# mcp_mokodreamhost
|
||||||
|
|
||||||
|
MCP server for DreamHost API — DNS records, hosting, and domain management.
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Package** | `@mokoconsulting/dreamhost-mcp` |
|
||||||
|
| **Entry** | `dist/index.js` |
|
||||||
|
| **Config** | `~/.mcp_mokodreamhost.json` (override: `DREAMHOST_MCP_CONFIG` env var) |
|
||||||
|
| **Language** | TypeScript |
|
||||||
|
| **Branch** | develop on `dev`, merge to `main` (protected) |
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install # Install dependencies
|
||||||
|
npm run build # Compile TypeScript → dist/
|
||||||
|
npm run dev # Watch mode
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── index.ts # MCP server entry, tool registration
|
||||||
|
├── config.ts # Loads ~/.mcp_mokodreamhost.json (just apiKey)
|
||||||
|
├── client.ts # DreamHost API client wrapper
|
||||||
|
└── types.ts # DreamHostConfig type
|
||||||
|
```
|
||||||
|
|
||||||
|
- Simple config — just an API key
|
||||||
|
- Manages DNS records, domains, and hosting for all DreamHost-hosted sites
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- **Never commit** `.claude/`, `.mcp.json`, `TODO.md`, `*.min.css`/`*.min.js`
|
||||||
|
- **Attribution**: `Authored-by: Moko Consulting`
|
||||||
|
- **Workflow directory**: `.mokogitea/` (not `.gitea/` or `.github/`)
|
||||||
|
- **Wiki**: documentation lives in the Gitea wiki, not `docs/` files
|
||||||
|
- **Standards**: [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## [1.0] — 2026-05-08
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Initial release
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Contributing
|
||||||
|
|
||||||
|
See [standards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki).
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
# dreamhost-mcp
|
||||||
|
|
||||||
|
[](LICENSE)
|
||||||
|
[](https://nodejs.org)
|
||||||
|
[](https://modelcontextprotocol.io)
|
||||||
|
[](https://www.typescriptlang.org)
|
||||||
|
|
||||||
|
MCP server for the [DreamHost API](https://help.dreamhost.com/hc/en-us/articles/217560167-API-overview) -- DNS records, domains, hosting accounts, MySQL databases, and email management.
|
||||||
|
|
||||||
|
Part of [Moko Consulting](https://mokoconsulting.tech) infrastructure.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tools
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|---|---|
|
||||||
|
| `dreamhost_dns_list` | List all DNS records (optionally filter by domain) |
|
||||||
|
| `dreamhost_dns_add` | Add a DNS record (A, AAAA, CNAME, MX, TXT, SRV) |
|
||||||
|
| `dreamhost_dns_remove` | Remove a DNS record (must match record, type, and value exactly) |
|
||||||
|
| `dreamhost_dns_check` | Check if a DNS record exists for a domain (optional type filter) |
|
||||||
|
| `dreamhost_domain_list` | List all hosted domains |
|
||||||
|
| `dreamhost_domain_registrations` | List domain registrations with expiry dates |
|
||||||
|
| `dreamhost_user_list` | List hosting users and accounts |
|
||||||
|
| `dreamhost_account_status` | Get account status and usage |
|
||||||
|
| `dreamhost_api_commands` | List available API commands for the configured key |
|
||||||
|
| `dreamhost_mysql_list` | List MySQL databases |
|
||||||
|
| `dreamhost_mysql_users` | List MySQL database users |
|
||||||
|
| `dreamhost_mail_list` | List email addresses (optionally filter by domain) |
|
||||||
|
| `dreamhost_rewards_referrals` | List referral rewards |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- **Node.js** >= 20
|
||||||
|
- A **DreamHost API key** -- generate one at [DreamHost Panel > Web Panel API](https://panel.dreamhost.com/?tree=home.api)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://git.mokoconsulting.tech/MokoConsulting/dreamhost-mcp.git
|
||||||
|
cd dreamhost-mcp
|
||||||
|
npm install && npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Create `~/.dreamhost-mcp.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"apiKey": "your-dreamhost-api-key"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Override the config path with the `DREAMHOST_MCP_CONFIG` environment variable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DREAMHOST_MCP_CONFIG=/path/to/config.json node dist/index.js
|
||||||
|
```
|
||||||
|
|
||||||
|
### Claude Code (`.mcp.json`)
|
||||||
|
|
||||||
|
Add to your project or global `.mcp.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"dreamhost": {
|
||||||
|
"command": "node",
|
||||||
|
"args": ["/path/to/dreamhost-mcp/dist/index.js"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
Once connected via MCP, the tools are available to the AI agent directly.
|
||||||
|
|
||||||
|
**List all DNS records for a domain:**
|
||||||
|
|
||||||
|
```
|
||||||
|
dreamhost_dns_list(domain: "example.com")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Add an A record:**
|
||||||
|
|
||||||
|
```
|
||||||
|
dreamhost_dns_add(record: "sub.example.com", type: "A", value: "1.2.3.4", comment: "staging server")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Remove a DNS record:**
|
||||||
|
|
||||||
|
```
|
||||||
|
dreamhost_dns_remove(record: "sub.example.com", type: "A", value: "1.2.3.4")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Check if a CNAME exists:**
|
||||||
|
|
||||||
|
```
|
||||||
|
dreamhost_dns_check(domain: "sub.example.com", type: "CNAME")
|
||||||
|
```
|
||||||
|
|
||||||
|
**List domain registrations and expiry dates:**
|
||||||
|
|
||||||
|
```
|
||||||
|
dreamhost_domain_registrations()
|
||||||
|
```
|
||||||
|
|
||||||
|
**List MySQL databases:**
|
||||||
|
|
||||||
|
```
|
||||||
|
dreamhost_mysql_list()
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
index.ts # MCP server entry point and tool definitions
|
||||||
|
client.ts # DreamHost API HTTP client (HTTPS, 30s timeout)
|
||||||
|
config.ts # Config file loader (~/.dreamhost-mcp.json)
|
||||||
|
types.ts # TypeScript interfaces (DreamHostConfig, DnsRecord, ApiResponse)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev # watch mode (tsc --watch)
|
||||||
|
npm run build # compile TypeScript to dist/
|
||||||
|
npm run start # run the compiled server
|
||||||
|
npm run clean # remove dist/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Full documentation is available on the [Wiki](https://git.mokoconsulting.tech/MokoConsulting/dreamhost-mcp/wiki).
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[GPL-3.0-or-later](LICENSE) -- Copyright (C) 2026 Moko Consulting
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*[Moko Consulting](https://mokoconsulting.tech) -- [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Security
|
||||||
|
|
||||||
|
Report to hello@mokoconsulting.tech.
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"apiKey": "your-dreamhost-api-key"
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "@mokoconsulting/dreamhost-mcp",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "MCP server for DreamHost API — DNS records, hosting, and domain management",
|
||||||
|
"type": "module",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"bin": { "dreamhost-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,84 @@
|
|||||||
|
import * as https from 'node:https';
|
||||||
|
import { randomUUID } from 'node:crypto';
|
||||||
|
import type { ApiResponse } from './types.js';
|
||||||
|
|
||||||
|
const API_HOST = 'api.dreamhost.com';
|
||||||
|
const TIMEOUT_MS = 30_000;
|
||||||
|
|
||||||
|
export class DreamHostClient {
|
||||||
|
private readonly apiKey: string;
|
||||||
|
|
||||||
|
constructor(apiKey: string) {
|
||||||
|
this.apiKey = apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
async callApi(cmd: string, params: Record<string, string> = {}): Promise<ApiResponse> {
|
||||||
|
const query = new URLSearchParams({
|
||||||
|
key: this.apiKey,
|
||||||
|
cmd,
|
||||||
|
format: 'json',
|
||||||
|
unique_id: randomUUID(),
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const opts: https.RequestOptions = {
|
||||||
|
hostname: API_HOST,
|
||||||
|
port: 443,
|
||||||
|
path: `/?${query.toString()}`,
|
||||||
|
method: 'GET',
|
||||||
|
timeout: TIMEOUT_MS,
|
||||||
|
};
|
||||||
|
|
||||||
|
const req = https.request(opts, (res) => {
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
res.on('data', (c: Buffer) => chunks.push(c));
|
||||||
|
res.on('end', () => {
|
||||||
|
const raw = Buffer.concat(chunks).toString();
|
||||||
|
try {
|
||||||
|
resolve(JSON.parse(raw) as ApiResponse);
|
||||||
|
} catch {
|
||||||
|
resolve({ result: 'error', reason: raw });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
req.on('error', reject);
|
||||||
|
req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); });
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async listDnsRecords(): Promise<ApiResponse> {
|
||||||
|
return this.callApi('dns-list_records');
|
||||||
|
}
|
||||||
|
|
||||||
|
async addDnsRecord(record: string, type: string, value: string, comment?: string): Promise<ApiResponse> {
|
||||||
|
const params: Record<string, string> = { record, type, value };
|
||||||
|
if (comment) params.comment = comment;
|
||||||
|
return this.callApi('dns-add_record', params);
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeDnsRecord(record: string, type: string, value: string): Promise<ApiResponse> {
|
||||||
|
return this.callApi('dns-remove_record', { record, type, value });
|
||||||
|
}
|
||||||
|
|
||||||
|
async listDomains(): Promise<ApiResponse> {
|
||||||
|
return this.callApi('domain-list_domains');
|
||||||
|
}
|
||||||
|
|
||||||
|
async listRegistrations(): Promise<ApiResponse> {
|
||||||
|
return this.callApi('domain-list_registrations');
|
||||||
|
}
|
||||||
|
|
||||||
|
async listUsers(): Promise<ApiResponse> {
|
||||||
|
return this.callApi('account-list_accounts');
|
||||||
|
}
|
||||||
|
|
||||||
|
async accountStatus(): Promise<ApiResponse> {
|
||||||
|
return this.callApi('account-status');
|
||||||
|
}
|
||||||
|
|
||||||
|
async listCommands(): Promise<ApiResponse> {
|
||||||
|
return this.callApi('api-list_accessible_cmds');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { readFile } from 'node:fs/promises';
|
||||||
|
import { resolve } from 'node:path';
|
||||||
|
import { homedir } from 'node:os';
|
||||||
|
import type { DreamHostConfig } from './types.js';
|
||||||
|
|
||||||
|
const CONFIG_FILENAME = '.mcp_mokodreamhost.json';
|
||||||
|
|
||||||
|
export async function loadConfig(): Promise<DreamHostConfig> {
|
||||||
|
const configPath = process.env.DREAMHOST_MCP_CONFIG
|
||||||
|
? resolve(process.env.DREAMHOST_MCP_CONFIG)
|
||||||
|
: resolve(homedir(), CONFIG_FILENAME);
|
||||||
|
|
||||||
|
const raw = await readFile(configPath, 'utf-8');
|
||||||
|
const parsed = JSON.parse(raw) as Partial<DreamHostConfig>;
|
||||||
|
if (!parsed.apiKey) {
|
||||||
|
throw new Error(`No apiKey in ${configPath}`);
|
||||||
|
}
|
||||||
|
return { apiKey: parsed.apiKey };
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
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 { DreamHostClient } from './client.js';
|
||||||
|
import type { DreamHostConfig, ApiResponse } from './types.js';
|
||||||
|
|
||||||
|
let client: DreamHostClient;
|
||||||
|
|
||||||
|
function fmt(res: ApiResponse) {
|
||||||
|
if (res.result === 'error') return { content: [{ type: 'text' as const, text: `Error: ${res.reason ?? JSON.stringify(res)}` }] };
|
||||||
|
return { content: [{ type: 'text' as const, text: JSON.stringify(res.data ?? res, null, 2) }] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = new McpServer({ name: 'dreamhost-mcp', version: '1.0.0' });
|
||||||
|
|
||||||
|
server.tool('dreamhost_dns_list', 'List all DNS records (optionally filter by domain)', {
|
||||||
|
domain: z.string().optional().describe('Filter by domain name'),
|
||||||
|
}, async ({ domain }) => {
|
||||||
|
const res = await client.listDnsRecords();
|
||||||
|
if (res.result === 'error') return fmt(res);
|
||||||
|
if (domain && Array.isArray(res.data)) {
|
||||||
|
res.data = (res.data as Array<{ record: string }>).filter(r => r.record.includes(domain));
|
||||||
|
}
|
||||||
|
return fmt(res);
|
||||||
|
});
|
||||||
|
|
||||||
|
server.tool('dreamhost_dns_add', 'Add a DNS record', {
|
||||||
|
record: z.string().describe('DNS record name (e.g. sub.example.com)'),
|
||||||
|
type: z.string().describe('Record type (A, AAAA, CNAME, MX, TXT, SRV)'),
|
||||||
|
value: z.string().describe('Record value'),
|
||||||
|
comment: z.string().optional().describe('Optional comment'),
|
||||||
|
}, async ({ record, type, value, comment }) => fmt(await client.addDnsRecord(record, type, value, comment)));
|
||||||
|
|
||||||
|
server.tool('dreamhost_dns_remove', 'Remove a DNS record', {
|
||||||
|
record: z.string().describe('DNS record name'),
|
||||||
|
type: z.string().describe('Record type'),
|
||||||
|
value: z.string().describe('Record value (must match exactly)'),
|
||||||
|
}, async ({ record, type, value }) => fmt(await client.removeDnsRecord(record, type, value)));
|
||||||
|
|
||||||
|
server.tool('dreamhost_domain_list', 'List all hosted domains', {},
|
||||||
|
async () => fmt(await client.listDomains()));
|
||||||
|
|
||||||
|
server.tool('dreamhost_domain_registrations', 'List domain registrations with expiry dates', {},
|
||||||
|
async () => fmt(await client.listRegistrations()));
|
||||||
|
|
||||||
|
server.tool('dreamhost_user_list', 'List hosting users/accounts', {},
|
||||||
|
async () => fmt(await client.listUsers()));
|
||||||
|
|
||||||
|
server.tool('dreamhost_account_status', 'Get account status and usage', {},
|
||||||
|
async () => fmt(await client.accountStatus()));
|
||||||
|
|
||||||
|
server.tool('dreamhost_api_commands', 'List available API commands', {},
|
||||||
|
async () => fmt(await client.listCommands()));
|
||||||
|
|
||||||
|
server.tool('dreamhost_mysql_list', 'List MySQL databases', {},
|
||||||
|
async () => fmt(await client.callApi('mysql-list_dbs')));
|
||||||
|
|
||||||
|
server.tool('dreamhost_mysql_users', 'List MySQL database users', {},
|
||||||
|
async () => fmt(await client.callApi('mysql-list_users')));
|
||||||
|
|
||||||
|
server.tool('dreamhost_mail_list', 'List email addresses for a domain', {
|
||||||
|
domain: z.string().optional().describe('Filter by domain'),
|
||||||
|
}, async ({ domain }) => {
|
||||||
|
const res = await client.callApi('mail-list_filters');
|
||||||
|
if (domain && res.result !== 'error' && Array.isArray(res.data)) {
|
||||||
|
res.data = (res.data as Array<{ account: string }>).filter(a => a.account?.includes(domain));
|
||||||
|
}
|
||||||
|
return fmt(res);
|
||||||
|
});
|
||||||
|
|
||||||
|
server.tool('dreamhost_dns_check', 'Check if a DNS record exists for a domain', {
|
||||||
|
domain: z.string().describe('Domain to check'),
|
||||||
|
type: z.string().optional().describe('Record type to filter (A, CNAME, MX, TXT)'),
|
||||||
|
}, async ({ domain, type }) => {
|
||||||
|
const res = await client.listDnsRecords();
|
||||||
|
if (res.result === 'error') return fmt(res);
|
||||||
|
let records = res.data as Array<{ record: string; type: string; value: string }>;
|
||||||
|
records = records.filter(r => r.record.includes(domain));
|
||||||
|
if (type) records = records.filter(r => r.type === type);
|
||||||
|
if (records.length === 0) return { content: [{ type: 'text' as const, text: `No ${type ?? ''} records found for ${domain}` }] };
|
||||||
|
return { content: [{ type: 'text' as const, text: records.map(r => `${r.record} ${r.type} ${r.value}`).join('\n') }] };
|
||||||
|
});
|
||||||
|
|
||||||
|
server.tool('dreamhost_rewards_referrals', 'List referral rewards', {},
|
||||||
|
async () => fmt(await client.callApi('rewards-list_referrals')));
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const config = await loadConfig();
|
||||||
|
client = new DreamHostClient(config.apiKey);
|
||||||
|
await server.connect(new StdioServerTransport());
|
||||||
|
}
|
||||||
|
main().catch(err => { console.error(err); process.exit(1); });
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
export interface DreamHostConfig {
|
||||||
|
apiKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DnsRecord {
|
||||||
|
record: string;
|
||||||
|
type: string;
|
||||||
|
value: string;
|
||||||
|
comment?: string;
|
||||||
|
zone: string;
|
||||||
|
editable: string;
|
||||||
|
account_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DomainInfo {
|
||||||
|
domain: string;
|
||||||
|
type: string;
|
||||||
|
home: string;
|
||||||
|
hosting_type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiResponse {
|
||||||
|
result: string;
|
||||||
|
data?: unknown;
|
||||||
|
reason?: string;
|
||||||
|
}
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
Submodule
+1
Submodule mcp/servers/mokogitea_api added at 44e1259c3e
@@ -0,0 +1,49 @@
|
|||||||
|
---
|
||||||
|
name: deploy
|
||||||
|
description: Build and deploy extension to a target server via SFTP/SSH
|
||||||
|
allowed-tools: Bash, Read, Glob, Grep
|
||||||
|
---
|
||||||
|
|
||||||
|
# Deploy Extension
|
||||||
|
|
||||||
|
Build the current project and deploy to a target server.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
- Current directory: !`pwd`
|
||||||
|
- Git status: !`git status --short`
|
||||||
|
- Platform manifest: !`cat .mokogitea/manifest.xml 2>/dev/null | head -10`
|
||||||
|
|
||||||
|
## Target Servers
|
||||||
|
|
||||||
|
| Name | Host | Joomla Path | Dolibarr Path |
|
||||||
|
|---|---|---|---|
|
||||||
|
| WAAS_DEV | waas.dev.mokoconsulting.tech | /home/mokoconsulting_dev/waas.dev.mokoconsulting.tech/ | — |
|
||||||
|
| WAAS_DEMO | waas.demo.mokoconsulting.tech | /home/mokoconsulting_demo/waas.demo.mokoconsulting.tech/ | — |
|
||||||
|
| WAAS_LIVE | mokoconsulting.tech | /home/mokoconsulting/mokoconsulting.tech/ | — |
|
||||||
|
| CRM_DEV | waas.dev.mokoconsulting.tech | — | /home/mokoconsulting_dev/crm.dev.mokoconsulting.tech/htdocs/custom/ |
|
||||||
|
| CRM_LIVE | crm.mokoconsulting.tech | — | /home/mokoconsulting_crm/crm.mokoconsulting.tech/htdocs/custom/ |
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
|
||||||
|
1. **Identify platform** from `.mokogitea/manifest.xml`:
|
||||||
|
- `joomla` → build ZIP, install via Joomla API or SFTP
|
||||||
|
- `dolibarr` → SFTP module files to `htdocs/custom/`
|
||||||
|
- `mcp-server` → npm build, no remote deploy needed
|
||||||
|
|
||||||
|
2. **Ask target** if not specified: dev, demo, or live?
|
||||||
|
- Default to dev for safety
|
||||||
|
|
||||||
|
3. **Build:**
|
||||||
|
- Run `make build` or `make release` depending on platform
|
||||||
|
- Verify build output exists
|
||||||
|
|
||||||
|
4. **Deploy:**
|
||||||
|
- **Joomla**: Use `mcp_mokowaas` tool to install ZIP via Joomla API, OR use `mcp_mokossh` to SFTP + run CLI installer
|
||||||
|
- **Dolibarr**: Use `mcp_mokossh` to rsync/scp module directory to `htdocs/custom/`
|
||||||
|
|
||||||
|
5. **Post-deploy:**
|
||||||
|
- Clear Joomla cache if applicable (`php cli/joomla.php cache:clean`)
|
||||||
|
- Verify extension is active
|
||||||
|
|
||||||
|
6. **NEVER deploy to LIVE without explicit user confirmation.**
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
name: mcp-rebuild
|
||||||
|
description: Rebuild one or all MCP servers — npm install + npm run build
|
||||||
|
allowed-tools: Bash, Read, Glob, Grep
|
||||||
|
---
|
||||||
|
|
||||||
|
# MCP Server Rebuild
|
||||||
|
|
||||||
|
Rebuild MCP server(s) by running `npm install` and `npm run build`.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
- Current directory: !`pwd`
|
||||||
|
- MCP servers live at `A:/MCP/mcp_moko*/`
|
||||||
|
|
||||||
|
## MCP Server List
|
||||||
|
|
||||||
|
| Server | Path | Build? |
|
||||||
|
|---|---|---|
|
||||||
|
| mcp_mokobackup | A:/MCP/mcp_mokobackup | Yes (TypeScript) |
|
||||||
|
| mcp_mokocrm | A:/MCP/mcp_mokocrm | Yes (TypeScript) |
|
||||||
|
| mcp_mokodreamhost | A:/MCP/mcp_mokodreamhost | Yes (TypeScript) |
|
||||||
|
| mcp_mokogitea_api | A:/MCP/mcp_mokogitea_api | Yes (TypeScript) |
|
||||||
|
| mcp_mokomonitor | A:/MCP/mcp_mokomonitor | Yes (TypeScript) |
|
||||||
|
| mcp_mokossh | A:/MCP/mcp_mokossh | No (plain JS) |
|
||||||
|
| mcp_mokowaas | A:/MCP/mcp_mokowaas | Yes (TypeScript) |
|
||||||
|
| mcp_windows | A:/MCP/mcp_windows | Yes (TypeScript) |
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
|
||||||
|
1. If the user specified a server name, rebuild only that one. If "all", rebuild all TypeScript MCPs (skip mcp_mokossh).
|
||||||
|
2. If no server specified and we're inside an MCP directory, rebuild that one.
|
||||||
|
3. For each server:
|
||||||
|
- Run `npm install` in the server directory
|
||||||
|
- Run `npm run build` (skip for mcp_mokossh — it's plain JS)
|
||||||
|
- Report success/failure
|
||||||
|
4. After rebuilding, remind the user they need to restart Claude Code for MCP changes to take effect.
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
---
|
||||||
|
name: new-client
|
||||||
|
description: Scaffold a new WaaS client — Gitea org, theme repo, MCP config, wiki, SSH key
|
||||||
|
allowed-tools: Bash, Read, Write, Edit, Glob, Grep
|
||||||
|
---
|
||||||
|
|
||||||
|
# New WaaS Client Setup
|
||||||
|
|
||||||
|
Scaffold everything needed for a new WaaS client deployment.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
- Current directory: !`pwd`
|
||||||
|
- Template repo: A:/Templates/Template-Client-WaaS/
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
|
||||||
|
Ask the user for:
|
||||||
|
1. **Client name** (e.g. "Acme Corp")
|
||||||
|
2. **Client slug** (e.g. "acmecorp" — lowercase, no spaces, used in org/repo names)
|
||||||
|
3. **Domain** (e.g. "acmecorp.com")
|
||||||
|
|
||||||
|
Then execute these steps:
|
||||||
|
|
||||||
|
### 1. Create Gitea Organization
|
||||||
|
- Use `mcp_mokogitea_api` tool `gitea_org_create`
|
||||||
|
- Org name: PascalCase of slug (e.g. "AcmeCorp")
|
||||||
|
- Description: "WaaS client: {Client name}"
|
||||||
|
|
||||||
|
### 2. Create Client Repository
|
||||||
|
- Use `mcp_mokogitea_api` tool `gitea_repo_create` in the new org
|
||||||
|
- Repo name: `client-waas-{slug}`
|
||||||
|
- Initialize from Template-Client-WaaS if possible, otherwise create empty and copy files
|
||||||
|
|
||||||
|
### 3. Clone and Scaffold Locally
|
||||||
|
- Clone to `A:/client-{slug}/`
|
||||||
|
- Copy template files from `A:/Templates/Template-Client-WaaS/`
|
||||||
|
- Update `src/*.xml` manifest:
|
||||||
|
- `<name>MokoOnyx Theme — {Client Name}</name>`
|
||||||
|
- `<element>file_mokoonyx_{slug}</element>`
|
||||||
|
- Create `.mokogitea/CLAUDE.md` with client-specific content
|
||||||
|
- Create initial `CHANGELOG.md`
|
||||||
|
- Commit and push
|
||||||
|
|
||||||
|
### 4. Create SSH Key
|
||||||
|
- Generate key pair at `C:/Users/jmill/OneDrive/Documents/Keys/repos/client-{slug}`
|
||||||
|
- Add public key as deploy key on the Gitea repo
|
||||||
|
|
||||||
|
### 5. Setup GitHub Mirror
|
||||||
|
- Use `gitea_repo_mirror_setup_github_backup_full` to create GitHub backup mirror
|
||||||
|
|
||||||
|
### 6. Initialize Wiki
|
||||||
|
- Create Home page with client info using `mcp_mokogitea_wiki` tools
|
||||||
|
|
||||||
|
### 7. Apply Labels
|
||||||
|
- Apply standard label set using `mcp_mokogitea_api` tools
|
||||||
|
- Labels use colon-style (e.g. "priority: critical")
|
||||||
|
|
||||||
|
### 8. Report
|
||||||
|
- Show all created resources: org URL, repo URL, GitHub mirror, local path
|
||||||
|
- Remind about: DNS setup, Joomla site creation, theme customization
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
---
|
||||||
|
name: release
|
||||||
|
description: Create a release — build ZIP, tag, update updates.xml, create Gitea release
|
||||||
|
allowed-tools: Bash, Read, Write, Edit, Glob, Grep
|
||||||
|
---
|
||||||
|
|
||||||
|
# Release Workflow
|
||||||
|
|
||||||
|
Create a release for the current repository.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
- Current directory: !`pwd`
|
||||||
|
- Git status: !`git status --short`
|
||||||
|
- Current branch: !`git rev-parse --abbrev-ref HEAD`
|
||||||
|
- Recent tags: !`git tag --sort=-creatordate | head -5`
|
||||||
|
- Platform manifest: !`cat .mokogitea/manifest.xml 2>/dev/null | head -10`
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
|
||||||
|
1. **Pre-flight checks:**
|
||||||
|
- Ensure working tree is clean (no uncommitted changes)
|
||||||
|
- Ensure we're on `main` or `dev` branch
|
||||||
|
- Read the current version from the manifest XML or package.json
|
||||||
|
|
||||||
|
2. **Determine release type:**
|
||||||
|
- If user specified a version, use it
|
||||||
|
- Otherwise, ask: patch, minor, or major bump?
|
||||||
|
|
||||||
|
3. **Platform-specific build:**
|
||||||
|
- **Joomla** (`make build` or `make release`): builds ZIP in `dist/` or `build/`
|
||||||
|
- **MCP/Node** (`npm run build`): compiles TypeScript
|
||||||
|
- **Dolibarr** (`make build`): packages module
|
||||||
|
|
||||||
|
4. **Update version files:**
|
||||||
|
- Joomla: update `<version>` in manifest XML, update `updates.xml` in repo root
|
||||||
|
- Node: update `version` in `package.json`
|
||||||
|
- Update CHANGELOG.md with release date
|
||||||
|
|
||||||
|
5. **Commit, tag, push:**
|
||||||
|
- Commit version bump: `chore(release): bump to vX.Y.Z`
|
||||||
|
- Create annotated tag: `git tag -a vX.Y.Z -m "Release X.Y.Z"`
|
||||||
|
- Push commits and tags: `git push origin && git push origin --tags`
|
||||||
|
|
||||||
|
6. **Create Gitea release:**
|
||||||
|
- Use `mcp_mokogitea_api` tool `gitea_release_create` with the tag
|
||||||
|
- Upload the built ZIP as a release asset via `gitea_release_upload_asset`
|
||||||
|
|
||||||
|
7. **Joomla-specific: update updates.xml:**
|
||||||
|
- Prepend new `<update>` block with version, download URL pointing to Gitea release asset
|
||||||
|
- ZIP filename follows Joomla convention from feedback_joomla_release_naming memory
|
||||||
|
- Commit and push the updates.xml change
|
||||||
|
|
||||||
|
8. **Report:** show the release URL and confirm success.
|
||||||
|
|
||||||
|
## Important Rules
|
||||||
|
|
||||||
|
- Release ZIP filenames follow Joomla conventions (see feedback_joomla_release_naming)
|
||||||
|
- updates.xml goes in repo root, not src/
|
||||||
|
- GitHub mirrors sync automatically — do NOT push to GitHub
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
---
|
||||||
|
name: server-health
|
||||||
|
description: Run health checks across all MokoGitea infrastructure servers
|
||||||
|
allowed-tools: Bash, Read, Glob, Grep
|
||||||
|
---
|
||||||
|
|
||||||
|
# Server Health Check
|
||||||
|
|
||||||
|
Run health checks across all infrastructure servers using SSH.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
- Current time: !`date`
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
|
||||||
|
Use `mcp_mokossh` MCP tools to run health checks on each server:
|
||||||
|
|
||||||
|
### 1. GIT Server (git.mokoconsulting.tech)
|
||||||
|
- `systemctl status gitea` — is Gitea running?
|
||||||
|
- `systemctl status act_runner` — is the Actions runner running?
|
||||||
|
- `df -h /` — disk space
|
||||||
|
- `du -sh /var/lib/gitea/repositories/ /var/lib/gitea/data/ /var/lib/gitea/log/` — Gitea storage breakdown
|
||||||
|
- `uptime` — load average
|
||||||
|
- `free -h` — memory usage
|
||||||
|
- `certbot certificates 2>/dev/null | grep -E "Expiry|Domains"` — SSL cert expiry
|
||||||
|
- `fail2ban-client status sshd 2>/dev/null | grep "Total banned"` — banned IPs
|
||||||
|
|
||||||
|
### 2. WAAS_DEV (waas.dev.mokoconsulting.tech)
|
||||||
|
- `df -h /` — disk space
|
||||||
|
- `uptime` — load average
|
||||||
|
- `free -h` — memory
|
||||||
|
- `php -v | head -1` — PHP version
|
||||||
|
- Check Joomla health endpoint if available
|
||||||
|
|
||||||
|
### 3. WAAS_DEMO (waas.demo.mokoconsulting.tech)
|
||||||
|
- Same checks as WAAS_DEV
|
||||||
|
|
||||||
|
### 4. WAAS_LIVE (mokoconsulting.tech)
|
||||||
|
- Same checks as WAAS_DEV
|
||||||
|
- Extra: check Apache/Nginx status
|
||||||
|
|
||||||
|
### 5. CRM_LIVE (crm.mokoconsulting.tech)
|
||||||
|
- `df -h /` — disk space
|
||||||
|
- `uptime` — load average
|
||||||
|
- `free -h` — memory
|
||||||
|
|
||||||
|
### Output Format
|
||||||
|
|
||||||
|
Present results as a summary table:
|
||||||
|
|
||||||
|
| Server | Status | Disk | Memory | Load | SSL Expiry | Issues |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
|
||||||
|
Flag any issues in red (disk > 85%, high load, expiring SSL, stopped services).
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
---
|
||||||
|
name: mokogitea
|
||||||
|
description: "MokoGitea server infrastructure, SSH commands, deployment, backup, and administration. Use when the user mentions: gitea, mokogitea, git server, deploy, deployment, restart gitea, ssh into, server status, backup gitea, restore, mirror, github backup, update server, gitea actions, CI/CD, runner, waas server, crm server, server health, disk space, systemctl, nginx, certbot, fail2ban, firewall, server logs, gitea logs, or any remote server operation."
|
||||||
|
when_to_use: "Auto-trigger when discussing: server management, SSH operations, Gitea administration, deployments, backups, mirrors, CI runners, or any infrastructure task involving git.mokoconsulting.tech or the MokoGitea instance."
|
||||||
|
---
|
||||||
|
|
||||||
|
# MokoGitea Infrastructure Reference
|
||||||
|
|
||||||
|
You are helping with MokoGitea server infrastructure. Use the `mcp_mokossh` MCP tools for SSH commands and `mcp_mokogitea_api` tools for Gitea API operations.
|
||||||
|
|
||||||
|
## Server Map
|
||||||
|
|
||||||
|
| Name | Host | User | Port | Purpose |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| GIT | git.mokoconsulting.tech | mokoconsulting | 2918 | MokoGitea instance (Gitea fork) |
|
||||||
|
| WAAS_DEV | waas.dev.mokoconsulting.tech | mokoconsulting_dev | 22 | WaaS dev (Joomla + Dolibarr) |
|
||||||
|
| WAAS_DEMO | waas.demo.mokoconsulting.tech | mokoconsulting_demo | 22 | WaaS demo |
|
||||||
|
| WAAS_LIVE | mokoconsulting.tech | mokoconsulting | 22 | WaaS production |
|
||||||
|
| CRM_DEV | waas.dev.mokoconsulting.tech | mokoconsulting_dev | 22 | CRM dev (shared host with WAAS_DEV) |
|
||||||
|
| CRM_LIVE | crm.mokoconsulting.tech | mokoconsulting_crm | 22 | CRM production |
|
||||||
|
|
||||||
|
SSH key: `jmiller_private.openssh` (all MCP connections)
|
||||||
|
|
||||||
|
## Common SSH Commands
|
||||||
|
|
||||||
|
### Gitea Server (GIT)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Service management
|
||||||
|
sudo systemctl status gitea
|
||||||
|
sudo systemctl restart gitea
|
||||||
|
sudo systemctl stop gitea
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
sudo journalctl -u gitea -f # Follow live logs
|
||||||
|
sudo journalctl -u gitea --since "1 hour ago" # Recent logs
|
||||||
|
|
||||||
|
# Gitea CLI (run as git user)
|
||||||
|
sudo -u git /usr/local/bin/gitea admin user list
|
||||||
|
sudo -u git /usr/local/bin/gitea admin auth list
|
||||||
|
sudo -u git /usr/local/bin/gitea doctor check
|
||||||
|
|
||||||
|
# Gitea Actions runner
|
||||||
|
sudo systemctl status act_runner
|
||||||
|
sudo systemctl restart act_runner
|
||||||
|
sudo journalctl -u act_runner -f
|
||||||
|
|
||||||
|
# Config
|
||||||
|
cat /etc/gitea/app.ini # Main config
|
||||||
|
sudo -u git /usr/local/bin/gitea admin config # Dump effective config
|
||||||
|
|
||||||
|
# Database
|
||||||
|
sudo mysql -u root gitea -e "SELECT name FROM repository ORDER BY updated_unix DESC LIMIT 10;"
|
||||||
|
|
||||||
|
# Disk space
|
||||||
|
df -h
|
||||||
|
du -sh /var/lib/gitea/repositories/
|
||||||
|
du -sh /var/lib/gitea/data/
|
||||||
|
du -sh /var/lib/gitea/log/
|
||||||
|
|
||||||
|
# Nginx
|
||||||
|
sudo nginx -t # Test config
|
||||||
|
sudo systemctl reload nginx
|
||||||
|
cat /etc/nginx/sites-enabled/gitea.conf
|
||||||
|
|
||||||
|
# SSL
|
||||||
|
sudo certbot certificates
|
||||||
|
sudo certbot renew --dry-run
|
||||||
|
|
||||||
|
# Firewall
|
||||||
|
sudo ufw status
|
||||||
|
sudo fail2ban-client status
|
||||||
|
sudo fail2ban-client status sshd
|
||||||
|
```
|
||||||
|
|
||||||
|
### WaaS Servers (WAAS_DEV / WAAS_DEMO / WAAS_LIVE)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Joomla paths
|
||||||
|
# Dev: /home/mokoconsulting_dev/waas.dev.mokoconsulting.tech/
|
||||||
|
# Demo: /home/mokoconsulting_demo/waas.demo.mokoconsulting.tech/
|
||||||
|
# Live: /home/mokoconsulting/mokoconsulting.tech/
|
||||||
|
|
||||||
|
# Check Joomla version
|
||||||
|
php cli/joomla.php core:check-updates
|
||||||
|
|
||||||
|
# Clear Joomla cache
|
||||||
|
php cli/joomla.php cache:clean
|
||||||
|
|
||||||
|
# Run scheduled tasks
|
||||||
|
php cli/joomla.php scheduler:run
|
||||||
|
|
||||||
|
# Check PHP version
|
||||||
|
php -v
|
||||||
|
|
||||||
|
# Apache/Nginx status
|
||||||
|
sudo systemctl status apache2 # or nginx
|
||||||
|
sudo apachectl -S # List virtual hosts
|
||||||
|
|
||||||
|
# Database
|
||||||
|
mysql -u root -e "SHOW DATABASES;"
|
||||||
|
|
||||||
|
# Disk
|
||||||
|
df -h
|
||||||
|
du -sh /home/*/
|
||||||
|
```
|
||||||
|
|
||||||
|
### CRM Servers (CRM_DEV / CRM_LIVE)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Dolibarr paths
|
||||||
|
# Dev: /home/mokoconsulting_dev/crm.dev.mokoconsulting.tech/htdocs/
|
||||||
|
# Live: /home/mokoconsulting_crm/crm.mokoconsulting.tech/htdocs/
|
||||||
|
|
||||||
|
# Check Dolibarr version
|
||||||
|
grep 'DOL_VERSION' htdocs/filefunc.inc.php
|
||||||
|
|
||||||
|
# Dolibarr conf
|
||||||
|
cat htdocs/conf/conf.php | grep -E "^\\$dolibarr_main_(db|url)"
|
||||||
|
|
||||||
|
# Custom modules
|
||||||
|
ls htdocs/custom/
|
||||||
|
|
||||||
|
# Database
|
||||||
|
mysql -u root dolibarr -e "SELECT name, value FROM llx_const WHERE name LIKE '%VERSION%';"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Gitea API Operations
|
||||||
|
|
||||||
|
Use `mcp_mokogitea_api` MCP tools for API operations:
|
||||||
|
|
||||||
|
- **Repos**: `gitea_repo_list`, `gitea_repo_create`, `gitea_repo_delete`, `gitea_repo_get`
|
||||||
|
- **Issues**: `gitea_issue_create`, `gitea_issue_list`, `gitea_issue_comment`
|
||||||
|
- **PRs**: `gitea_pr_create`, `gitea_pr_list`, `gitea_pr_merge`
|
||||||
|
- **Releases**: `gitea_release_create`, `gitea_release_list`, `gitea_release_upload_asset`
|
||||||
|
- **Mirrors**: `gitea_repo_mirror_create`, `gitea_repo_mirror_setup_github_backup`, `gitea_repo_mirror_setup_github_backup_full`
|
||||||
|
- **Actions**: `gitea_actions_list_runs`, `gitea_actions_get_run`
|
||||||
|
- **Orgs**: `gitea_org_list`, `gitea_org_create`, `gitea_org_get`
|
||||||
|
- **Wiki**: use `mcp_mokogitea_wiki` tools for wiki CRUD
|
||||||
|
|
||||||
|
## Backup Operations
|
||||||
|
|
||||||
|
Use `mcp_mokobackup` MCP tools:
|
||||||
|
|
||||||
|
| Target | Type | What it backs up |
|
||||||
|
|---|---|---|
|
||||||
|
| gitea-db | mysql | Gitea MySQL database |
|
||||||
|
| gitea-files | files | `/var/lib/gitea/` (repos, data, avatars) |
|
||||||
|
| waas-dev | akeeba | Joomla dev site via Akeeba API |
|
||||||
|
| waas-demo | akeeba | Joomla demo site |
|
||||||
|
| waas-live | akeeba | Joomla production site |
|
||||||
|
| crm-live | dolibarr | Dolibarr production (DB + documents + custom) |
|
||||||
|
| crm-dev | dolibarr | Dolibarr dev |
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
Deployments are handled by **Gitea Actions workflows** (not manual SSH):
|
||||||
|
- Push to `dev` → CI runs (lint, build, validate)
|
||||||
|
- Merge PR to `main` → release workflow builds ZIP, creates release, deploys via SFTP
|
||||||
|
- Client sites: `client-release.yml` workflow handles theme package deployment
|
||||||
|
- Dolibarr modules: manual SFTP to `htdocs/custom/` via `mcp_mokossh`
|
||||||
|
|
||||||
|
## Mirror / GitHub Backup
|
||||||
|
|
||||||
|
All repos mirror to GitHub (mokoconsulting-tech org) as backup:
|
||||||
|
- Code + wiki mirrors, synced every 8 hours + on commit
|
||||||
|
- Use `gitea_repo_mirror_setup_github_backup_full` for new repos
|
||||||
|
- GitHub is **backup only** — never create PRs or enable Actions on GitHub
|
||||||
|
|
||||||
|
## Key Paths on GIT Server
|
||||||
|
|
||||||
|
| Path | Contents |
|
||||||
|
|---|---|
|
||||||
|
| `/usr/local/bin/gitea` | Gitea binary |
|
||||||
|
| `/etc/gitea/app.ini` | Main configuration |
|
||||||
|
| `/var/lib/gitea/` | All Gitea data |
|
||||||
|
| `/var/lib/gitea/repositories/` | Git bare repos |
|
||||||
|
| `/var/lib/gitea/data/` | Attachments, avatars, LFS |
|
||||||
|
| `/var/lib/gitea/log/` | Gitea logs |
|
||||||
|
| `/var/lib/gitea/custom/` | Custom templates, public files |
|
||||||
|
| `/etc/nginx/sites-enabled/gitea.conf` | Nginx reverse proxy config |
|
||||||
|
| `/home/git/.act_runner/` | Actions runner config |
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
|
||||||
|
- Port 2918 on GIT server is **shell SSH** (not just git protocol) — full command execution
|
||||||
|
- Gitea repo names on server use **hyphens** (e.g. `mcp-mokobackup`), local dirs use **underscores**
|
||||||
|
- `moko-platform` CLI tools handle CI checks — don't inline bash in workflows
|
||||||
|
- All infra docs live in **mokogitea-private wiki**, not public repos
|
||||||
|
- Two master SSH keys (jmiller + moko) on all servers
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<mokoplatform schema-version="1.0">
|
||||||
|
<identity><name>monitor-mcp</name><org>MokoConsulting</org></identity>
|
||||||
|
<governance><standards-version>05.00.00</standards-version></governance>
|
||||||
|
</mokoplatform>
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# mcp_mokomonitor
|
||||||
|
|
||||||
|
MCP server for server health monitoring, uptime checks, Grafana dashboards, and log tailing.
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Package** | `@mokoconsulting/mcp-mokomonitor` |
|
||||||
|
| **Entry** | `dist/index.js` |
|
||||||
|
| **Config** | `~/.mcp_mokomonitor.json` (override: `MONITOR_MCP_CONFIG` env var) |
|
||||||
|
| **Language** | TypeScript |
|
||||||
|
| **Branch** | develop on `dev`, merge to `main` (protected) |
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install # Install dependencies
|
||||||
|
npm run build # Compile TypeScript → dist/
|
||||||
|
npm run dev # Watch mode
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── index.ts # MCP server entry, tool registration
|
||||||
|
├── config.ts # Loads ~/.mcp_mokomonitor.json, resolves connections + Grafana + sites.json
|
||||||
|
├── client.ts # HTTP health check client
|
||||||
|
├── grafana.ts # Grafana API integration (dashboards, panels, alerts)
|
||||||
|
└── types.ts # MonitorConfig, MonitorConnection, SitesConfig types
|
||||||
|
```
|
||||||
|
|
||||||
|
- Config defines **connections** for health checks + optional **Grafana** config
|
||||||
|
- Sites list at `A:/moko-platform/monitoring/sites.json` for bulk monitoring
|
||||||
|
- Grafana at bench.mokoconsulting.tech — WaaS dashboard for uptime/performance
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- **Never commit** `.claude/`, `.mcp.json`, `TODO.md`, `*.min.css`/`*.min.js`
|
||||||
|
- **Attribution**: `Authored-by: Moko Consulting`
|
||||||
|
- **Workflow directory**: `.mokogitea/` (not `.gitea/` or `.github/`)
|
||||||
|
- **Wiki**: documentation lives in the Gitea wiki, not `docs/` files
|
||||||
|
- **Standards**: [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
---
|
||||||
|
name: API Integration Request
|
||||||
|
about: Request integration with a new REST API or service
|
||||||
|
title: '[API] '
|
||||||
|
labels: 'enhancement, api-integration'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API Integration Request
|
||||||
|
|
||||||
|
### Target API
|
||||||
|
- **Service Name**: [e.g., Akeeba Backup, Joomla Web Services]
|
||||||
|
- **API Documentation**: [URL to API docs]
|
||||||
|
- **API Type**: [REST / GraphQL / SOAP]
|
||||||
|
- **Authentication**: [API Key / OAuth / Bearer Token / Basic Auth]
|
||||||
|
|
||||||
|
### Proposed Tools
|
||||||
|
List the MCP tools this integration would provide:
|
||||||
|
|
||||||
|
| Tool Name | HTTP Method | Endpoint | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `service_list` | GET | `/api/items` | List all items |
|
||||||
|
| `service_get` | GET | `/api/items/{id}` | Get single item |
|
||||||
|
| `service_create` | POST | `/api/items` | Create item |
|
||||||
|
|
||||||
|
### Multi-Connection
|
||||||
|
- [ ] Single instance only
|
||||||
|
- [ ] Multiple instances (production, staging, dev)
|
||||||
|
- [ ] Multi-tenant (one connection per client)
|
||||||
|
|
||||||
|
### Use Case
|
||||||
|
Describe the workflow this integration enables for AI assistants.
|
||||||
|
|
||||||
|
### Priority
|
||||||
|
- [ ] Critical — blocking current work
|
||||||
|
- [ ] High — needed soon
|
||||||
|
- [ ] Medium — would improve workflow
|
||||||
|
- [ ] Low — nice to have
|
||||||
|
|
||||||
|
### Existing Alternatives
|
||||||
|
Are there other ways to accomplish this today? If so, why is an MCP integration better?
|
||||||
|
|
||||||
|
### Checklist
|
||||||
|
- [ ] API documentation is available and accessible
|
||||||
|
- [ ] API supports the required authentication method
|
||||||
|
- [ ] I have tested the API endpoints manually
|
||||||
|
- [ ] The integration follows the Template-MCP architecture pattern
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
---
|
||||||
|
name: MCP Connection Issue
|
||||||
|
about: Report a connection, authentication, or API communication issue
|
||||||
|
title: '[CONNECTION] '
|
||||||
|
labels: 'bug, mcp-connection'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Connection Issue
|
||||||
|
|
||||||
|
### Issue Type
|
||||||
|
- [ ] Authentication failure (401/403)
|
||||||
|
- [ ] Connection refused / timeout
|
||||||
|
- [ ] TLS / SSL certificate error
|
||||||
|
- [ ] Wrong connection used (wrong environment)
|
||||||
|
- [ ] Config file not found / parse error
|
||||||
|
- [ ] API response error (4xx / 5xx)
|
||||||
|
|
||||||
|
### MCP Server
|
||||||
|
- **Server Name**: [e.g., mcp_mokowaas]
|
||||||
|
- **Server Version**: [e.g., 1.0.0]
|
||||||
|
- **Node.js Version**: [e.g., 20.x]
|
||||||
|
|
||||||
|
### Connection Details
|
||||||
|
- **Connection Name**: [e.g., production, staging, default]
|
||||||
|
- **API Base URL**: [e.g., https://api.example.com] *(do not include API keys)*
|
||||||
|
- **Insecure Mode**: [Yes / No]
|
||||||
|
|
||||||
|
### Error Message
|
||||||
|
```
|
||||||
|
Paste the exact error message here
|
||||||
|
```
|
||||||
|
|
||||||
|
### Steps to Reproduce
|
||||||
|
1. Configure connection with `npm run setup`
|
||||||
|
2. Call tool `...` with parameters `...`
|
||||||
|
3. See error
|
||||||
|
|
||||||
|
### Expected Behavior
|
||||||
|
What should have happened.
|
||||||
|
|
||||||
|
### Debugging Attempted
|
||||||
|
- [ ] Tested API directly with curl
|
||||||
|
- [ ] Verified API key is valid
|
||||||
|
- [ ] Checked config file exists and is valid JSON
|
||||||
|
- [ ] Tested with `list_connections` tool
|
||||||
|
- [ ] Ran server manually: `node dist/index.js 2> debug.log`
|
||||||
|
|
||||||
|
### Config File
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"defaultConnection": "...",
|
||||||
|
"connections": {
|
||||||
|
"connection_name": {
|
||||||
|
"baseUrl": "https://...",
|
||||||
|
"apiKey": "REDACTED"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
*(Redact all API keys and tokens)*
|
||||||
|
|
||||||
|
### Environment
|
||||||
|
- **OS**: [e.g., macOS 14, Ubuntu 22.04, Windows 11]
|
||||||
|
- **Claude Code Version**: [e.g., latest]
|
||||||
|
- **Registration**: [.mcp.json / ~/.claude.json]
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
---
|
||||||
|
name: New MCP Tool Request
|
||||||
|
about: Request a new tool to be added to this MCP server
|
||||||
|
title: '[TOOL] '
|
||||||
|
labels: 'enhancement, mcp-tool'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tool Request
|
||||||
|
|
||||||
|
### Tool Name
|
||||||
|
Proposed tool name (snake_case): `resource_action`
|
||||||
|
|
||||||
|
### Description
|
||||||
|
What should this tool do? What API endpoint(s) does it map to?
|
||||||
|
|
||||||
|
### API Endpoint(s)
|
||||||
|
- **Method**: [GET / POST / PUT / PATCH / DELETE]
|
||||||
|
- **Endpoint**: `/api/v1/...`
|
||||||
|
- **Auth**: [API Key / Token / None]
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `id` | number | Yes | Resource ID |
|
||||||
|
| `search` | string | No | Search filter |
|
||||||
|
|
||||||
|
### Expected Response
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Example"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Use Case
|
||||||
|
Describe when and why someone would use this tool from Claude or another AI assistant.
|
||||||
|
|
||||||
|
### Connection Scope
|
||||||
|
- [ ] Works with all connections
|
||||||
|
- [ ] Specific to certain API versions
|
||||||
|
- [ ] Requires additional permissions
|
||||||
|
|
||||||
|
### Checklist
|
||||||
|
- [ ] I have checked this tool does not already exist
|
||||||
|
- [ ] I have verified the API endpoint exists and is documented
|
||||||
|
- [ ] The proposed name follows the `resource_action` convention
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
name: Manual Deploy
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
environment:
|
||||||
|
description: 'Target environment'
|
||||||
|
required: true
|
||||||
|
type: choice
|
||||||
|
options: [dev, demo, production]
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
name: Deploy
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Deploy
|
||||||
|
run: echo "Deploy to ${{ inputs.environment }} — configure in deploy-mcp"
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: MokoStandards.Security
|
||||||
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
|
||||||
|
# PATH: /templates/workflows/gitleaks.yml.template
|
||||||
|
# VERSION: 01.00.00
|
||||||
|
# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens
|
||||||
|
#
|
||||||
|
# +========================================================================+
|
||||||
|
# | SECRET SCANNING |
|
||||||
|
# +========================================================================+
|
||||||
|
# | |
|
||||||
|
# | Scans commits for leaked secrets using Gitleaks. |
|
||||||
|
# | |
|
||||||
|
# | - PR scan: only new commits in the PR |
|
||||||
|
# | - Scheduled: full repo scan weekly |
|
||||||
|
# | - Alerts via ntfy on findings |
|
||||||
|
# | |
|
||||||
|
# +========================================================================+
|
||||||
|
|
||||||
|
name: Secret Scanning
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- 'dev/**'
|
||||||
|
schedule:
|
||||||
|
- cron: '0 5 * * 1' # Weekly Monday 05:00 UTC
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
env:
|
||||||
|
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
|
||||||
|
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-security' }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
gitleaks:
|
||||||
|
name: Gitleaks Secret Scan
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Install Gitleaks
|
||||||
|
run: |
|
||||||
|
GITLEAKS_VERSION="8.21.2"
|
||||||
|
curl -sSL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \
|
||||||
|
| tar -xz -C /usr/local/bin gitleaks
|
||||||
|
gitleaks version
|
||||||
|
|
||||||
|
- name: Scan for secrets
|
||||||
|
id: scan
|
||||||
|
run: |
|
||||||
|
echo "### Secret Scanning" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ARGS="--source . --verbose --report-format json --report-path /tmp/gitleaks-report.json"
|
||||||
|
|
||||||
|
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
||||||
|
# Scan only PR commits
|
||||||
|
ARGS="$ARGS --log-opts=${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}"
|
||||||
|
echo "Scanning PR commits only" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "Full repository scan" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
if gitleaks detect $ARGS 2>&1; then
|
||||||
|
echo "result=clean" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "**No secrets detected.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "result=found" >> "$GITHUB_OUTPUT"
|
||||||
|
FINDINGS=$(jq length /tmp/gitleaks-report.json 2>/dev/null || echo "unknown")
|
||||||
|
echo "**${FINDINGS} potential secret(s) detected.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Review the findings and rotate any exposed credentials immediately." >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Notify on findings
|
||||||
|
if: failure() && steps.scan.outputs.result == 'found'
|
||||||
|
run: |
|
||||||
|
REPO="${{ github.event.repository.name }}"
|
||||||
|
curl -sS \
|
||||||
|
-H "Title: ${REPO} — secrets detected in code" \
|
||||||
|
-H "Tags: rotating_light,key" \
|
||||||
|
-H "Priority: urgent" \
|
||||||
|
-d "Gitleaks found potential secrets. Review and rotate credentials immediately." \
|
||||||
|
"${NTFY_URL}/${NTFY_TOPIC}" || true
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# Enforces branch merge policy:
|
||||||
|
# feature/* → dev only
|
||||||
|
# fix/* → dev only
|
||||||
|
# hotfix/* → dev or main (emergency)
|
||||||
|
# dev → main only
|
||||||
|
# alpha/* → dev only
|
||||||
|
# beta/* → dev only
|
||||||
|
# rc/* → main only
|
||||||
|
|
||||||
|
name: Branch Policy Check
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize, reopened, edited]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-target:
|
||||||
|
name: Verify merge target
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check branch policy
|
||||||
|
run: |
|
||||||
|
HEAD="${{ github.head_ref }}"
|
||||||
|
BASE="${{ github.base_ref }}"
|
||||||
|
|
||||||
|
echo "PR: ${HEAD} → ${BASE}"
|
||||||
|
|
||||||
|
ALLOWED=true
|
||||||
|
REASON=""
|
||||||
|
|
||||||
|
case "$HEAD" in
|
||||||
|
feature/*|feat/*)
|
||||||
|
if [ "$BASE" != "dev" ]; then
|
||||||
|
ALLOWED=false
|
||||||
|
REASON="Feature branches must target 'dev', not '${BASE}'"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
fix/*|bugfix/*)
|
||||||
|
if [ "$BASE" != "dev" ]; then
|
||||||
|
ALLOWED=false
|
||||||
|
REASON="Fix branches must target 'dev', not '${BASE}'"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
hotfix/*)
|
||||||
|
if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
|
||||||
|
ALLOWED=false
|
||||||
|
REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
alpha/*|beta/*)
|
||||||
|
if [ "$BASE" != "dev" ]; then
|
||||||
|
ALLOWED=false
|
||||||
|
REASON="Pre-release branches must target 'dev', not '${BASE}'"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
rc/*)
|
||||||
|
if [ "$BASE" != "main" ]; then
|
||||||
|
ALLOWED=false
|
||||||
|
REASON="Release candidate branches must target 'main', not '${BASE}'"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
dev)
|
||||||
|
if [ "$BASE" != "main" ]; then
|
||||||
|
ALLOWED=false
|
||||||
|
REASON="Dev branch can only merge into 'main', not '${BASE}'"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ "$ALLOWED" = false ]; then
|
||||||
|
echo "::error::${REASON}"
|
||||||
|
echo ""
|
||||||
|
echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "${REASON}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Branch policy: OK (${HEAD} → ${BASE})"
|
||||||
|
echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
|
||||||
@@ -0,0 +1,765 @@
|
|||||||
|
# ============================================================================
|
||||||
|
# Copyright (C) 2025 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: Gitea.Workflow
|
||||||
|
# INGROUP: MokoStandards.Validation
|
||||||
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
|
||||||
|
# PATH: /templates/workflows/joomla/repo_health.yml.template
|
||||||
|
# VERSION: 04.06.00
|
||||||
|
# BRIEF: Enforces repository guardrails by validating release configuration, scripts governance, tooling availability, and core repository health artifacts.
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
name: Repo Health
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: repo-health-${{ github.repository }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
profile:
|
||||||
|
description: 'Validation profile: all, release, scripts, or repo'
|
||||||
|
required: true
|
||||||
|
default: all
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- all
|
||||||
|
- release
|
||||||
|
- scripts
|
||||||
|
- repo
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
env:
|
||||||
|
# Release policy - Repository Variables Only
|
||||||
|
RELEASE_REQUIRED_REPO_VARS: RS_FTP_PATH_SUFFIX
|
||||||
|
RELEASE_OPTIONAL_REPO_VARS: DEV_FTP_SUFFIX
|
||||||
|
|
||||||
|
# Scripts governance policy
|
||||||
|
SCRIPTS_REQUIRED_DIRS:
|
||||||
|
SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate
|
||||||
|
|
||||||
|
# Repo health policy
|
||||||
|
REPO_REQUIRED_ARTIFACTS: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,.gitea/workflows/
|
||||||
|
REPO_OPTIONAL_FILES: SECURITY.md,GOVERNANCE.md,.editorconfig,.gitattributes,.gitignore,README.md
|
||||||
|
REPO_DISALLOWED_DIRS:
|
||||||
|
REPO_DISALLOWED_FILES: TODO.md,todo.md
|
||||||
|
|
||||||
|
# Extended checks toggles
|
||||||
|
EXTENDED_CHECKS: "true"
|
||||||
|
|
||||||
|
# File / directory variables
|
||||||
|
DOCS_INDEX: ""
|
||||||
|
SCRIPT_DIR: scripts
|
||||||
|
WORKFLOWS_DIR: .gitea/workflows
|
||||||
|
SHELLCHECK_PATTERN: '*.sh'
|
||||||
|
SPDX_FILE_GLOBS: '*.sh,*.php,*.js,*.ts,*.css,*.xml,*.yml,*.yaml'
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
access_check:
|
||||||
|
name: Access control
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 10
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
allowed: ${{ steps.perm.outputs.allowed }}
|
||||||
|
permission: ${{ steps.perm.outputs.permission }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check actor permission (admin only)
|
||||||
|
id: perm
|
||||||
|
env:
|
||||||
|
TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
ACTOR: ${{ github.actor }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
ALLOWED=false
|
||||||
|
PERMISSION=unknown
|
||||||
|
METHOD=""
|
||||||
|
|
||||||
|
# Hardcoded authorized users — always allowed
|
||||||
|
case "$ACTOR" in
|
||||||
|
jmiller|gitea-actions[bot])
|
||||||
|
ALLOWED=true
|
||||||
|
PERMISSION=admin
|
||||||
|
METHOD="hardcoded allowlist"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
# Detect platform and check permissions via API
|
||||||
|
API_BASE="${GITHUB_API_URL:-${GITEA_API_URL:-https://api.github.com}}"
|
||||||
|
RESP=$(curl -sf -H "Authorization: token ${TOKEN}" \
|
||||||
|
"${API_BASE}/repos/${REPO}/collaborators/${ACTOR}/permission" 2>/dev/null || echo '{}')
|
||||||
|
PERMISSION=$(echo "$RESP" | grep -oP '"permission"\s*:\s*"\K[^"]+' || echo "unknown")
|
||||||
|
if [ "$PERMISSION" = "admin" ] || [ "$PERMISSION" = "maintain" ] || [ "$PERMISSION" = "owner" ]; then
|
||||||
|
ALLOWED=true
|
||||||
|
fi
|
||||||
|
METHOD="collaborator API"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo "permission=${PERMISSION}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "allowed=${ALLOWED}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "## Access Authorization"
|
||||||
|
echo ""
|
||||||
|
echo "| Field | Value |"
|
||||||
|
echo "|-------|-------|"
|
||||||
|
echo "| **Actor** | \`${ACTOR}\` |"
|
||||||
|
echo "| **Repository** | \`${REPO}\` |"
|
||||||
|
echo "| **Permission** | \`${PERMISSION}\` |"
|
||||||
|
echo "| **Method** | ${METHOD} |"
|
||||||
|
echo "| **Authorized** | ${ALLOWED} |"
|
||||||
|
echo ""
|
||||||
|
if [ "$ALLOWED" = "true" ]; then
|
||||||
|
echo "${ACTOR} authorized (${METHOD})"
|
||||||
|
else
|
||||||
|
echo "${ACTOR} is NOT authorized. Requires admin or maintain role."
|
||||||
|
fi
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
|
- name: Deny execution when not permitted
|
||||||
|
if: ${{ steps.perm.outputs.allowed != 'true' }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
printf '%s\n' 'ERROR: Access denied. Admin permission required.' >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
release_config:
|
||||||
|
name: Release configuration
|
||||||
|
needs: access_check
|
||||||
|
if: ${{ needs.access_check.outputs.allowed == 'true' }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 20
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Guardrails release vars
|
||||||
|
env:
|
||||||
|
PROFILE_RAW: ${{ github.event.inputs.profile }}
|
||||||
|
RS_FTP_PATH_SUFFIX: ${{ vars.RS_FTP_PATH_SUFFIX }}
|
||||||
|
DEV_FTP_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
profile="${PROFILE_RAW:-all}"
|
||||||
|
case "${profile}" in
|
||||||
|
all|release|scripts|repo) ;;
|
||||||
|
*)
|
||||||
|
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ "${profile}" = 'scripts' ] || [ "${profile}" = 'repo' ]; then
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Release configuration (Repository Variables)'
|
||||||
|
printf '%s\n' "Profile: ${profile}"
|
||||||
|
printf '%s\n' 'Status: SKIPPED'
|
||||||
|
printf '%s\n' 'Reason: profile excludes release validation'
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
IFS=',' read -r -a required <<< "${RELEASE_REQUIRED_REPO_VARS}"
|
||||||
|
IFS=',' read -r -a optional <<< "${RELEASE_OPTIONAL_REPO_VARS}"
|
||||||
|
|
||||||
|
missing=()
|
||||||
|
missing_optional=()
|
||||||
|
|
||||||
|
for k in "${required[@]}"; do
|
||||||
|
v="${!k:-}"
|
||||||
|
[ -z "${v}" ] && missing+=("${k}")
|
||||||
|
done
|
||||||
|
|
||||||
|
for k in "${optional[@]}"; do
|
||||||
|
v="${!k:-}"
|
||||||
|
[ -z "${v}" ] && missing_optional+=("${k}")
|
||||||
|
done
|
||||||
|
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Release configuration (Repository Variables)'
|
||||||
|
printf '%s\n' "Profile: ${profile}"
|
||||||
|
printf '%s\n' '| Variable | Status |'
|
||||||
|
printf '%s\n' '|---|---|'
|
||||||
|
printf '%s\n' "| RS_FTP_PATH_SUFFIX | ${RS_FTP_PATH_SUFFIX:-NOT SET} |"
|
||||||
|
printf '%s\n' "| DEV_FTP_SUFFIX | ${DEV_FTP_SUFFIX:-NOT SET} |"
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
|
if [ "${#missing_optional[@]}" -gt 0 ]; then
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Missing optional repository variables'
|
||||||
|
for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${#missing[@]}" -gt 0 ]; then
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Missing required repository variables'
|
||||||
|
for m in "${missing[@]}"; do printf '%s\n' "- ${m}"; done
|
||||||
|
printf '%s\n' 'ERROR: Guardrails failed. Missing required repository variables.'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Repository variables validation result'
|
||||||
|
printf '%s\n' 'Status: OK'
|
||||||
|
printf '%s\n' 'All required repository variables present.'
|
||||||
|
printf '%s\n' ''
|
||||||
|
printf '%s\n' '**Note**: Organization secrets (RS_FTP_HOST, RS_FTP_USER, etc.) are validated at deployment time, not in repository health checks.'
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
|
scripts_governance:
|
||||||
|
name: Scripts governance
|
||||||
|
needs: access_check
|
||||||
|
if: ${{ needs.access_check.outputs.allowed == 'true' }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Scripts folder checks
|
||||||
|
env:
|
||||||
|
PROFILE_RAW: ${{ github.event.inputs.profile }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
profile="${PROFILE_RAW:-all}"
|
||||||
|
case "${profile}" in
|
||||||
|
all|release|scripts|repo) ;;
|
||||||
|
*)
|
||||||
|
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ "${profile}" = 'release' ] || [ "${profile}" = 'repo' ]; then
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Scripts governance'
|
||||||
|
printf '%s\n' "Profile: ${profile}"
|
||||||
|
printf '%s\n' 'Status: SKIPPED'
|
||||||
|
printf '%s\n' 'Reason: profile excludes scripts governance'
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d "${SCRIPT_DIR}" ]; then
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Scripts governance'
|
||||||
|
printf '%s\n' 'Status: OK (advisory)'
|
||||||
|
printf '%s\n' 'scripts/ directory not present. No scripts governance enforced.'
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
IFS=',' read -r -a required_dirs <<< "${SCRIPTS_REQUIRED_DIRS}"
|
||||||
|
IFS=',' read -r -a allowed_dirs <<< "${SCRIPTS_ALLOWED_DIRS}"
|
||||||
|
|
||||||
|
missing_dirs=()
|
||||||
|
unapproved_dirs=()
|
||||||
|
|
||||||
|
for d in "${required_dirs[@]}"; do
|
||||||
|
req="${d%/}"
|
||||||
|
[ ! -d "${req}" ] && missing_dirs+=("${req}/")
|
||||||
|
done
|
||||||
|
|
||||||
|
while IFS= read -r d; do
|
||||||
|
allowed=false
|
||||||
|
for a in "${allowed_dirs[@]}"; do
|
||||||
|
a_norm="${a%/}"
|
||||||
|
[ "${d%/}" = "${a_norm}" ] && allowed=true
|
||||||
|
done
|
||||||
|
[ "${allowed}" = false ] && unapproved_dirs+=("${d%/}/")
|
||||||
|
done < <(find "${SCRIPT_DIR}" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | sed 's#^\./##')
|
||||||
|
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Scripts governance'
|
||||||
|
printf '%s\n' "Profile: ${profile}"
|
||||||
|
printf '%s\n' '| Area | Status | Notes |'
|
||||||
|
printf '%s\n' '|---|---|---|'
|
||||||
|
|
||||||
|
if [ "${#missing_dirs[@]}" -gt 0 ]; then
|
||||||
|
printf '%s\n' '| Required directories | Warning | Missing required subfolders |'
|
||||||
|
else
|
||||||
|
printf '%s\n' '| Required directories | OK | All required subfolders present |'
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${#unapproved_dirs[@]}" -gt 0 ]; then
|
||||||
|
printf '%s\n' '| Directory policy | Warning | Unapproved directories detected |'
|
||||||
|
else
|
||||||
|
printf '%s\n' '| Directory policy | OK | No unapproved directories |'
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' '| Enforcement mode | Advisory | scripts folder is optional |'
|
||||||
|
printf '\n'
|
||||||
|
|
||||||
|
if [ "${#missing_dirs[@]}" -gt 0 ]; then
|
||||||
|
printf '%s\n' 'Missing required script directories:'
|
||||||
|
for m in "${missing_dirs[@]}"; do printf '%s\n' "- ${m}"; done
|
||||||
|
printf '\n'
|
||||||
|
else
|
||||||
|
printf '%s\n' 'Missing required script directories: none.'
|
||||||
|
printf '\n'
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${#unapproved_dirs[@]}" -gt 0 ]; then
|
||||||
|
printf '%s\n' 'Unapproved script directories detected:'
|
||||||
|
for m in "${unapproved_dirs[@]}"; do printf '%s\n' "- ${m}"; done
|
||||||
|
printf '\n'
|
||||||
|
else
|
||||||
|
printf '%s\n' 'Unapproved script directories detected: none.'
|
||||||
|
printf '\n'
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' 'Scripts governance completed in advisory mode.'
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
|
repo_health:
|
||||||
|
name: Repository health
|
||||||
|
needs: access_check
|
||||||
|
if: ${{ needs.access_check.outputs.allowed == 'true' }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 20
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Repository health checks
|
||||||
|
env:
|
||||||
|
PROFILE_RAW: ${{ github.event.inputs.profile }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
profile="${PROFILE_RAW:-all}"
|
||||||
|
case "${profile}" in
|
||||||
|
all|release|scripts|repo) ;;
|
||||||
|
*)
|
||||||
|
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ "${profile}" = 'release' ] || [ "${profile}" = 'scripts' ]; then
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Repository health'
|
||||||
|
printf '%s\n' "Profile: ${profile}"
|
||||||
|
printf '%s\n' 'Status: SKIPPED'
|
||||||
|
printf '%s\n' 'Reason: profile excludes repository health'
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Source directory: src/ or htdocs/ (either is valid)
|
||||||
|
if [ -d "src" ]; then
|
||||||
|
SOURCE_DIR="src"
|
||||||
|
elif [ -d "htdocs" ]; then
|
||||||
|
SOURCE_DIR="htdocs"
|
||||||
|
else
|
||||||
|
missing_required+=("src/ or htdocs/ (source directory required)")
|
||||||
|
fi
|
||||||
|
|
||||||
|
IFS=',' read -r -a required_artifacts <<< "${REPO_REQUIRED_ARTIFACTS}"
|
||||||
|
IFS=',' read -r -a optional_files <<< "${REPO_OPTIONAL_FILES}"
|
||||||
|
IFS=',' read -r -a disallowed_dirs <<< "${REPO_DISALLOWED_DIRS}"
|
||||||
|
IFS=',' read -r -a disallowed_files <<< "${REPO_DISALLOWED_FILES}"
|
||||||
|
|
||||||
|
missing_required=()
|
||||||
|
missing_optional=()
|
||||||
|
|
||||||
|
for item in "${required_artifacts[@]}"; do
|
||||||
|
if printf '%s' "${item}" | grep -q '/$'; then
|
||||||
|
d="${item%/}"
|
||||||
|
[ ! -d "${d}" ] && missing_required+=("${item}")
|
||||||
|
else
|
||||||
|
[ ! -f "${item}" ] && missing_required+=("${item}")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
for f in "${optional_files[@]}"; do
|
||||||
|
if printf '%s' "${f}" | grep -q '/$'; then
|
||||||
|
d="${f%/}"
|
||||||
|
[ ! -d "${d}" ] && missing_optional+=("${f}")
|
||||||
|
else
|
||||||
|
[ ! -f "${f}" ] && missing_optional+=("${f}")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
for d in "${disallowed_dirs[@]}"; do
|
||||||
|
d_norm="${d%/}"
|
||||||
|
[ -d "${d_norm}" ] && missing_required+=("${d_norm}/ (disallowed)")
|
||||||
|
done
|
||||||
|
|
||||||
|
for f in "${disallowed_files[@]}"; do
|
||||||
|
[ -f "${f}" ] && missing_required+=("${f} (disallowed)")
|
||||||
|
done
|
||||||
|
|
||||||
|
git fetch origin --prune
|
||||||
|
|
||||||
|
dev_paths=()
|
||||||
|
dev_branches=()
|
||||||
|
|
||||||
|
while IFS= read -r b; do
|
||||||
|
name="${b#origin/}"
|
||||||
|
if [ "${name}" = 'dev' ]; then
|
||||||
|
dev_branches+=("${name}")
|
||||||
|
else
|
||||||
|
dev_paths+=("${name}")
|
||||||
|
fi
|
||||||
|
done < <(git branch -r --list 'origin/dev*' | sed 's/^ *//')
|
||||||
|
|
||||||
|
if [ "${#dev_paths[@]}" -eq 0 ] && [ "${#dev_branches[@]}" -eq 0 ]; then
|
||||||
|
missing_required+=("dev branch")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${#dev_branches[@]}" -gt 0 ]; then
|
||||||
|
fi
|
||||||
|
|
||||||
|
content_warnings=()
|
||||||
|
|
||||||
|
if [ -f 'CHANGELOG.md' ] && ! grep -Eq '^# Changelog' CHANGELOG.md; then
|
||||||
|
content_warnings+=("CHANGELOG.md missing '# Changelog' header")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f 'CHANGELOG.md' ] && grep -Eq '^[# ]*Unreleased' CHANGELOG.md; then
|
||||||
|
content_warnings+=("CHANGELOG.md contains Unreleased section (review release readiness)")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f 'LICENSE' ] && ! grep -qiE 'GNU GENERAL PUBLIC LICENSE|GPL' LICENSE; then
|
||||||
|
content_warnings+=("LICENSE does not look like a GPL text")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f 'README.md' ] && ! grep -qiE 'moko|Moko' README.md; then
|
||||||
|
content_warnings+=("README.md missing expected brand keyword")
|
||||||
|
fi
|
||||||
|
|
||||||
|
export PROFILE_RAW="${profile}"
|
||||||
|
export MISSING_REQUIRED="$(printf '%s\n' "${missing_required[@]:-}")"
|
||||||
|
export MISSING_OPTIONAL="$(printf '%s\n' "${missing_optional[@]:-}")"
|
||||||
|
export CONTENT_WARNINGS="$(printf '%s\n' "${content_warnings[@]:-}")"
|
||||||
|
|
||||||
|
report_json="$(python3 - <<'PY'
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
profile = os.environ.get('PROFILE_RAW') or 'all'
|
||||||
|
|
||||||
|
missing_required = os.environ.get('MISSING_REQUIRED', '').splitlines() if os.environ.get('MISSING_REQUIRED') else []
|
||||||
|
missing_optional = os.environ.get('MISSING_OPTIONAL', '').splitlines() if os.environ.get('MISSING_OPTIONAL') else []
|
||||||
|
content_warnings = os.environ.get('CONTENT_WARNINGS', '').splitlines() if os.environ.get('CONTENT_WARNINGS') else []
|
||||||
|
|
||||||
|
out = {
|
||||||
|
'profile': profile,
|
||||||
|
'missing_required': [x for x in missing_required if x],
|
||||||
|
'missing_optional': [x for x in missing_optional if x],
|
||||||
|
'content_warnings': [x for x in content_warnings if x],
|
||||||
|
}
|
||||||
|
|
||||||
|
print(json.dumps(out, indent=2))
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Repository health'
|
||||||
|
printf '%s\n' "Profile: ${profile}"
|
||||||
|
printf '%s\n' '| Metric | Value |'
|
||||||
|
printf '%s\n' '|---|---|'
|
||||||
|
printf '%s\n' "| Missing required | ${#missing_required[@]} |"
|
||||||
|
printf '%s\n' "| Missing optional | ${#missing_optional[@]} |"
|
||||||
|
printf '%s\n' "| Content warnings | ${#content_warnings[@]} |"
|
||||||
|
printf '\n'
|
||||||
|
|
||||||
|
printf '%s\n' '### Guardrails report (JSON)'
|
||||||
|
printf '%s\n' '```json'
|
||||||
|
printf '%s\n' "${report_json}"
|
||||||
|
printf '%s\n' '```'
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
|
if [ "${#missing_required[@]}" -gt 0 ]; then
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Missing required repo artifacts'
|
||||||
|
for m in "${missing_required[@]}"; do printf '%s\n' "- ${m}"; done
|
||||||
|
printf '%s\n' 'ERROR: Guardrails failed. Missing required repository artifacts.'
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${#missing_optional[@]}" -gt 0 ]; then
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Missing optional repo artifacts'
|
||||||
|
for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${#content_warnings[@]}" -gt 0 ]; then
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Repo content warnings'
|
||||||
|
for m in "${content_warnings[@]}"; do printf '%s\n' "- ${m}"; done
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -- Joomla-specific checks --
|
||||||
|
joomla_findings=()
|
||||||
|
|
||||||
|
MANIFEST="$(find . -maxdepth 2 -name '*.xml' -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true)"
|
||||||
|
if [ -z "${MANIFEST}" ]; then
|
||||||
|
joomla_findings+=("Joomla XML manifest not found (no *.xml with <extension> tag)")
|
||||||
|
else
|
||||||
|
if ! grep -qP '<version>' "${MANIFEST}"; then
|
||||||
|
joomla_findings+=("XML manifest: <version> tag missing")
|
||||||
|
fi
|
||||||
|
if ! grep -qP 'type="(component|module|plugin|library|package|template|language)"' "${MANIFEST}"; then
|
||||||
|
joomla_findings+=("XML manifest: type attribute missing or invalid")
|
||||||
|
fi
|
||||||
|
if ! grep -qP '<name>' "${MANIFEST}"; then
|
||||||
|
joomla_findings+=("XML manifest: <name> tag missing")
|
||||||
|
fi
|
||||||
|
if ! grep -qP '<author>' "${MANIFEST}"; then
|
||||||
|
joomla_findings+=("XML manifest: <author> tag missing")
|
||||||
|
fi
|
||||||
|
if ! grep -qP '<namespace' "${MANIFEST}"; then
|
||||||
|
joomla_findings+=("XML manifest: <namespace> missing (required for Joomla 5+)")
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
INI_COUNT="$(find . -name '*.ini' -type f 2>/dev/null | wc -l)"
|
||||||
|
if [ "${INI_COUNT}" -eq 0 ]; then
|
||||||
|
joomla_findings+=("No .ini language files found")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f 'updates.xml' ]; then
|
||||||
|
joomla_findings+=("updates.xml missing in root (required for Joomla update server)")
|
||||||
|
fi
|
||||||
|
|
||||||
|
INDEX_DIRS=("${SOURCE_DIR}" "${SOURCE_DIR}/admin" "${SOURCE_DIR}/site")
|
||||||
|
for dir in "${INDEX_DIRS[@]}"; do
|
||||||
|
if [ -d "${dir}" ] && [ ! -f "${dir}/index.html" ]; then
|
||||||
|
joomla_findings+=("${dir}/index.html missing (directory listing protection)")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "${#joomla_findings[@]}" -gt 0 ]; then
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Joomla extension checks'
|
||||||
|
printf '%s\n' '| Check | Status |'
|
||||||
|
printf '%s\n' '|---|---|'
|
||||||
|
for f in "${joomla_findings[@]}"; do
|
||||||
|
printf '%s\n' "| ${f} | Warning |"
|
||||||
|
done
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
else
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Joomla extension checks'
|
||||||
|
printf '%s\n' 'All Joomla-specific checks passed.'
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
extended_enabled="${EXTENDED_CHECKS:-true}"
|
||||||
|
extended_findings=()
|
||||||
|
|
||||||
|
if [ "${extended_enabled}" = 'true' ]; then
|
||||||
|
if [ -f '.github/CODEOWNERS' ] || [ -f 'CODEOWNERS' ] || [ -f 'docs/CODEOWNERS' ]; then
|
||||||
|
:
|
||||||
|
else
|
||||||
|
extended_findings+=("CODEOWNERS not found (.github/CODEOWNERS preferred)")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ls "${WORKFLOWS_DIR}"/*.yml >/dev/null 2>&1 || ls "${WORKFLOWS_DIR}"/*.yaml >/dev/null 2>&1; then
|
||||||
|
bad_refs="$(grep -RIn --include='*.yml' --include='*.yaml' -E '^[[:space:]]*uses:[[:space:]]*[^#]+@(main|master)\b' "${WORKFLOWS_DIR}" 2>/dev/null || true)"
|
||||||
|
if [ -n "${bad_refs}" ]; then
|
||||||
|
extended_findings+=("Workflows reference actions @main/@master (pin versions): see log excerpt")
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Workflow pinning advisory'
|
||||||
|
printf '%s\n' 'Found uses: entries pinned to main/master:'
|
||||||
|
printf '%s\n' '```'
|
||||||
|
printf '%s\n' "${bad_refs}"
|
||||||
|
printf '%s\n' '```'
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "${DOCS_INDEX}" ]; then
|
||||||
|
missing_links="$(python3 - <<'PY'
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
idx = os.environ.get('DOCS_INDEX', 'docs/docs-index.md')
|
||||||
|
base = os.getcwd()
|
||||||
|
|
||||||
|
bad = []
|
||||||
|
pat = re.compile(r'\[[^\]]+\]\(([^)]+)\)')
|
||||||
|
|
||||||
|
with open(idx, 'r', encoding='utf-8') as f:
|
||||||
|
for line in f:
|
||||||
|
for m in pat.findall(line):
|
||||||
|
link = m.strip()
|
||||||
|
if link.startswith('http://') or link.startswith('https://') or link.startswith('#') or link.startswith('mailto:'):
|
||||||
|
continue
|
||||||
|
if link.startswith('/'):
|
||||||
|
rel = link.lstrip('/')
|
||||||
|
else:
|
||||||
|
rel = os.path.normpath(os.path.join(os.path.dirname(idx), link))
|
||||||
|
rel = rel.split('#', 1)[0]
|
||||||
|
rel = rel.split('?', 1)[0]
|
||||||
|
if not rel:
|
||||||
|
continue
|
||||||
|
p = os.path.join(base, rel)
|
||||||
|
if not os.path.exists(p):
|
||||||
|
bad.append(rel)
|
||||||
|
|
||||||
|
print('\n'.join(sorted(set(bad))))
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
if [ -n "${missing_links}" ]; then
|
||||||
|
extended_findings+=("docs/docs-index.md contains broken relative links")
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Docs index link integrity'
|
||||||
|
printf '%s\n' 'Broken relative links:'
|
||||||
|
while IFS= read -r l; do [ -n "${l}" ] && printf '%s\n' "- ${l}"; done <<< "${missing_links}"
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -d "${SCRIPT_DIR}" ]; then
|
||||||
|
if ! command -v shellcheck >/dev/null 2>&1; then
|
||||||
|
sudo apt-get update -qq
|
||||||
|
sudo apt-get install -y shellcheck >/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
sc_out=''
|
||||||
|
while IFS= read -r shf; do
|
||||||
|
[ -z "${shf}" ] && continue
|
||||||
|
out_one="$(shellcheck -S warning -x "${shf}" 2>/dev/null || true)"
|
||||||
|
if [ -n "${out_one}" ]; then
|
||||||
|
sc_out="${sc_out}${out_one}\n"
|
||||||
|
fi
|
||||||
|
done < <(find "${SCRIPT_DIR}" -type f -name "${SHELLCHECK_PATTERN}" 2>/dev/null | sort)
|
||||||
|
|
||||||
|
if [ -n "${sc_out}" ]; then
|
||||||
|
extended_findings+=("ShellCheck warnings detected (advisory)")
|
||||||
|
sc_head="$(printf '%s' "${sc_out}" | head -n 200)"
|
||||||
|
{
|
||||||
|
printf '%s\n' '### ShellCheck (advisory)'
|
||||||
|
printf '%s\n' '```'
|
||||||
|
printf '%s\n' "${sc_head}"
|
||||||
|
printf '%s\n' '```'
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
spdx_missing=()
|
||||||
|
IFS=',' read -r -a spdx_globs <<< "${SPDX_FILE_GLOBS}"
|
||||||
|
spdx_args=()
|
||||||
|
for g in "${spdx_globs[@]}"; do spdx_args+=("${g}"); done
|
||||||
|
|
||||||
|
while IFS= read -r f; do
|
||||||
|
[ -z "${f}" ] && continue
|
||||||
|
if ! head -n 40 "${f}" | grep -q 'SPDX-License-Identifier:'; then
|
||||||
|
spdx_missing+=("${f}")
|
||||||
|
fi
|
||||||
|
done < <(git ls-files "${spdx_args[@]}" 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [ "${#spdx_missing[@]}" -gt 0 ]; then
|
||||||
|
extended_findings+=("SPDX header missing in some tracked files (advisory)")
|
||||||
|
{
|
||||||
|
printf '%s\n' '### SPDX header advisory'
|
||||||
|
printf '%s\n' 'Files missing SPDX-License-Identifier (first 40 lines scan):'
|
||||||
|
for f in "${spdx_missing[@]}"; do printf '%s\n' "- ${f}"; done
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
stale_cutoff_days=180
|
||||||
|
stale_branches="$(git for-each-ref --format='%(refname:short) %(committerdate:unix)' refs/remotes/origin 2>/dev/null | awk -v now="$(date +%s)" -v days="${stale_cutoff_days}" '{if (now-$2 > days*86400) print $1}' | head -50)"
|
||||||
|
if [ -n "${stale_branches}" ]; then
|
||||||
|
extended_findings+=("Stale remote branches detected (advisory)")
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Git hygiene advisory'
|
||||||
|
printf '%s\n' "Branches with last commit older than ${stale_cutoff_days} days (sample up to 50):"
|
||||||
|
while IFS= read -r b; do [ -n "${b}" ] && printf '%s\n' "- ${b}"; done <<< "${stale_branches}"
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Guardrails coverage matrix'
|
||||||
|
printf '%s\n' '| Domain | Status | Notes |'
|
||||||
|
printf '%s\n' '|---|---|---|'
|
||||||
|
printf '%s\n' '| Access control | OK | Admin-only execution gate |'
|
||||||
|
printf '%s\n' '| Release variables | OK | Repository variables validation |'
|
||||||
|
printf '%s\n' '| Scripts governance | OK | Directory policy and advisory reporting |'
|
||||||
|
printf '%s\n' '| Repo required artifacts | OK | Required, optional, disallowed enforcement |'
|
||||||
|
printf '%s\n' '| Repo content heuristics | OK | Brand, license, changelog structure |'
|
||||||
|
if [ "${extended_enabled}" = 'true' ]; then
|
||||||
|
if [ "${#extended_findings[@]}" -gt 0 ]; then
|
||||||
|
printf '%s\n' '| Extended checks | Warning | See extended findings below |'
|
||||||
|
else
|
||||||
|
printf '%s\n' '| Extended checks | OK | No findings |'
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
printf '%s\n' '| Extended checks | SKIPPED | EXTENDED_CHECKS disabled |'
|
||||||
|
fi
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
|
if [ "${extended_enabled}" = 'true' ] && [ "${#extended_findings[@]}" -gt 0 ]; then
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Extended findings (advisory)'
|
||||||
|
for f in "${extended_findings[@]}"; do printf '%s\n' "- ${f}"; done
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' 'Repository health guardrails passed.' >> "${GITHUB_STEP_SUMMARY}"
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## [1.0] — 2026-05-08
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Initial release
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Contributing
|
||||||
|
|
||||||
|
See [standards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki).
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# monitor-mcp
|
||||||
|
|
||||||
|
MCP server for server health monitoring, uptime, and log tailing
|
||||||
|
|
||||||
|
  
|
||||||
|
|
||||||
|
|
||||||
|
MCP server for infrastructure monitoring -- server health, Grafana dashboards, and site uptime checks.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Type** | MCP Server |
|
||||||
|
| **Language** | Node.js |
|
||||||
|
| **License** | GPL-3.0-or-later |
|
||||||
|
| **Config** | `~/.monitor-mcp.json` |
|
||||||
|
| **Platform** | [Gitea](https://git.mokoconsulting.tech/MokoConsulting/monitor-mcp) (primary) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
monitor-mcp provides MCP tools for monitoring server infrastructure and Grafana dashboards. It enables Claude Code to check server health, query metrics, inspect alerts, and verify site uptime.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Wiki Pages
|
||||||
|
|
||||||
|
### Guides
|
||||||
|
|
||||||
|
- [Grafana Integration](https://git.mokoconsulting.tech/MokoConsulting/monitor-mcp/wiki/Grafana-Integration) -- connecting to Grafana dashboards and alerts
|
||||||
|
- [Sites Monitoring](https://git.mokoconsulting.tech/MokoConsulting/monitor-mcp/wiki/Sites-Monitoring) -- uptime and site health monitoring
|
||||||
|
|
||||||
|
### Reference
|
||||||
|
|
||||||
|
- [Tools Reference](https://git.mokoconsulting.tech/MokoConsulting/monitor-mcp/wiki/Tools-Reference) -- full list of available MCP tools
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Wikis
|
||||||
|
|
||||||
|
| Repo | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| [ssh-mcp](https://git.mokoconsulting.tech/MokoConsulting/ssh-mcp/wiki) | SSH server management MCP |
|
||||||
|
| [deploy-mcp](https://git.mokoconsulting.tech/MokoConsulting/deploy-mcp/wiki) | Git-based deployment MCP |
|
||||||
|
| [backup-mcp](https://git.mokoconsulting.tech/MokoConsulting/backup-mcp/wiki) | Backup MCP with Akeeba integration |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **[MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki)** -- central standards hub for all Moko Consulting projects.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Full documentation is available on the [Wiki](https://git.mokoconsulting.tech/MokoConsulting/monitor-mcp/wiki).
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
See the wiki for development guidelines and contribution instructions.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the GNU General Public License v3.0 or later -- see the [LICENSE](LICENSE) file.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*[Moko Consulting](https://mokoconsulting.tech) -- [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Security
|
||||||
|
|
||||||
|
Report to hello@mokoconsulting.tech.
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"defaultConnection": "git",
|
||||||
|
"connections": {
|
||||||
|
"git": {
|
||||||
|
"host": "git.mokoconsulting.tech",
|
||||||
|
"port": 2918,
|
||||||
|
"username": "mokoconsulting",
|
||||||
|
"keyPath": "~/.ssh/id_ed25519"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.env
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Local config (contains secrets)
|
||||||
|
config.json
|
||||||
|
.env
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<mokoplatform schema-version="1.0">
|
||||||
|
<identity><name>monitor-mcp</name><org>MokoConsulting</org></identity>
|
||||||
|
<governance><standards-version>05.00.00</standards-version></governance>
|
||||||
|
</mokoplatform>
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# mcp_mokomonitor
|
||||||
|
|
||||||
|
MCP server for server health monitoring, uptime checks, Grafana dashboards, and log tailing.
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Package** | `@mokoconsulting/mcp-mokomonitor` |
|
||||||
|
| **Entry** | `dist/index.js` |
|
||||||
|
| **Config** | `~/.mcp_mokomonitor.json` (override: `MONITOR_MCP_CONFIG` env var) |
|
||||||
|
| **Language** | TypeScript |
|
||||||
|
| **Branch** | develop on `dev`, merge to `main` (protected) |
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install # Install dependencies
|
||||||
|
npm run build # Compile TypeScript → dist/
|
||||||
|
npm run dev # Watch mode
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── index.ts # MCP server entry, tool registration
|
||||||
|
├── config.ts # Loads ~/.mcp_mokomonitor.json, resolves connections + Grafana + sites.json
|
||||||
|
├── client.ts # HTTP health check client
|
||||||
|
├── grafana.ts # Grafana API integration (dashboards, panels, alerts)
|
||||||
|
└── types.ts # MonitorConfig, MonitorConnection, SitesConfig types
|
||||||
|
```
|
||||||
|
|
||||||
|
- Config defines **connections** for health checks + optional **Grafana** config
|
||||||
|
- Sites list at `A:/moko-platform/monitoring/sites.json` for bulk monitoring
|
||||||
|
- Grafana at bench.mokoconsulting.tech — WaaS dashboard for uptime/performance
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- **Never commit** `.claude/`, `.mcp.json`, `TODO.md`, `*.min.css`/`*.min.js`
|
||||||
|
- **Attribution**: `Authored-by: Moko Consulting`
|
||||||
|
- **Workflow directory**: `.mokogitea/` (not `.gitea/` or `.github/`)
|
||||||
|
- **Wiki**: documentation lives in the Gitea wiki, not `docs/` files
|
||||||
|
- **Standards**: [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)
|
||||||
+48
@@ -0,0 +1,48 @@
|
|||||||
|
---
|
||||||
|
name: API Integration Request
|
||||||
|
about: Request integration with a new REST API or service
|
||||||
|
title: '[API] '
|
||||||
|
labels: 'enhancement, api-integration'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API Integration Request
|
||||||
|
|
||||||
|
### Target API
|
||||||
|
- **Service Name**: [e.g., Akeeba Backup, Joomla Web Services]
|
||||||
|
- **API Documentation**: [URL to API docs]
|
||||||
|
- **API Type**: [REST / GraphQL / SOAP]
|
||||||
|
- **Authentication**: [API Key / OAuth / Bearer Token / Basic Auth]
|
||||||
|
|
||||||
|
### Proposed Tools
|
||||||
|
List the MCP tools this integration would provide:
|
||||||
|
|
||||||
|
| Tool Name | HTTP Method | Endpoint | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `service_list` | GET | `/api/items` | List all items |
|
||||||
|
| `service_get` | GET | `/api/items/{id}` | Get single item |
|
||||||
|
| `service_create` | POST | `/api/items` | Create item |
|
||||||
|
|
||||||
|
### Multi-Connection
|
||||||
|
- [ ] Single instance only
|
||||||
|
- [ ] Multiple instances (production, staging, dev)
|
||||||
|
- [ ] Multi-tenant (one connection per client)
|
||||||
|
|
||||||
|
### Use Case
|
||||||
|
Describe the workflow this integration enables for AI assistants.
|
||||||
|
|
||||||
|
### Priority
|
||||||
|
- [ ] Critical — blocking current work
|
||||||
|
- [ ] High — needed soon
|
||||||
|
- [ ] Medium — would improve workflow
|
||||||
|
- [ ] Low — nice to have
|
||||||
|
|
||||||
|
### Existing Alternatives
|
||||||
|
Are there other ways to accomplish this today? If so, why is an MCP integration better?
|
||||||
|
|
||||||
|
### Checklist
|
||||||
|
- [ ] API documentation is available and accessible
|
||||||
|
- [ ] API supports the required authentication method
|
||||||
|
- [ ] I have tested the API endpoints manually
|
||||||
|
- [ ] The integration follows the Template-MCP architecture pattern
|
||||||
+67
@@ -0,0 +1,67 @@
|
|||||||
|
---
|
||||||
|
name: MCP Connection Issue
|
||||||
|
about: Report a connection, authentication, or API communication issue
|
||||||
|
title: '[CONNECTION] '
|
||||||
|
labels: 'bug, mcp-connection'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Connection Issue
|
||||||
|
|
||||||
|
### Issue Type
|
||||||
|
- [ ] Authentication failure (401/403)
|
||||||
|
- [ ] Connection refused / timeout
|
||||||
|
- [ ] TLS / SSL certificate error
|
||||||
|
- [ ] Wrong connection used (wrong environment)
|
||||||
|
- [ ] Config file not found / parse error
|
||||||
|
- [ ] API response error (4xx / 5xx)
|
||||||
|
|
||||||
|
### MCP Server
|
||||||
|
- **Server Name**: [e.g., mcp_mokowaas]
|
||||||
|
- **Server Version**: [e.g., 1.0.0]
|
||||||
|
- **Node.js Version**: [e.g., 20.x]
|
||||||
|
|
||||||
|
### Connection Details
|
||||||
|
- **Connection Name**: [e.g., production, staging, default]
|
||||||
|
- **API Base URL**: [e.g., https://api.example.com] *(do not include API keys)*
|
||||||
|
- **Insecure Mode**: [Yes / No]
|
||||||
|
|
||||||
|
### Error Message
|
||||||
|
```
|
||||||
|
Paste the exact error message here
|
||||||
|
```
|
||||||
|
|
||||||
|
### Steps to Reproduce
|
||||||
|
1. Configure connection with `npm run setup`
|
||||||
|
2. Call tool `...` with parameters `...`
|
||||||
|
3. See error
|
||||||
|
|
||||||
|
### Expected Behavior
|
||||||
|
What should have happened.
|
||||||
|
|
||||||
|
### Debugging Attempted
|
||||||
|
- [ ] Tested API directly with curl
|
||||||
|
- [ ] Verified API key is valid
|
||||||
|
- [ ] Checked config file exists and is valid JSON
|
||||||
|
- [ ] Tested with `list_connections` tool
|
||||||
|
- [ ] Ran server manually: `node dist/index.js 2> debug.log`
|
||||||
|
|
||||||
|
### Config File
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"defaultConnection": "...",
|
||||||
|
"connections": {
|
||||||
|
"connection_name": {
|
||||||
|
"baseUrl": "https://...",
|
||||||
|
"apiKey": "REDACTED"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
*(Redact all API keys and tokens)*
|
||||||
|
|
||||||
|
### Environment
|
||||||
|
- **OS**: [e.g., macOS 14, Ubuntu 22.04, Windows 11]
|
||||||
|
- **Claude Code Version**: [e.g., latest]
|
||||||
|
- **Registration**: [.mcp.json / ~/.claude.json]
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
---
|
||||||
|
name: New MCP Tool Request
|
||||||
|
about: Request a new tool to be added to this MCP server
|
||||||
|
title: '[TOOL] '
|
||||||
|
labels: 'enhancement, mcp-tool'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tool Request
|
||||||
|
|
||||||
|
### Tool Name
|
||||||
|
Proposed tool name (snake_case): `resource_action`
|
||||||
|
|
||||||
|
### Description
|
||||||
|
What should this tool do? What API endpoint(s) does it map to?
|
||||||
|
|
||||||
|
### API Endpoint(s)
|
||||||
|
- **Method**: [GET / POST / PUT / PATCH / DELETE]
|
||||||
|
- **Endpoint**: `/api/v1/...`
|
||||||
|
- **Auth**: [API Key / Token / None]
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `id` | number | Yes | Resource ID |
|
||||||
|
| `search` | string | No | Search filter |
|
||||||
|
|
||||||
|
### Expected Response
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Example"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Use Case
|
||||||
|
Describe when and why someone would use this tool from Claude or another AI assistant.
|
||||||
|
|
||||||
|
### Connection Scope
|
||||||
|
- [ ] Works with all connections
|
||||||
|
- [ ] Specific to certain API versions
|
||||||
|
- [ ] Requires additional permissions
|
||||||
|
|
||||||
|
### Checklist
|
||||||
|
- [ ] I have checked this tool does not already exist
|
||||||
|
- [ ] I have verified the API endpoint exists and is documented
|
||||||
|
- [ ] The proposed name follows the `resource_action` convention
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
name: Manual Deploy
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
environment:
|
||||||
|
description: 'Target environment'
|
||||||
|
required: true
|
||||||
|
type: choice
|
||||||
|
options: [dev, demo, production]
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
name: Deploy
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Deploy
|
||||||
|
run: echo "Deploy to ${{ inputs.environment }} — configure in deploy-mcp"
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: MokoStandards.Security
|
||||||
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
|
||||||
|
# PATH: /templates/workflows/gitleaks.yml.template
|
||||||
|
# VERSION: 01.00.00
|
||||||
|
# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens
|
||||||
|
#
|
||||||
|
# +========================================================================+
|
||||||
|
# | SECRET SCANNING |
|
||||||
|
# +========================================================================+
|
||||||
|
# | |
|
||||||
|
# | Scans commits for leaked secrets using Gitleaks. |
|
||||||
|
# | |
|
||||||
|
# | - PR scan: only new commits in the PR |
|
||||||
|
# | - Scheduled: full repo scan weekly |
|
||||||
|
# | - Alerts via ntfy on findings |
|
||||||
|
# | |
|
||||||
|
# +========================================================================+
|
||||||
|
|
||||||
|
name: Secret Scanning
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- 'dev/**'
|
||||||
|
schedule:
|
||||||
|
- cron: '0 5 * * 1' # Weekly Monday 05:00 UTC
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
env:
|
||||||
|
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
|
||||||
|
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-security' }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
gitleaks:
|
||||||
|
name: Gitleaks Secret Scan
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Install Gitleaks
|
||||||
|
run: |
|
||||||
|
GITLEAKS_VERSION="8.21.2"
|
||||||
|
curl -sSL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \
|
||||||
|
| tar -xz -C /usr/local/bin gitleaks
|
||||||
|
gitleaks version
|
||||||
|
|
||||||
|
- name: Scan for secrets
|
||||||
|
id: scan
|
||||||
|
run: |
|
||||||
|
echo "### Secret Scanning" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ARGS="--source . --verbose --report-format json --report-path /tmp/gitleaks-report.json"
|
||||||
|
|
||||||
|
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
||||||
|
# Scan only PR commits
|
||||||
|
ARGS="$ARGS --log-opts=${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}"
|
||||||
|
echo "Scanning PR commits only" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "Full repository scan" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
if gitleaks detect $ARGS 2>&1; then
|
||||||
|
echo "result=clean" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "**No secrets detected.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "result=found" >> "$GITHUB_OUTPUT"
|
||||||
|
FINDINGS=$(jq length /tmp/gitleaks-report.json 2>/dev/null || echo "unknown")
|
||||||
|
echo "**${FINDINGS} potential secret(s) detected.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Review the findings and rotate any exposed credentials immediately." >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Notify on findings
|
||||||
|
if: failure() && steps.scan.outputs.result == 'found'
|
||||||
|
run: |
|
||||||
|
REPO="${{ github.event.repository.name }}"
|
||||||
|
curl -sS \
|
||||||
|
-H "Title: ${REPO} — secrets detected in code" \
|
||||||
|
-H "Tags: rotating_light,key" \
|
||||||
|
-H "Priority: urgent" \
|
||||||
|
-d "Gitleaks found potential secrets. Review and rotate credentials immediately." \
|
||||||
|
"${NTFY_URL}/${NTFY_TOPIC}" || true
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# Enforces branch merge policy:
|
||||||
|
# feature/* → dev only
|
||||||
|
# fix/* → dev only
|
||||||
|
# hotfix/* → dev or main (emergency)
|
||||||
|
# dev → main only
|
||||||
|
# alpha/* → dev only
|
||||||
|
# beta/* → dev only
|
||||||
|
# rc/* → main only
|
||||||
|
|
||||||
|
name: Branch Policy Check
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize, reopened, edited]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-target:
|
||||||
|
name: Verify merge target
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check branch policy
|
||||||
|
run: |
|
||||||
|
HEAD="${{ github.head_ref }}"
|
||||||
|
BASE="${{ github.base_ref }}"
|
||||||
|
|
||||||
|
echo "PR: ${HEAD} → ${BASE}"
|
||||||
|
|
||||||
|
ALLOWED=true
|
||||||
|
REASON=""
|
||||||
|
|
||||||
|
case "$HEAD" in
|
||||||
|
feature/*|feat/*)
|
||||||
|
if [ "$BASE" != "dev" ]; then
|
||||||
|
ALLOWED=false
|
||||||
|
REASON="Feature branches must target 'dev', not '${BASE}'"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
fix/*|bugfix/*)
|
||||||
|
if [ "$BASE" != "dev" ]; then
|
||||||
|
ALLOWED=false
|
||||||
|
REASON="Fix branches must target 'dev', not '${BASE}'"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
hotfix/*)
|
||||||
|
if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
|
||||||
|
ALLOWED=false
|
||||||
|
REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
alpha/*|beta/*)
|
||||||
|
if [ "$BASE" != "dev" ]; then
|
||||||
|
ALLOWED=false
|
||||||
|
REASON="Pre-release branches must target 'dev', not '${BASE}'"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
rc/*)
|
||||||
|
if [ "$BASE" != "main" ]; then
|
||||||
|
ALLOWED=false
|
||||||
|
REASON="Release candidate branches must target 'main', not '${BASE}'"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
dev)
|
||||||
|
if [ "$BASE" != "main" ]; then
|
||||||
|
ALLOWED=false
|
||||||
|
REASON="Dev branch can only merge into 'main', not '${BASE}'"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ "$ALLOWED" = false ]; then
|
||||||
|
echo "::error::${REASON}"
|
||||||
|
echo ""
|
||||||
|
echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "${REASON}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Branch policy: OK (${HEAD} → ${BASE})"
|
||||||
|
echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
|
||||||
@@ -0,0 +1,765 @@
|
|||||||
|
# ============================================================================
|
||||||
|
# Copyright (C) 2025 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: Gitea.Workflow
|
||||||
|
# INGROUP: MokoStandards.Validation
|
||||||
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
|
||||||
|
# PATH: /templates/workflows/joomla/repo_health.yml.template
|
||||||
|
# VERSION: 04.06.00
|
||||||
|
# BRIEF: Enforces repository guardrails by validating release configuration, scripts governance, tooling availability, and core repository health artifacts.
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
name: Repo Health
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: repo-health-${{ github.repository }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
profile:
|
||||||
|
description: 'Validation profile: all, release, scripts, or repo'
|
||||||
|
required: true
|
||||||
|
default: all
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- all
|
||||||
|
- release
|
||||||
|
- scripts
|
||||||
|
- repo
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
env:
|
||||||
|
# Release policy - Repository Variables Only
|
||||||
|
RELEASE_REQUIRED_REPO_VARS: RS_FTP_PATH_SUFFIX
|
||||||
|
RELEASE_OPTIONAL_REPO_VARS: DEV_FTP_SUFFIX
|
||||||
|
|
||||||
|
# Scripts governance policy
|
||||||
|
SCRIPTS_REQUIRED_DIRS:
|
||||||
|
SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate
|
||||||
|
|
||||||
|
# Repo health policy
|
||||||
|
REPO_REQUIRED_ARTIFACTS: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,.gitea/workflows/
|
||||||
|
REPO_OPTIONAL_FILES: SECURITY.md,GOVERNANCE.md,.editorconfig,.gitattributes,.gitignore,README.md
|
||||||
|
REPO_DISALLOWED_DIRS:
|
||||||
|
REPO_DISALLOWED_FILES: TODO.md,todo.md
|
||||||
|
|
||||||
|
# Extended checks toggles
|
||||||
|
EXTENDED_CHECKS: "true"
|
||||||
|
|
||||||
|
# File / directory variables
|
||||||
|
DOCS_INDEX: ""
|
||||||
|
SCRIPT_DIR: scripts
|
||||||
|
WORKFLOWS_DIR: .gitea/workflows
|
||||||
|
SHELLCHECK_PATTERN: '*.sh'
|
||||||
|
SPDX_FILE_GLOBS: '*.sh,*.php,*.js,*.ts,*.css,*.xml,*.yml,*.yaml'
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
access_check:
|
||||||
|
name: Access control
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 10
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
allowed: ${{ steps.perm.outputs.allowed }}
|
||||||
|
permission: ${{ steps.perm.outputs.permission }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check actor permission (admin only)
|
||||||
|
id: perm
|
||||||
|
env:
|
||||||
|
TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
ACTOR: ${{ github.actor }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
ALLOWED=false
|
||||||
|
PERMISSION=unknown
|
||||||
|
METHOD=""
|
||||||
|
|
||||||
|
# Hardcoded authorized users — always allowed
|
||||||
|
case "$ACTOR" in
|
||||||
|
jmiller|gitea-actions[bot])
|
||||||
|
ALLOWED=true
|
||||||
|
PERMISSION=admin
|
||||||
|
METHOD="hardcoded allowlist"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
# Detect platform and check permissions via API
|
||||||
|
API_BASE="${GITHUB_API_URL:-${GITEA_API_URL:-https://api.github.com}}"
|
||||||
|
RESP=$(curl -sf -H "Authorization: token ${TOKEN}" \
|
||||||
|
"${API_BASE}/repos/${REPO}/collaborators/${ACTOR}/permission" 2>/dev/null || echo '{}')
|
||||||
|
PERMISSION=$(echo "$RESP" | grep -oP '"permission"\s*:\s*"\K[^"]+' || echo "unknown")
|
||||||
|
if [ "$PERMISSION" = "admin" ] || [ "$PERMISSION" = "maintain" ] || [ "$PERMISSION" = "owner" ]; then
|
||||||
|
ALLOWED=true
|
||||||
|
fi
|
||||||
|
METHOD="collaborator API"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo "permission=${PERMISSION}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "allowed=${ALLOWED}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "## Access Authorization"
|
||||||
|
echo ""
|
||||||
|
echo "| Field | Value |"
|
||||||
|
echo "|-------|-------|"
|
||||||
|
echo "| **Actor** | \`${ACTOR}\` |"
|
||||||
|
echo "| **Repository** | \`${REPO}\` |"
|
||||||
|
echo "| **Permission** | \`${PERMISSION}\` |"
|
||||||
|
echo "| **Method** | ${METHOD} |"
|
||||||
|
echo "| **Authorized** | ${ALLOWED} |"
|
||||||
|
echo ""
|
||||||
|
if [ "$ALLOWED" = "true" ]; then
|
||||||
|
echo "${ACTOR} authorized (${METHOD})"
|
||||||
|
else
|
||||||
|
echo "${ACTOR} is NOT authorized. Requires admin or maintain role."
|
||||||
|
fi
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
|
- name: Deny execution when not permitted
|
||||||
|
if: ${{ steps.perm.outputs.allowed != 'true' }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
printf '%s\n' 'ERROR: Access denied. Admin permission required.' >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
release_config:
|
||||||
|
name: Release configuration
|
||||||
|
needs: access_check
|
||||||
|
if: ${{ needs.access_check.outputs.allowed == 'true' }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 20
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Guardrails release vars
|
||||||
|
env:
|
||||||
|
PROFILE_RAW: ${{ github.event.inputs.profile }}
|
||||||
|
RS_FTP_PATH_SUFFIX: ${{ vars.RS_FTP_PATH_SUFFIX }}
|
||||||
|
DEV_FTP_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
profile="${PROFILE_RAW:-all}"
|
||||||
|
case "${profile}" in
|
||||||
|
all|release|scripts|repo) ;;
|
||||||
|
*)
|
||||||
|
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ "${profile}" = 'scripts' ] || [ "${profile}" = 'repo' ]; then
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Release configuration (Repository Variables)'
|
||||||
|
printf '%s\n' "Profile: ${profile}"
|
||||||
|
printf '%s\n' 'Status: SKIPPED'
|
||||||
|
printf '%s\n' 'Reason: profile excludes release validation'
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
IFS=',' read -r -a required <<< "${RELEASE_REQUIRED_REPO_VARS}"
|
||||||
|
IFS=',' read -r -a optional <<< "${RELEASE_OPTIONAL_REPO_VARS}"
|
||||||
|
|
||||||
|
missing=()
|
||||||
|
missing_optional=()
|
||||||
|
|
||||||
|
for k in "${required[@]}"; do
|
||||||
|
v="${!k:-}"
|
||||||
|
[ -z "${v}" ] && missing+=("${k}")
|
||||||
|
done
|
||||||
|
|
||||||
|
for k in "${optional[@]}"; do
|
||||||
|
v="${!k:-}"
|
||||||
|
[ -z "${v}" ] && missing_optional+=("${k}")
|
||||||
|
done
|
||||||
|
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Release configuration (Repository Variables)'
|
||||||
|
printf '%s\n' "Profile: ${profile}"
|
||||||
|
printf '%s\n' '| Variable | Status |'
|
||||||
|
printf '%s\n' '|---|---|'
|
||||||
|
printf '%s\n' "| RS_FTP_PATH_SUFFIX | ${RS_FTP_PATH_SUFFIX:-NOT SET} |"
|
||||||
|
printf '%s\n' "| DEV_FTP_SUFFIX | ${DEV_FTP_SUFFIX:-NOT SET} |"
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
|
if [ "${#missing_optional[@]}" -gt 0 ]; then
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Missing optional repository variables'
|
||||||
|
for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${#missing[@]}" -gt 0 ]; then
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Missing required repository variables'
|
||||||
|
for m in "${missing[@]}"; do printf '%s\n' "- ${m}"; done
|
||||||
|
printf '%s\n' 'ERROR: Guardrails failed. Missing required repository variables.'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Repository variables validation result'
|
||||||
|
printf '%s\n' 'Status: OK'
|
||||||
|
printf '%s\n' 'All required repository variables present.'
|
||||||
|
printf '%s\n' ''
|
||||||
|
printf '%s\n' '**Note**: Organization secrets (RS_FTP_HOST, RS_FTP_USER, etc.) are validated at deployment time, not in repository health checks.'
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
|
scripts_governance:
|
||||||
|
name: Scripts governance
|
||||||
|
needs: access_check
|
||||||
|
if: ${{ needs.access_check.outputs.allowed == 'true' }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Scripts folder checks
|
||||||
|
env:
|
||||||
|
PROFILE_RAW: ${{ github.event.inputs.profile }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
profile="${PROFILE_RAW:-all}"
|
||||||
|
case "${profile}" in
|
||||||
|
all|release|scripts|repo) ;;
|
||||||
|
*)
|
||||||
|
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ "${profile}" = 'release' ] || [ "${profile}" = 'repo' ]; then
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Scripts governance'
|
||||||
|
printf '%s\n' "Profile: ${profile}"
|
||||||
|
printf '%s\n' 'Status: SKIPPED'
|
||||||
|
printf '%s\n' 'Reason: profile excludes scripts governance'
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d "${SCRIPT_DIR}" ]; then
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Scripts governance'
|
||||||
|
printf '%s\n' 'Status: OK (advisory)'
|
||||||
|
printf '%s\n' 'scripts/ directory not present. No scripts governance enforced.'
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
IFS=',' read -r -a required_dirs <<< "${SCRIPTS_REQUIRED_DIRS}"
|
||||||
|
IFS=',' read -r -a allowed_dirs <<< "${SCRIPTS_ALLOWED_DIRS}"
|
||||||
|
|
||||||
|
missing_dirs=()
|
||||||
|
unapproved_dirs=()
|
||||||
|
|
||||||
|
for d in "${required_dirs[@]}"; do
|
||||||
|
req="${d%/}"
|
||||||
|
[ ! -d "${req}" ] && missing_dirs+=("${req}/")
|
||||||
|
done
|
||||||
|
|
||||||
|
while IFS= read -r d; do
|
||||||
|
allowed=false
|
||||||
|
for a in "${allowed_dirs[@]}"; do
|
||||||
|
a_norm="${a%/}"
|
||||||
|
[ "${d%/}" = "${a_norm}" ] && allowed=true
|
||||||
|
done
|
||||||
|
[ "${allowed}" = false ] && unapproved_dirs+=("${d%/}/")
|
||||||
|
done < <(find "${SCRIPT_DIR}" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | sed 's#^\./##')
|
||||||
|
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Scripts governance'
|
||||||
|
printf '%s\n' "Profile: ${profile}"
|
||||||
|
printf '%s\n' '| Area | Status | Notes |'
|
||||||
|
printf '%s\n' '|---|---|---|'
|
||||||
|
|
||||||
|
if [ "${#missing_dirs[@]}" -gt 0 ]; then
|
||||||
|
printf '%s\n' '| Required directories | Warning | Missing required subfolders |'
|
||||||
|
else
|
||||||
|
printf '%s\n' '| Required directories | OK | All required subfolders present |'
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${#unapproved_dirs[@]}" -gt 0 ]; then
|
||||||
|
printf '%s\n' '| Directory policy | Warning | Unapproved directories detected |'
|
||||||
|
else
|
||||||
|
printf '%s\n' '| Directory policy | OK | No unapproved directories |'
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' '| Enforcement mode | Advisory | scripts folder is optional |'
|
||||||
|
printf '\n'
|
||||||
|
|
||||||
|
if [ "${#missing_dirs[@]}" -gt 0 ]; then
|
||||||
|
printf '%s\n' 'Missing required script directories:'
|
||||||
|
for m in "${missing_dirs[@]}"; do printf '%s\n' "- ${m}"; done
|
||||||
|
printf '\n'
|
||||||
|
else
|
||||||
|
printf '%s\n' 'Missing required script directories: none.'
|
||||||
|
printf '\n'
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${#unapproved_dirs[@]}" -gt 0 ]; then
|
||||||
|
printf '%s\n' 'Unapproved script directories detected:'
|
||||||
|
for m in "${unapproved_dirs[@]}"; do printf '%s\n' "- ${m}"; done
|
||||||
|
printf '\n'
|
||||||
|
else
|
||||||
|
printf '%s\n' 'Unapproved script directories detected: none.'
|
||||||
|
printf '\n'
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' 'Scripts governance completed in advisory mode.'
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
|
repo_health:
|
||||||
|
name: Repository health
|
||||||
|
needs: access_check
|
||||||
|
if: ${{ needs.access_check.outputs.allowed == 'true' }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 20
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Repository health checks
|
||||||
|
env:
|
||||||
|
PROFILE_RAW: ${{ github.event.inputs.profile }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
profile="${PROFILE_RAW:-all}"
|
||||||
|
case "${profile}" in
|
||||||
|
all|release|scripts|repo) ;;
|
||||||
|
*)
|
||||||
|
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ "${profile}" = 'release' ] || [ "${profile}" = 'scripts' ]; then
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Repository health'
|
||||||
|
printf '%s\n' "Profile: ${profile}"
|
||||||
|
printf '%s\n' 'Status: SKIPPED'
|
||||||
|
printf '%s\n' 'Reason: profile excludes repository health'
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Source directory: src/ or htdocs/ (either is valid)
|
||||||
|
if [ -d "src" ]; then
|
||||||
|
SOURCE_DIR="src"
|
||||||
|
elif [ -d "htdocs" ]; then
|
||||||
|
SOURCE_DIR="htdocs"
|
||||||
|
else
|
||||||
|
missing_required+=("src/ or htdocs/ (source directory required)")
|
||||||
|
fi
|
||||||
|
|
||||||
|
IFS=',' read -r -a required_artifacts <<< "${REPO_REQUIRED_ARTIFACTS}"
|
||||||
|
IFS=',' read -r -a optional_files <<< "${REPO_OPTIONAL_FILES}"
|
||||||
|
IFS=',' read -r -a disallowed_dirs <<< "${REPO_DISALLOWED_DIRS}"
|
||||||
|
IFS=',' read -r -a disallowed_files <<< "${REPO_DISALLOWED_FILES}"
|
||||||
|
|
||||||
|
missing_required=()
|
||||||
|
missing_optional=()
|
||||||
|
|
||||||
|
for item in "${required_artifacts[@]}"; do
|
||||||
|
if printf '%s' "${item}" | grep -q '/$'; then
|
||||||
|
d="${item%/}"
|
||||||
|
[ ! -d "${d}" ] && missing_required+=("${item}")
|
||||||
|
else
|
||||||
|
[ ! -f "${item}" ] && missing_required+=("${item}")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
for f in "${optional_files[@]}"; do
|
||||||
|
if printf '%s' "${f}" | grep -q '/$'; then
|
||||||
|
d="${f%/}"
|
||||||
|
[ ! -d "${d}" ] && missing_optional+=("${f}")
|
||||||
|
else
|
||||||
|
[ ! -f "${f}" ] && missing_optional+=("${f}")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
for d in "${disallowed_dirs[@]}"; do
|
||||||
|
d_norm="${d%/}"
|
||||||
|
[ -d "${d_norm}" ] && missing_required+=("${d_norm}/ (disallowed)")
|
||||||
|
done
|
||||||
|
|
||||||
|
for f in "${disallowed_files[@]}"; do
|
||||||
|
[ -f "${f}" ] && missing_required+=("${f} (disallowed)")
|
||||||
|
done
|
||||||
|
|
||||||
|
git fetch origin --prune
|
||||||
|
|
||||||
|
dev_paths=()
|
||||||
|
dev_branches=()
|
||||||
|
|
||||||
|
while IFS= read -r b; do
|
||||||
|
name="${b#origin/}"
|
||||||
|
if [ "${name}" = 'dev' ]; then
|
||||||
|
dev_branches+=("${name}")
|
||||||
|
else
|
||||||
|
dev_paths+=("${name}")
|
||||||
|
fi
|
||||||
|
done < <(git branch -r --list 'origin/dev*' | sed 's/^ *//')
|
||||||
|
|
||||||
|
if [ "${#dev_paths[@]}" -eq 0 ] && [ "${#dev_branches[@]}" -eq 0 ]; then
|
||||||
|
missing_required+=("dev branch")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${#dev_branches[@]}" -gt 0 ]; then
|
||||||
|
fi
|
||||||
|
|
||||||
|
content_warnings=()
|
||||||
|
|
||||||
|
if [ -f 'CHANGELOG.md' ] && ! grep -Eq '^# Changelog' CHANGELOG.md; then
|
||||||
|
content_warnings+=("CHANGELOG.md missing '# Changelog' header")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f 'CHANGELOG.md' ] && grep -Eq '^[# ]*Unreleased' CHANGELOG.md; then
|
||||||
|
content_warnings+=("CHANGELOG.md contains Unreleased section (review release readiness)")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f 'LICENSE' ] && ! grep -qiE 'GNU GENERAL PUBLIC LICENSE|GPL' LICENSE; then
|
||||||
|
content_warnings+=("LICENSE does not look like a GPL text")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f 'README.md' ] && ! grep -qiE 'moko|Moko' README.md; then
|
||||||
|
content_warnings+=("README.md missing expected brand keyword")
|
||||||
|
fi
|
||||||
|
|
||||||
|
export PROFILE_RAW="${profile}"
|
||||||
|
export MISSING_REQUIRED="$(printf '%s\n' "${missing_required[@]:-}")"
|
||||||
|
export MISSING_OPTIONAL="$(printf '%s\n' "${missing_optional[@]:-}")"
|
||||||
|
export CONTENT_WARNINGS="$(printf '%s\n' "${content_warnings[@]:-}")"
|
||||||
|
|
||||||
|
report_json="$(python3 - <<'PY'
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
profile = os.environ.get('PROFILE_RAW') or 'all'
|
||||||
|
|
||||||
|
missing_required = os.environ.get('MISSING_REQUIRED', '').splitlines() if os.environ.get('MISSING_REQUIRED') else []
|
||||||
|
missing_optional = os.environ.get('MISSING_OPTIONAL', '').splitlines() if os.environ.get('MISSING_OPTIONAL') else []
|
||||||
|
content_warnings = os.environ.get('CONTENT_WARNINGS', '').splitlines() if os.environ.get('CONTENT_WARNINGS') else []
|
||||||
|
|
||||||
|
out = {
|
||||||
|
'profile': profile,
|
||||||
|
'missing_required': [x for x in missing_required if x],
|
||||||
|
'missing_optional': [x for x in missing_optional if x],
|
||||||
|
'content_warnings': [x for x in content_warnings if x],
|
||||||
|
}
|
||||||
|
|
||||||
|
print(json.dumps(out, indent=2))
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Repository health'
|
||||||
|
printf '%s\n' "Profile: ${profile}"
|
||||||
|
printf '%s\n' '| Metric | Value |'
|
||||||
|
printf '%s\n' '|---|---|'
|
||||||
|
printf '%s\n' "| Missing required | ${#missing_required[@]} |"
|
||||||
|
printf '%s\n' "| Missing optional | ${#missing_optional[@]} |"
|
||||||
|
printf '%s\n' "| Content warnings | ${#content_warnings[@]} |"
|
||||||
|
printf '\n'
|
||||||
|
|
||||||
|
printf '%s\n' '### Guardrails report (JSON)'
|
||||||
|
printf '%s\n' '```json'
|
||||||
|
printf '%s\n' "${report_json}"
|
||||||
|
printf '%s\n' '```'
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
|
if [ "${#missing_required[@]}" -gt 0 ]; then
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Missing required repo artifacts'
|
||||||
|
for m in "${missing_required[@]}"; do printf '%s\n' "- ${m}"; done
|
||||||
|
printf '%s\n' 'ERROR: Guardrails failed. Missing required repository artifacts.'
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${#missing_optional[@]}" -gt 0 ]; then
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Missing optional repo artifacts'
|
||||||
|
for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${#content_warnings[@]}" -gt 0 ]; then
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Repo content warnings'
|
||||||
|
for m in "${content_warnings[@]}"; do printf '%s\n' "- ${m}"; done
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -- Joomla-specific checks --
|
||||||
|
joomla_findings=()
|
||||||
|
|
||||||
|
MANIFEST="$(find . -maxdepth 2 -name '*.xml' -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true)"
|
||||||
|
if [ -z "${MANIFEST}" ]; then
|
||||||
|
joomla_findings+=("Joomla XML manifest not found (no *.xml with <extension> tag)")
|
||||||
|
else
|
||||||
|
if ! grep -qP '<version>' "${MANIFEST}"; then
|
||||||
|
joomla_findings+=("XML manifest: <version> tag missing")
|
||||||
|
fi
|
||||||
|
if ! grep -qP 'type="(component|module|plugin|library|package|template|language)"' "${MANIFEST}"; then
|
||||||
|
joomla_findings+=("XML manifest: type attribute missing or invalid")
|
||||||
|
fi
|
||||||
|
if ! grep -qP '<name>' "${MANIFEST}"; then
|
||||||
|
joomla_findings+=("XML manifest: <name> tag missing")
|
||||||
|
fi
|
||||||
|
if ! grep -qP '<author>' "${MANIFEST}"; then
|
||||||
|
joomla_findings+=("XML manifest: <author> tag missing")
|
||||||
|
fi
|
||||||
|
if ! grep -qP '<namespace' "${MANIFEST}"; then
|
||||||
|
joomla_findings+=("XML manifest: <namespace> missing (required for Joomla 5+)")
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
INI_COUNT="$(find . -name '*.ini' -type f 2>/dev/null | wc -l)"
|
||||||
|
if [ "${INI_COUNT}" -eq 0 ]; then
|
||||||
|
joomla_findings+=("No .ini language files found")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f 'updates.xml' ]; then
|
||||||
|
joomla_findings+=("updates.xml missing in root (required for Joomla update server)")
|
||||||
|
fi
|
||||||
|
|
||||||
|
INDEX_DIRS=("${SOURCE_DIR}" "${SOURCE_DIR}/admin" "${SOURCE_DIR}/site")
|
||||||
|
for dir in "${INDEX_DIRS[@]}"; do
|
||||||
|
if [ -d "${dir}" ] && [ ! -f "${dir}/index.html" ]; then
|
||||||
|
joomla_findings+=("${dir}/index.html missing (directory listing protection)")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "${#joomla_findings[@]}" -gt 0 ]; then
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Joomla extension checks'
|
||||||
|
printf '%s\n' '| Check | Status |'
|
||||||
|
printf '%s\n' '|---|---|'
|
||||||
|
for f in "${joomla_findings[@]}"; do
|
||||||
|
printf '%s\n' "| ${f} | Warning |"
|
||||||
|
done
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
else
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Joomla extension checks'
|
||||||
|
printf '%s\n' 'All Joomla-specific checks passed.'
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
extended_enabled="${EXTENDED_CHECKS:-true}"
|
||||||
|
extended_findings=()
|
||||||
|
|
||||||
|
if [ "${extended_enabled}" = 'true' ]; then
|
||||||
|
if [ -f '.github/CODEOWNERS' ] || [ -f 'CODEOWNERS' ] || [ -f 'docs/CODEOWNERS' ]; then
|
||||||
|
:
|
||||||
|
else
|
||||||
|
extended_findings+=("CODEOWNERS not found (.github/CODEOWNERS preferred)")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ls "${WORKFLOWS_DIR}"/*.yml >/dev/null 2>&1 || ls "${WORKFLOWS_DIR}"/*.yaml >/dev/null 2>&1; then
|
||||||
|
bad_refs="$(grep -RIn --include='*.yml' --include='*.yaml' -E '^[[:space:]]*uses:[[:space:]]*[^#]+@(main|master)\b' "${WORKFLOWS_DIR}" 2>/dev/null || true)"
|
||||||
|
if [ -n "${bad_refs}" ]; then
|
||||||
|
extended_findings+=("Workflows reference actions @main/@master (pin versions): see log excerpt")
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Workflow pinning advisory'
|
||||||
|
printf '%s\n' 'Found uses: entries pinned to main/master:'
|
||||||
|
printf '%s\n' '```'
|
||||||
|
printf '%s\n' "${bad_refs}"
|
||||||
|
printf '%s\n' '```'
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "${DOCS_INDEX}" ]; then
|
||||||
|
missing_links="$(python3 - <<'PY'
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
idx = os.environ.get('DOCS_INDEX', 'docs/docs-index.md')
|
||||||
|
base = os.getcwd()
|
||||||
|
|
||||||
|
bad = []
|
||||||
|
pat = re.compile(r'\[[^\]]+\]\(([^)]+)\)')
|
||||||
|
|
||||||
|
with open(idx, 'r', encoding='utf-8') as f:
|
||||||
|
for line in f:
|
||||||
|
for m in pat.findall(line):
|
||||||
|
link = m.strip()
|
||||||
|
if link.startswith('http://') or link.startswith('https://') or link.startswith('#') or link.startswith('mailto:'):
|
||||||
|
continue
|
||||||
|
if link.startswith('/'):
|
||||||
|
rel = link.lstrip('/')
|
||||||
|
else:
|
||||||
|
rel = os.path.normpath(os.path.join(os.path.dirname(idx), link))
|
||||||
|
rel = rel.split('#', 1)[0]
|
||||||
|
rel = rel.split('?', 1)[0]
|
||||||
|
if not rel:
|
||||||
|
continue
|
||||||
|
p = os.path.join(base, rel)
|
||||||
|
if not os.path.exists(p):
|
||||||
|
bad.append(rel)
|
||||||
|
|
||||||
|
print('\n'.join(sorted(set(bad))))
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
if [ -n "${missing_links}" ]; then
|
||||||
|
extended_findings+=("docs/docs-index.md contains broken relative links")
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Docs index link integrity'
|
||||||
|
printf '%s\n' 'Broken relative links:'
|
||||||
|
while IFS= read -r l; do [ -n "${l}" ] && printf '%s\n' "- ${l}"; done <<< "${missing_links}"
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -d "${SCRIPT_DIR}" ]; then
|
||||||
|
if ! command -v shellcheck >/dev/null 2>&1; then
|
||||||
|
sudo apt-get update -qq
|
||||||
|
sudo apt-get install -y shellcheck >/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
sc_out=''
|
||||||
|
while IFS= read -r shf; do
|
||||||
|
[ -z "${shf}" ] && continue
|
||||||
|
out_one="$(shellcheck -S warning -x "${shf}" 2>/dev/null || true)"
|
||||||
|
if [ -n "${out_one}" ]; then
|
||||||
|
sc_out="${sc_out}${out_one}\n"
|
||||||
|
fi
|
||||||
|
done < <(find "${SCRIPT_DIR}" -type f -name "${SHELLCHECK_PATTERN}" 2>/dev/null | sort)
|
||||||
|
|
||||||
|
if [ -n "${sc_out}" ]; then
|
||||||
|
extended_findings+=("ShellCheck warnings detected (advisory)")
|
||||||
|
sc_head="$(printf '%s' "${sc_out}" | head -n 200)"
|
||||||
|
{
|
||||||
|
printf '%s\n' '### ShellCheck (advisory)'
|
||||||
|
printf '%s\n' '```'
|
||||||
|
printf '%s\n' "${sc_head}"
|
||||||
|
printf '%s\n' '```'
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
spdx_missing=()
|
||||||
|
IFS=',' read -r -a spdx_globs <<< "${SPDX_FILE_GLOBS}"
|
||||||
|
spdx_args=()
|
||||||
|
for g in "${spdx_globs[@]}"; do spdx_args+=("${g}"); done
|
||||||
|
|
||||||
|
while IFS= read -r f; do
|
||||||
|
[ -z "${f}" ] && continue
|
||||||
|
if ! head -n 40 "${f}" | grep -q 'SPDX-License-Identifier:'; then
|
||||||
|
spdx_missing+=("${f}")
|
||||||
|
fi
|
||||||
|
done < <(git ls-files "${spdx_args[@]}" 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [ "${#spdx_missing[@]}" -gt 0 ]; then
|
||||||
|
extended_findings+=("SPDX header missing in some tracked files (advisory)")
|
||||||
|
{
|
||||||
|
printf '%s\n' '### SPDX header advisory'
|
||||||
|
printf '%s\n' 'Files missing SPDX-License-Identifier (first 40 lines scan):'
|
||||||
|
for f in "${spdx_missing[@]}"; do printf '%s\n' "- ${f}"; done
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
stale_cutoff_days=180
|
||||||
|
stale_branches="$(git for-each-ref --format='%(refname:short) %(committerdate:unix)' refs/remotes/origin 2>/dev/null | awk -v now="$(date +%s)" -v days="${stale_cutoff_days}" '{if (now-$2 > days*86400) print $1}' | head -50)"
|
||||||
|
if [ -n "${stale_branches}" ]; then
|
||||||
|
extended_findings+=("Stale remote branches detected (advisory)")
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Git hygiene advisory'
|
||||||
|
printf '%s\n' "Branches with last commit older than ${stale_cutoff_days} days (sample up to 50):"
|
||||||
|
while IFS= read -r b; do [ -n "${b}" ] && printf '%s\n' "- ${b}"; done <<< "${stale_branches}"
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Guardrails coverage matrix'
|
||||||
|
printf '%s\n' '| Domain | Status | Notes |'
|
||||||
|
printf '%s\n' '|---|---|---|'
|
||||||
|
printf '%s\n' '| Access control | OK | Admin-only execution gate |'
|
||||||
|
printf '%s\n' '| Release variables | OK | Repository variables validation |'
|
||||||
|
printf '%s\n' '| Scripts governance | OK | Directory policy and advisory reporting |'
|
||||||
|
printf '%s\n' '| Repo required artifacts | OK | Required, optional, disallowed enforcement |'
|
||||||
|
printf '%s\n' '| Repo content heuristics | OK | Brand, license, changelog structure |'
|
||||||
|
if [ "${extended_enabled}" = 'true' ]; then
|
||||||
|
if [ "${#extended_findings[@]}" -gt 0 ]; then
|
||||||
|
printf '%s\n' '| Extended checks | Warning | See extended findings below |'
|
||||||
|
else
|
||||||
|
printf '%s\n' '| Extended checks | OK | No findings |'
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
printf '%s\n' '| Extended checks | SKIPPED | EXTENDED_CHECKS disabled |'
|
||||||
|
fi
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
|
if [ "${extended_enabled}" = 'true' ] && [ "${#extended_findings[@]}" -gt 0 ]; then
|
||||||
|
{
|
||||||
|
printf '%s\n' '### Extended findings (advisory)'
|
||||||
|
for f in "${extended_findings[@]}"; do printf '%s\n' "- ${f}"; done
|
||||||
|
printf '\n'
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' 'Repository health guardrails passed.' >> "${GITHUB_STEP_SUMMARY}"
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "@mokoconsulting/mcp-mokomonitor",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "MCP server for server health monitoring, uptime, and log tailing",
|
||||||
|
"type": "module",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"bin": { "monitor-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,51 @@
|
|||||||
|
// Note: Using execFile (not exec) — safe against shell injection.
|
||||||
|
// SSH arguments are passed as array elements, never interpolated into a shell string.
|
||||||
|
import { execFile as execFileCb } from 'node:child_process';
|
||||||
|
import { promisify } from 'node:util';
|
||||||
|
import type { MonitorConnection } from './types.js';
|
||||||
|
|
||||||
|
const execFile = promisify(execFileCb);
|
||||||
|
const TIMEOUT = 30_000;
|
||||||
|
|
||||||
|
export class MonitorClient {
|
||||||
|
private readonly conn: MonitorConnection;
|
||||||
|
|
||||||
|
constructor(conn: MonitorConnection) {
|
||||||
|
this.conn = conn;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sshArgs(cmd: string): string[] {
|
||||||
|
const args = ['-o', 'StrictHostKeyChecking=accept-new', '-o', 'BatchMode=yes'];
|
||||||
|
if (this.conn.port) args.push('-p', String(this.conn.port));
|
||||||
|
if (this.conn.keyPath) args.push('-i', this.conn.keyPath);
|
||||||
|
args.push(`${this.conn.username}@${this.conn.host}`, cmd);
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
async exec(cmd: string): Promise<string> {
|
||||||
|
const { stdout } = await execFile('ssh', this.sshArgs(cmd), { timeout: TIMEOUT });
|
||||||
|
return stdout.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
async health(): Promise<string> {
|
||||||
|
return this.exec('echo "hostname: $(hostname)" && echo "uptime: $(uptime)" && echo "memory:" && free -h && echo "disk:" && df -h / && echo "cpu: $(nproc) cores" && echo "load: $(cat /proc/loadavg)"');
|
||||||
|
}
|
||||||
|
|
||||||
|
async disk(): Promise<string> { return this.exec('df -h'); }
|
||||||
|
async memory(): Promise<string> { return this.exec('free -h'); }
|
||||||
|
|
||||||
|
async processes(sortBy: 'cpu' | 'mem' = 'cpu', count = 15): Promise<string> {
|
||||||
|
const sort = sortBy === 'cpu' ? '-pcpu' : '-pmem';
|
||||||
|
return this.exec(`ps aux --sort=${sort} | head -${count + 1}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async serviceStatus(service: string): Promise<string> {
|
||||||
|
return this.exec(`systemctl status ${service} --no-pager -l 2>&1 | head -30`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async tailLog(logPath: string, lines = 50): Promise<string> {
|
||||||
|
return this.exec(`tail -n ${lines} ${logPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async uptime(): Promise<string> { return this.exec('uptime'); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { readFile } from 'node:fs/promises';
|
||||||
|
import { resolve } from 'node:path';
|
||||||
|
import { homedir } from 'node:os';
|
||||||
|
import type { MonitorConfig, MonitorConnection, SitesConfig } from './types.js';
|
||||||
|
|
||||||
|
const CONFIG_FILENAME = '.mcp_mokomonitor.json';
|
||||||
|
|
||||||
|
export async function loadConfig(): Promise<MonitorConfig> {
|
||||||
|
const configPath = process.env.MONITOR_MCP_CONFIG
|
||||||
|
? resolve(process.env.MONITOR_MCP_CONFIG)
|
||||||
|
: resolve(homedir(), CONFIG_FILENAME);
|
||||||
|
|
||||||
|
const raw = await readFile(configPath, 'utf-8');
|
||||||
|
const parsed = JSON.parse(raw) as Partial<MonitorConfig>;
|
||||||
|
if (!parsed.connections || Object.keys(parsed.connections).length === 0) {
|
||||||
|
throw new Error(`No connections in ${configPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const config: MonitorConfig = {
|
||||||
|
connections: parsed.connections,
|
||||||
|
defaultConnection: parsed.defaultConnection ?? Object.keys(parsed.connections)[0],
|
||||||
|
grafana: parsed.grafana,
|
||||||
|
sitesJsonPath: parsed.sitesJsonPath,
|
||||||
|
};
|
||||||
|
|
||||||
|
// If sitesJsonPath is set, load sites.json and merge Grafana config from it
|
||||||
|
if (config.sitesJsonPath) {
|
||||||
|
try {
|
||||||
|
const sites = await loadSitesJson(config.sitesJsonPath);
|
||||||
|
if (!config.grafana && sites.grafana) {
|
||||||
|
config.grafana = {
|
||||||
|
baseUrl: sites.grafana.baseUrl,
|
||||||
|
apiKey: sites.grafana.apiKey,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch { /* sites.json is optional */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadSitesJson(path: string): Promise<SitesConfig> {
|
||||||
|
const raw = await readFile(resolve(path), 'utf-8');
|
||||||
|
return JSON.parse(raw) as SitesConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getConnection(config: MonitorConfig, name?: string): MonitorConnection {
|
||||||
|
const key = name ?? config.defaultConnection;
|
||||||
|
const conn = config.connections[key];
|
||||||
|
if (!conn) throw new Error(`Connection "${key}" not found. Available: ${Object.keys(config.connections).join(', ')}`);
|
||||||
|
return conn;
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import * as https from 'node:https';
|
||||||
|
import * as http from 'node:http';
|
||||||
|
import type { GrafanaConfig } from './types.js';
|
||||||
|
|
||||||
|
const TIMEOUT_MS = 30_000;
|
||||||
|
|
||||||
|
export class GrafanaClient {
|
||||||
|
private readonly baseUrl: string;
|
||||||
|
private readonly headers: Record<string, string>;
|
||||||
|
|
||||||
|
constructor(config: GrafanaConfig) {
|
||||||
|
this.baseUrl = config.baseUrl.replace(/\/+$/, '');
|
||||||
|
this.headers = {
|
||||||
|
'Authorization': `Bearer ${config.apiKey}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private request(url: string, method = 'GET'): Promise<{ status: number; data: unknown }> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const parsed = new URL(url);
|
||||||
|
const mod = parsed.protocol === 'https:' ? https : http;
|
||||||
|
const opts: http.RequestOptions = {
|
||||||
|
hostname: parsed.hostname,
|
||||||
|
port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
|
||||||
|
path: parsed.pathname + parsed.search,
|
||||||
|
method,
|
||||||
|
headers: this.headers,
|
||||||
|
timeout: TIMEOUT_MS,
|
||||||
|
};
|
||||||
|
|
||||||
|
const req = mod.request(opts, (res) => {
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
res.on('data', (c: Buffer) => chunks.push(c));
|
||||||
|
res.on('end', () => {
|
||||||
|
const raw = Buffer.concat(chunks).toString();
|
||||||
|
let data: unknown;
|
||||||
|
try { data = JSON.parse(raw); } catch { data = raw; }
|
||||||
|
resolve({ status: res.statusCode ?? 0, data });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
req.on('error', reject);
|
||||||
|
req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async health(): Promise<string> {
|
||||||
|
const res = await this.request(`${this.baseUrl}/api/health`);
|
||||||
|
return JSON.stringify(res.data, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
async listDashboards(): Promise<string> {
|
||||||
|
const res = await this.request(`${this.baseUrl}/api/search?type=dash-db`);
|
||||||
|
if (res.status >= 400) return `Error ${res.status}: ${JSON.stringify(res.data)}`;
|
||||||
|
const dashboards = res.data as Array<{ uid: string; title: string; url: string; tags: string[] }>;
|
||||||
|
return dashboards.map(d => `${d.title} (${d.uid}) — ${this.baseUrl}${d.url}`).join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDashboard(uid: string): Promise<string> {
|
||||||
|
const res = await this.request(`${this.baseUrl}/api/dashboards/uid/${encodeURIComponent(uid)}`);
|
||||||
|
return JSON.stringify(res.data, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
async listDatasources(): Promise<string> {
|
||||||
|
const res = await this.request(`${this.baseUrl}/api/datasources`);
|
||||||
|
if (res.status >= 400) return `Error ${res.status}: ${JSON.stringify(res.data)}`;
|
||||||
|
const sources = res.data as Array<{ id: number; name: string; type: string; url: string }>;
|
||||||
|
return sources.map(s => `[${s.id}] ${s.name} (${s.type}) — ${s.url}`).join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
async queryDatasource(datasourceId: number, query: string, from = 'now-1h', to = 'now'): Promise<string> {
|
||||||
|
// Grafana /api/ds/query for ad-hoc queries
|
||||||
|
const body = JSON.stringify({
|
||||||
|
queries: [{
|
||||||
|
datasourceId,
|
||||||
|
rawSql: query,
|
||||||
|
format: 'table',
|
||||||
|
refId: 'A',
|
||||||
|
}],
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const parsed = new URL(`${this.baseUrl}/api/ds/query`);
|
||||||
|
const mod = parsed.protocol === 'https:' ? https : http;
|
||||||
|
const opts: http.RequestOptions = {
|
||||||
|
hostname: parsed.hostname,
|
||||||
|
port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
|
||||||
|
path: parsed.pathname,
|
||||||
|
method: 'POST',
|
||||||
|
headers: { ...this.headers, 'Content-Length': Buffer.byteLength(body) },
|
||||||
|
timeout: TIMEOUT_MS,
|
||||||
|
};
|
||||||
|
const req = mod.request(opts, (res) => {
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
res.on('data', (c: Buffer) => chunks.push(c));
|
||||||
|
res.on('end', () => resolve(Buffer.concat(chunks).toString()));
|
||||||
|
});
|
||||||
|
req.on('error', reject);
|
||||||
|
req.write(body);
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async listAlerts(): Promise<string> {
|
||||||
|
const res = await this.request(`${this.baseUrl}/api/alertmanager/grafana/api/v2/alerts`);
|
||||||
|
if (res.status >= 400) return `Error ${res.status}: ${JSON.stringify(res.data)}`;
|
||||||
|
const alerts = res.data as Array<{ labels: Record<string, string>; status: { state: string } }>;
|
||||||
|
if (alerts.length === 0) return 'No active alerts';
|
||||||
|
return alerts.map(a => `[${a.status.state}] ${a.labels.alertname ?? 'unnamed'}: ${a.labels.summary ?? ''}`).join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAlertRules(): Promise<string> {
|
||||||
|
const res = await this.request(`${this.baseUrl}/api/v1/provisioning/alert-rules`);
|
||||||
|
return JSON.stringify(res.data, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
async listAnnotations(from?: string, to?: string): Promise<string> {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (from) params.set('from', from);
|
||||||
|
if (to) params.set('to', to);
|
||||||
|
params.set('limit', '50');
|
||||||
|
const res = await this.request(`${this.baseUrl}/api/annotations?${params.toString()}`);
|
||||||
|
return JSON.stringify(res.data, null, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,256 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||||
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { loadConfig, getConnection, loadSitesJson } from './config.js';
|
||||||
|
import { MonitorClient } from './client.js';
|
||||||
|
import { GrafanaClient } from './grafana.js';
|
||||||
|
import type { MonitorConfig, SitesConfig } from './types.js';
|
||||||
|
|
||||||
|
let config: MonitorConfig;
|
||||||
|
let grafana: GrafanaClient | null = null;
|
||||||
|
let sitesConfig: SitesConfig | null = null;
|
||||||
|
|
||||||
|
function clientFor(connection?: string): MonitorClient {
|
||||||
|
return new MonitorClient(getConnection(config, connection));
|
||||||
|
}
|
||||||
|
|
||||||
|
function text(s: string) {
|
||||||
|
return { content: [{ type: 'text' as const, text: s }] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const Conn = { connection: z.string().optional().describe('Server connection name') };
|
||||||
|
|
||||||
|
const server = new McpServer({ name: 'monitor-mcp', version: '1.0.0' });
|
||||||
|
|
||||||
|
// ── SSH-based monitoring ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
server.tool('monitor_health', 'Full health check — uptime, disk, memory, cpu, load', { ...Conn },
|
||||||
|
async ({ connection }) => text(await clientFor(connection).health()));
|
||||||
|
|
||||||
|
server.tool('monitor_disk', 'Disk usage details', { ...Conn },
|
||||||
|
async ({ connection }) => text(await clientFor(connection).disk()));
|
||||||
|
|
||||||
|
server.tool('monitor_memory', 'Memory and swap usage', { ...Conn },
|
||||||
|
async ({ connection }) => text(await clientFor(connection).memory()));
|
||||||
|
|
||||||
|
server.tool('monitor_processes', 'Top processes by CPU or memory', {
|
||||||
|
...Conn,
|
||||||
|
sort_by: z.enum(['cpu', 'mem']).optional().describe('Sort by cpu or mem (default cpu)'),
|
||||||
|
count: z.number().optional().describe('Number of processes (default 15)'),
|
||||||
|
}, async ({ connection, sort_by, count }) => text(await clientFor(connection).processes(sort_by ?? 'cpu', count ?? 15)));
|
||||||
|
|
||||||
|
server.tool('monitor_service', 'Check systemd service status', {
|
||||||
|
...Conn, service: z.string().describe('Service name (e.g. gitea, nginx, mysql)'),
|
||||||
|
}, async ({ connection, service }) => text(await clientFor(connection).serviceStatus(service)));
|
||||||
|
|
||||||
|
server.tool('monitor_logs', 'Tail a log file', {
|
||||||
|
...Conn,
|
||||||
|
log_path: z.string().describe('Full path to log file'),
|
||||||
|
lines: z.number().optional().describe('Number of lines (default 50)'),
|
||||||
|
}, async ({ connection, log_path, lines }) => text(await clientFor(connection).tailLog(log_path, lines ?? 50)));
|
||||||
|
|
||||||
|
server.tool('monitor_uptime', 'Server uptime and load average', { ...Conn },
|
||||||
|
async ({ connection }) => text(await clientFor(connection).uptime()));
|
||||||
|
|
||||||
|
server.tool('monitor_list_servers', 'List configured server connections', {},
|
||||||
|
async () => text(Object.entries(config.connections).map(([k, v]) =>
|
||||||
|
`${k}${k === config.defaultConnection ? ' (default)' : ''}: ${v.username}@${v.host}:${v.port ?? 22}`
|
||||||
|
).join('\n') + (config.grafana ? `\n\nGrafana: ${config.grafana.baseUrl}` : '\n\nGrafana: not configured')));
|
||||||
|
|
||||||
|
// ── Grafana monitoring ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
server.tool('grafana_health', 'Check Grafana instance health', {},
|
||||||
|
async () => {
|
||||||
|
if (!grafana) return text('Grafana not configured. Add grafana.baseUrl and grafana.apiKey to config.');
|
||||||
|
return text(await grafana.health());
|
||||||
|
});
|
||||||
|
|
||||||
|
server.tool('grafana_dashboards', 'List all Grafana dashboards', {},
|
||||||
|
async () => {
|
||||||
|
if (!grafana) return text('Grafana not configured');
|
||||||
|
return text(await grafana.listDashboards());
|
||||||
|
});
|
||||||
|
|
||||||
|
server.tool('grafana_dashboard_get', 'Get dashboard details by UID', {
|
||||||
|
uid: z.string().describe('Dashboard UID'),
|
||||||
|
}, async ({ uid }) => {
|
||||||
|
if (!grafana) return text('Grafana not configured');
|
||||||
|
return text(await grafana.getDashboard(uid));
|
||||||
|
});
|
||||||
|
|
||||||
|
server.tool('grafana_datasources', 'List Grafana datasources', {},
|
||||||
|
async () => {
|
||||||
|
if (!grafana) return text('Grafana not configured');
|
||||||
|
return text(await grafana.listDatasources());
|
||||||
|
});
|
||||||
|
|
||||||
|
server.tool('grafana_alerts', 'List active Grafana alerts', {},
|
||||||
|
async () => {
|
||||||
|
if (!grafana) return text('Grafana not configured');
|
||||||
|
return text(await grafana.listAlerts());
|
||||||
|
});
|
||||||
|
|
||||||
|
server.tool('grafana_alert_rules', 'List Grafana alert rules', {},
|
||||||
|
async () => {
|
||||||
|
if (!grafana) return text('Grafana not configured');
|
||||||
|
return text(await grafana.getAlertRules());
|
||||||
|
});
|
||||||
|
|
||||||
|
server.tool('grafana_query', 'Run a query against a Grafana datasource', {
|
||||||
|
datasource_id: z.number().describe('Datasource ID'),
|
||||||
|
query: z.string().describe('SQL or PromQL query'),
|
||||||
|
from: z.string().optional().describe('Start time (default now-1h)'),
|
||||||
|
to: z.string().optional().describe('End time (default now)'),
|
||||||
|
}, async ({ datasource_id, query, from, to }) => {
|
||||||
|
if (!grafana) return text('Grafana not configured');
|
||||||
|
return text(await grafana.queryDatasource(datasource_id, query, from ?? 'now-1h', to ?? 'now'));
|
||||||
|
});
|
||||||
|
|
||||||
|
server.tool('grafana_annotations', 'List recent Grafana annotations', {
|
||||||
|
from: z.string().optional().describe('Start time'),
|
||||||
|
to: z.string().optional().describe('End time'),
|
||||||
|
}, async ({ from, to }) => {
|
||||||
|
if (!grafana) return text('Grafana not configured');
|
||||||
|
return text(await grafana.listAnnotations(from, to));
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Sites Definition (sites.json) ──────────────────────────────────
|
||||||
|
|
||||||
|
server.tool('monitor_sites', 'List all monitored sites from sites.json — shows URL, type, client, and token status', {},
|
||||||
|
async () => {
|
||||||
|
if (!sitesConfig) return text('No sites.json configured. Set sitesJsonPath in ~/.monitor-mcp.json');
|
||||||
|
const lines = sitesConfig.sites.map(s => {
|
||||||
|
const token = s.joomlaToken ? ' [API]' : s.dolibarrToken ? ' [API]' : '';
|
||||||
|
const akeeba = s.akeebaSecret ? ' [Backup]' : '';
|
||||||
|
return ` ${s.name} (${s.type}) — ${s.url} [${s.client}]${token}${akeeba}`;
|
||||||
|
});
|
||||||
|
return text(`Monitored sites (${sitesConfig.sites.length}):\n${lines.join('\n')}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
server.tool('monitor_site_add', 'Add a new site to sites.json monitoring definition', {
|
||||||
|
name: z.string().describe('Unique site name (e.g. "mysite-live", "mysite-dev")'),
|
||||||
|
url: z.string().describe('Site URL (e.g. "https://example.com")'),
|
||||||
|
type: z.enum(['mokowaas', 'mokowaas-dev', 'mokowaas-demo', 'mokocrm', 'mokocrm-dev', 'mokocrm-demo', 'other']).describe('Site type (mokowaas=Joomla, mokocrm=Dolibarr, other=everything else)'),
|
||||||
|
client: z.string().describe('Client name label'),
|
||||||
|
joomlaToken: z.string().optional().describe('Joomla API token'),
|
||||||
|
akeebaSecret: z.string().optional().describe('Akeeba Backup frontend secret word'),
|
||||||
|
tlsVerify: z.boolean().optional().describe('Verify TLS certificates (default true)'),
|
||||||
|
}, async ({ name, url, type, client, joomlaToken, akeebaSecret, tlsVerify }) => {
|
||||||
|
if (!sitesConfig || !config.sitesJsonPath) {
|
||||||
|
return text('No sites.json configured. Set sitesJsonPath in ~/.monitor-mcp.json');
|
||||||
|
}
|
||||||
|
// Check for duplicate name
|
||||||
|
if (sitesConfig.sites.some(s => s.name === name)) {
|
||||||
|
return text(`Site "${name}" already exists. Use a different name.`);
|
||||||
|
}
|
||||||
|
const newSite: Record<string, unknown> = { name, url, type, client };
|
||||||
|
if (joomlaToken) newSite.joomlaToken = joomlaToken;
|
||||||
|
if (akeebaSecret) newSite.akeebaSecret = akeebaSecret;
|
||||||
|
if (tlsVerify !== undefined) newSite.tlsVerify = tlsVerify;
|
||||||
|
|
||||||
|
sitesConfig.sites.push(newSite as any);
|
||||||
|
|
||||||
|
const { writeFile } = await import('node:fs/promises');
|
||||||
|
const { resolve } = await import('node:path');
|
||||||
|
await writeFile(resolve(config.sitesJsonPath), JSON.stringify(sitesConfig, null, '\t') + '\n');
|
||||||
|
|
||||||
|
return text(`Added site "${name}" (${type}) — ${url}\n\nRun generate-targets.sh to update Prometheus and monitoring configs.`);
|
||||||
|
});
|
||||||
|
|
||||||
|
server.tool('monitor_site_remove', 'Remove a site from sites.json', {
|
||||||
|
name: z.string().describe('Site name to remove'),
|
||||||
|
}, async ({ name }) => {
|
||||||
|
if (!sitesConfig || !config.sitesJsonPath) {
|
||||||
|
return text('No sites.json configured.');
|
||||||
|
}
|
||||||
|
const idx = sitesConfig.sites.findIndex(s => s.name === name);
|
||||||
|
if (idx === -1) return text(`Site "${name}" not found.`);
|
||||||
|
|
||||||
|
const removed = sitesConfig.sites.splice(idx, 1)[0];
|
||||||
|
const { writeFile } = await import('node:fs/promises');
|
||||||
|
const { resolve } = await import('node:path');
|
||||||
|
await writeFile(resolve(config.sitesJsonPath), JSON.stringify(sitesConfig, null, '\t') + '\n');
|
||||||
|
|
||||||
|
return text(`Removed site "${removed.name}" (${removed.url})\n\nRun generate-targets.sh to update Prometheus configs.`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Extended SSH monitoring (all commands run via execFile/ssh - safe) ──
|
||||||
|
|
||||||
|
server.tool('monitor_docker', 'List Docker containers with status', { ...Conn },
|
||||||
|
async ({ connection }) => text(await clientFor(connection).exec(
|
||||||
|
'docker ps --format "table {{.Names}}\\t{{.Status}}\\t{{.Ports}}" 2>/dev/null || echo "Docker not available"')));
|
||||||
|
|
||||||
|
server.tool('monitor_docker_stats', 'Docker container CPU/memory stats', { ...Conn },
|
||||||
|
async ({ connection }) => text(await clientFor(connection).exec(
|
||||||
|
'docker stats --no-stream --format "table {{.Name}}\\t{{.CPUPerc}}\\t{{.MemUsage}}\\t{{.NetIO}}" 2>/dev/null || echo "Docker not available"')));
|
||||||
|
|
||||||
|
server.tool('monitor_network', 'Network connections and listening ports', { ...Conn },
|
||||||
|
async ({ connection }) => text(await clientFor(connection).exec('ss -tlnp 2>/dev/null | head -30')));
|
||||||
|
|
||||||
|
server.tool('monitor_failed_logins', 'Recent failed SSH login attempts', {
|
||||||
|
...Conn, lines: z.number().optional().describe('Lines (default 20)'),
|
||||||
|
}, async ({ connection, lines }) => text(await clientFor(connection).exec(
|
||||||
|
`grep "Failed password" /var/log/auth.log 2>/dev/null | tail -${lines ?? 20}`)));
|
||||||
|
|
||||||
|
server.tool('monitor_cron', 'List active cron jobs', { ...Conn },
|
||||||
|
async ({ connection }) => text(await clientFor(connection).exec(
|
||||||
|
'for user in $(cut -f1 -d: /etc/passwd); do crontab -l -u $user 2>/dev/null | grep -v "^#" | grep -v "^$" | sed "s/^/$user: /"; done')));
|
||||||
|
|
||||||
|
server.tool('monitor_reboot_required', 'Check if server needs a reboot', { ...Conn },
|
||||||
|
async ({ connection }) => text(await clientFor(connection).exec(
|
||||||
|
'[ -f /var/run/reboot-required ] && cat /var/run/reboot-required || echo "No reboot required"')));
|
||||||
|
|
||||||
|
server.tool('monitor_updates', 'Check available system updates', { ...Conn },
|
||||||
|
async ({ connection }) => text(await clientFor(connection).exec(
|
||||||
|
'apt list --upgradable 2>/dev/null | grep -v Listing | head -30 || echo "Cannot check"')));
|
||||||
|
|
||||||
|
server.tool('monitor_journal', 'Query systemd journal for a service', {
|
||||||
|
...Conn,
|
||||||
|
service: z.string().describe('Service/unit name'),
|
||||||
|
lines: z.number().optional().describe('Lines (default 30)'),
|
||||||
|
priority: z.enum(['emerg', 'alert', 'crit', 'err', 'warning', 'notice', 'info', 'debug']).optional(),
|
||||||
|
}, async ({ connection, service, lines, priority }) => {
|
||||||
|
let cmd = `journalctl -u ${service} --no-pager -n ${lines ?? 30}`;
|
||||||
|
if (priority) cmd += ` -p ${priority}`;
|
||||||
|
return text(await clientFor(connection).exec(cmd));
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── WaaS Heartbeat & Infrastructure ─────────────────────────────────
|
||||||
|
// Note: These tools use SSH exec to run commands on the monitoring server.
|
||||||
|
// All commands are hardcoded strings with no user input interpolation.
|
||||||
|
|
||||||
|
server.tool('monitor_waas_heartbeats', 'List registered MokoWaaS heartbeat provisioning files', { ...Conn },
|
||||||
|
async ({ connection }) => text(await clientFor(connection).exec(
|
||||||
|
'ls -la /opt/gitea-server-setup/docker/monitoring/grafana/provisioning/datasources/mokowaas-*.yml 2>/dev/null || echo "No heartbeat files"')));
|
||||||
|
|
||||||
|
server.tool('monitor_docker_containers', 'List Docker containers with status and ports', { ...Conn },
|
||||||
|
async ({ connection }) => text(await clientFor(connection).exec(
|
||||||
|
'docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" 2>/dev/null || echo "Docker not available"')));
|
||||||
|
|
||||||
|
server.tool('monitor_prometheus_targets', 'List Prometheus target files and entries', { ...Conn },
|
||||||
|
async ({ connection }) => text(await clientFor(connection).exec(
|
||||||
|
'for f in /opt/gitea-server-setup/docker/monitoring/targets/*.json; do echo "=== $(basename $f) ==="; cat "$f" 2>/dev/null; echo; done')));
|
||||||
|
|
||||||
|
server.tool('monitor_nginx_status', 'Get nginx connection and request stats', { ...Conn },
|
||||||
|
async ({ connection }) => text(await clientFor(connection).exec(
|
||||||
|
'curl -s http://127.0.0.1:8888/nginx_status 2>/dev/null || echo "Nginx status not available"')));
|
||||||
|
|
||||||
|
server.tool('monitor_ntfy_status', 'Check ntfy server health', { ...Conn },
|
||||||
|
async ({ connection }) => text(await clientFor(connection).exec(
|
||||||
|
'curl -s http://127.0.0.1:2586/v1/health 2>/dev/null || echo "ntfy not available"')));
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
config = await loadConfig();
|
||||||
|
if (config.grafana?.baseUrl && config.grafana?.apiKey) {
|
||||||
|
grafana = new GrafanaClient(config.grafana);
|
||||||
|
}
|
||||||
|
if (config.sitesJsonPath) {
|
||||||
|
try {
|
||||||
|
sitesConfig = await loadSitesJson(config.sitesJsonPath);
|
||||||
|
} catch { /* sites.json is optional */ }
|
||||||
|
}
|
||||||
|
await server.connect(new StdioServerTransport());
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => { console.error(err); process.exit(1); });
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
export interface MonitorConnection {
|
||||||
|
host: string;
|
||||||
|
port?: number;
|
||||||
|
username: string;
|
||||||
|
keyPath?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GrafanaConfig {
|
||||||
|
baseUrl: string;
|
||||||
|
apiKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MonitorConfig {
|
||||||
|
connections: Record<string, MonitorConnection>;
|
||||||
|
defaultConnection: string;
|
||||||
|
grafana?: GrafanaConfig;
|
||||||
|
sitesJsonPath?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SiteDefinition {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
type: string;
|
||||||
|
client: string;
|
||||||
|
joomlaToken?: string;
|
||||||
|
dolibarrToken?: string;
|
||||||
|
akeebaSecret?: string;
|
||||||
|
tlsVerify?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SitesConfig {
|
||||||
|
grafana?: {
|
||||||
|
baseUrl: string;
|
||||||
|
apiKey: string;
|
||||||
|
dashboardFolder?: string;
|
||||||
|
dashboardFolderUid?: string;
|
||||||
|
};
|
||||||
|
sites: SiteDefinition[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HealthResult {
|
||||||
|
hostname: string;
|
||||||
|
uptime: string;
|
||||||
|
loadAvg: string;
|
||||||
|
memoryUsed: string;
|
||||||
|
memoryTotal: string;
|
||||||
|
diskUsage: string;
|
||||||
|
cpuCount: 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,24 @@
|
|||||||
|
# MCP SSH Manager — Server Configuration
|
||||||
|
# Copy to .env and fill in your values
|
||||||
|
# Auth: use KEYPATH (recommended) or PASSWORD
|
||||||
|
|
||||||
|
# Gitea Server
|
||||||
|
SSH_SERVER_GITEA_HOST=git.mokoconsulting.tech
|
||||||
|
SSH_SERVER_GITEA_USER=root
|
||||||
|
SSH_SERVER_GITEA_KEYPATH=~/.ssh/id_ed25519
|
||||||
|
SSH_SERVER_GITEA_PORT=22
|
||||||
|
SSH_SERVER_GITEA_DEFAULT_DIR=/opt
|
||||||
|
|
||||||
|
# CRM / Dolibarr Server
|
||||||
|
SSH_SERVER_CRM_HOST=crm.mokoconsulting.tech
|
||||||
|
SSH_SERVER_CRM_USER=root
|
||||||
|
SSH_SERVER_CRM_KEYPATH=~/.ssh/id_ed25519
|
||||||
|
SSH_SERVER_CRM_PORT=22
|
||||||
|
SSH_SERVER_CRM_DEFAULT_DIR=/var/www
|
||||||
|
|
||||||
|
# Web Server (Joomla sites)
|
||||||
|
SSH_SERVER_WEB_HOST=web.mokoconsulting.tech
|
||||||
|
SSH_SERVER_WEB_USER=root
|
||||||
|
SSH_SERVER_WEB_KEYPATH=~/.ssh/id_ed25519
|
||||||
|
SSH_SERVER_WEB_PORT=22
|
||||||
|
SSH_SERVER_WEB_DEFAULT_DIR=/var/www
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
###############################################################################
|
||||||
|
# Core normalization
|
||||||
|
###############################################################################
|
||||||
|
* text=auto eol=lf
|
||||||
|
|
||||||
|
# Ensure consistent line endings for scripts
|
||||||
|
*.sh text eol=lf
|
||||||
|
*.bash text eol=lf
|
||||||
|
*.ps1 text eol=lf
|
||||||
|
*.cmd text eol=crlf
|
||||||
|
*.bat text eol=crlf
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Binary handling
|
||||||
|
###############################################################################
|
||||||
|
*.png binary
|
||||||
|
*.jpg binary
|
||||||
|
*.jpeg binary
|
||||||
|
*.gif binary
|
||||||
|
*.svg binary
|
||||||
|
*.ico binary
|
||||||
|
*.pdf binary
|
||||||
|
*.zip binary
|
||||||
|
*.tar binary
|
||||||
|
*.tar.gz binary
|
||||||
|
*.7z binary
|
||||||
|
*.docx binary
|
||||||
|
*.xlsx binary
|
||||||
|
*.pptx binary
|
||||||
|
*.mp3 binary
|
||||||
|
*.mp4 binary
|
||||||
|
*.woff binary
|
||||||
|
*.woff2 binary
|
||||||
|
*.ttf binary
|
||||||
|
*.otf binary
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Export control for GitHub Releases
|
||||||
|
# These paths will NOT appear in generated release archives
|
||||||
|
###############################################################################
|
||||||
|
# CI and automation
|
||||||
|
.github/ export-ignore
|
||||||
|
.github/workflows/ export-ignore
|
||||||
|
.gitea/ export-ignore
|
||||||
|
.gitlab/ export-ignore
|
||||||
|
|
||||||
|
# Development and tooling
|
||||||
|
tests/ export-ignore
|
||||||
|
testing/ export-ignore
|
||||||
|
tmp/ export-ignore
|
||||||
|
docs-internal/ export-ignore
|
||||||
|
tools/ export-ignore
|
||||||
|
|
||||||
|
# Dependency folders that should not ship in release bundles
|
||||||
|
node_modules/ export-ignore
|
||||||
|
vendor-dev/ export-ignore
|
||||||
|
|
||||||
|
# Local environment and editor configs
|
||||||
|
*.local export-ignore
|
||||||
|
*.env export-ignore
|
||||||
|
*.env.example export-ignore
|
||||||
|
*.code-workspace export-ignore
|
||||||
|
|
||||||
|
# Project specific non release scaffolding
|
||||||
|
dev-assets/ export-ignore
|
||||||
|
analysis/ export-ignore
|
||||||
|
research/ export-ignore
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Linguistic settings for code statistics
|
||||||
|
###############################################################################
|
||||||
|
*.css linguist-language=CSS
|
||||||
|
*.scss linguist-language=SCSS
|
||||||
|
*.js linguist-language=JavaScript
|
||||||
|
*.ts linguist-language=TypeScript
|
||||||
|
*.php linguist-language=PHP
|
||||||
|
*.xml linguist-language=XML
|
||||||
|
*.json linguist-language=JSON
|
||||||
|
*.ini linguist-language=INI
|
||||||
|
*.sql linguist-language=SQL
|
||||||
|
*.md linguist-language=Markdown
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Prevent diff noise for vendor or minified content
|
||||||
|
###############################################################################
|
||||||
|
vendor/* -diff
|
||||||
|
*.min.js -diff
|
||||||
|
*.min.css -diff
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Lockdown for generated files
|
||||||
|
###############################################################################
|
||||||
|
*.min.js linguist-generated=true
|
||||||
|
*.min.css linguist-generated=true
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# <type>(<scope>): <subject>
|
||||||
|
# types: build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test
|
||||||
|
# subject: imperative, lower-case, no trailing period
|
||||||
|
|
||||||
|
# Body: what and why
|
||||||
|
|
||||||
|
# BREAKING CHANGE: <description>
|
||||||
|
# Closes: #123
|
||||||
|
# Signed-off-by: <Your Name> <you@example.com>
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
{
|
||||||
|
"pre-deploy": {
|
||||||
|
"enabled": true,
|
||||||
|
"description": "Run before any deployment",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "validation",
|
||||||
|
"name": "check-git-status",
|
||||||
|
"command": "git status --porcelain",
|
||||||
|
"expectEmpty": true,
|
||||||
|
"errorMessage": "Uncommitted changes detected. Please commit or stash changes before deployment."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "validation",
|
||||||
|
"name": "run-tests",
|
||||||
|
"command": "npm test 2>/dev/null || echo \"No tests configured\"",
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"post-deploy": {
|
||||||
|
"enabled": true,
|
||||||
|
"description": "Run after successful deployment",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "notification",
|
||||||
|
"name": "log-deployment",
|
||||||
|
"command": "echo \"[$(date)] Deployment completed to {server}\" >> deployments.log"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "notification",
|
||||||
|
"name": "notify-slack",
|
||||||
|
"command": "curl -X POST -H \"Content-Type: application/json\" -d \"{{\\\"text\\\":\\\"Deployment to {server} completed\\\"}}\" $SLACK_WEBHOOK_URL 2>/dev/null || true",
|
||||||
|
"optional": true,
|
||||||
|
"requiresEnv": [
|
||||||
|
"SLACK_WEBHOOK_URL"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"on-error": {
|
||||||
|
"enabled": true,
|
||||||
|
"description": "Run when an error occurs",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "notification",
|
||||||
|
"name": "log-error",
|
||||||
|
"command": "echo \"[$(date)] Error on {server}: {error}\" >> errors.log"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "recovery",
|
||||||
|
"name": "attempt-recovery",
|
||||||
|
"remoteCommand": "bench restart && bench clear-cache",
|
||||||
|
"server": "{server}",
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pre-bench-update": {
|
||||||
|
"enabled": true,
|
||||||
|
"description": "Run before bench update",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "backup",
|
||||||
|
"name": "backup-database",
|
||||||
|
"remoteCommand": "bench --site all backup --with-files",
|
||||||
|
"server": "{server}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "validation",
|
||||||
|
"name": "check-disk-space",
|
||||||
|
"remoteCommand": "df -h | grep -E \"/$|/home\" | awk '{print $5}' | sed 's/%//' | awk '{if($1 > 80) exit 1}'",
|
||||||
|
"server": "{server}",
|
||||||
|
"errorMessage": "Insufficient disk space (>80% used)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"post-bench-update": {
|
||||||
|
"enabled": true,
|
||||||
|
"description": "Run after bench update",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "verification",
|
||||||
|
"name": "check-services",
|
||||||
|
"remoteCommand": "supervisorctl status | grep -E \"RUNNING|STOPPED\"",
|
||||||
|
"server": "{server}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "verification",
|
||||||
|
"name": "check-site-status",
|
||||||
|
"remoteCommand": "bench --site all doctor",
|
||||||
|
"server": "{server}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pre-connect": {
|
||||||
|
"enabled": false,
|
||||||
|
"description": "Run before connecting to a server",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "validation",
|
||||||
|
"name": "check-vpn",
|
||||||
|
"command": "ping -c 1 -W 2 10.0.0.1 > /dev/null 2>&1",
|
||||||
|
"errorMessage": "VPN not connected",
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"post-connect": {
|
||||||
|
"enabled": false,
|
||||||
|
"description": "Run after successful connection",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "notification",
|
||||||
|
"name": "log-connection",
|
||||||
|
"command": "echo \"[$(date)] Connected to {server}\" >> connections.log"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"context-test": {
|
||||||
|
"enabled": true,
|
||||||
|
"description": "Context replacement test",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "notification",
|
||||||
|
"name": "use-context",
|
||||||
|
"command": "echo \"Server: {server}, Error: {error}\" > /Users/jeremy/mcp/mcp-ssh-manager/context-test.txt"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
platform: mcp-server
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# mcp_mokossh
|
||||||
|
|
||||||
|
MCP server for SSH remote server management — execute commands, transfer files, database operations, backups, health monitoring, and DevOps automation.
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Package** | `@mokoconsulting/mcp-mokossh` v3.4 |
|
||||||
|
| **Entry** | `src/index.js` (plain JS, no build step) |
|
||||||
|
| **Config** | `.env` file in project root (env vars) |
|
||||||
|
| **Language** | JavaScript (Node.js) |
|
||||||
|
| **Branch** | develop on `dev`, merge to `main` (protected) |
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install # Install dependencies
|
||||||
|
npm start # Start MCP server (requires stdin)
|
||||||
|
node --check src/index.js # Syntax check
|
||||||
|
```
|
||||||
|
|
||||||
|
No build step — this MCP runs plain JavaScript directly.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── index.js # MCP server entry, tool registration, connection pooling
|
||||||
|
├── config.js # Output limits, timeouts
|
||||||
|
├── config-loader.js # .env and TOML config loading
|
||||||
|
├── server-aliases.js # Server name alias resolution
|
||||||
|
├── server-groups.js # Group operations across servers
|
||||||
|
├── session-manager.js # Persistent SSH sessions
|
||||||
|
├── deploy-helper.js # Deployment with permission handling
|
||||||
|
├── backup-manager.js # Database/file backup operations
|
||||||
|
├── database-manager.js # DB dump, import, query, list
|
||||||
|
├── health-monitor.js # CPU, RAM, disk, network checks
|
||||||
|
├── hooks-system.js # Automation hooks
|
||||||
|
├── ssh-key-manager.js # SSH key management
|
||||||
|
├── profile-loader.js # Profile management
|
||||||
|
├── logger.js # Logging
|
||||||
|
└── command-aliases.js # Command shortcut management
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Connection pooling**: maintains persistent SSH connections in a Map
|
||||||
|
- **Server resolution**: aliases first, then direct lookup, normalized to lowercase
|
||||||
|
- **37 tools** organized in groups: core (5), sessions (4), monitoring (6), backup (4), database (4), advanced (14)
|
||||||
|
|
||||||
|
## Servers Connected
|
||||||
|
|
||||||
|
| Name | Host | User | Port |
|
||||||
|
|---|---|---|---|
|
||||||
|
| GIT | git.mokoconsulting.tech | mokoconsulting | 2918 |
|
||||||
|
| WAAS_DEV | waas.dev.mokoconsulting.tech | mokoconsulting_dev | 22 |
|
||||||
|
| WAAS_DEMO | waas.demo.mokoconsulting.tech | mokoconsulting_demo | 22 |
|
||||||
|
| WAAS_LIVE | mokoconsulting.tech | mokoconsulting | 22 |
|
||||||
|
| CRM_DEV | waas.dev.mokoconsulting.tech | mokoconsulting_dev | 22 |
|
||||||
|
| CRM_LIVE | crm.mokoconsulting.tech | mokoconsulting_crm | 22 |
|
||||||
|
|
||||||
|
SSH key: `jmiller_private.openssh`
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- **Never commit** `.env`, `.claude/`, `.mcp.json`, `TODO.md`
|
||||||
|
- **Attribution**: `Authored-by: Moko Consulting`
|
||||||
|
- **Workflow directory**: `.mokogitea/` (not `.gitea/` or `.github/`)
|
||||||
|
- **Standards**: [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
---
|
||||||
|
name: Architecture Decision Record (ADR)
|
||||||
|
about: Propose or document an architectural decision
|
||||||
|
title: '[ADR] '
|
||||||
|
labels: 'architecture, decision'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## ADR Number
|
||||||
|
ADR-XXXX
|
||||||
|
|
||||||
|
## Status
|
||||||
|
- [ ] Proposed
|
||||||
|
- [ ] Accepted
|
||||||
|
- [ ] Deprecated
|
||||||
|
- [ ] Superseded by ADR-XXXX
|
||||||
|
|
||||||
|
## Context
|
||||||
|
Describe the issue or problem that motivates this decision.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
State the architecture decision and provide rationale.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
### Positive
|
||||||
|
- List positive consequences
|
||||||
|
|
||||||
|
### Negative
|
||||||
|
- List negative consequences or trade-offs
|
||||||
|
|
||||||
|
### Neutral
|
||||||
|
- List neutral aspects
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
### Alternative 1
|
||||||
|
- Description
|
||||||
|
- Pros
|
||||||
|
- Cons
|
||||||
|
- Why not chosen
|
||||||
|
|
||||||
|
### Alternative 2
|
||||||
|
- Description
|
||||||
|
- Pros
|
||||||
|
- Cons
|
||||||
|
- Why not chosen
|
||||||
|
|
||||||
|
## Implementation Plan
|
||||||
|
1. Step 1
|
||||||
|
2. Step 2
|
||||||
|
3. Step 3
|
||||||
|
|
||||||
|
## Stakeholders
|
||||||
|
- **Decision Makers**: @user1, @user2
|
||||||
|
- **Consulted**: @user3, @user4
|
||||||
|
- **Informed**: team-name
|
||||||
|
|
||||||
|
## Technical Details
|
||||||
|
### Architecture Diagram
|
||||||
|
```
|
||||||
|
[Add diagram or link]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
- Dependency 1
|
||||||
|
- Dependency 2
|
||||||
|
|
||||||
|
### Impact Analysis
|
||||||
|
- **Performance**: [Impact description]
|
||||||
|
- **Security**: [Impact description]
|
||||||
|
- **Scalability**: [Impact description]
|
||||||
|
- **Maintainability**: [Impact description]
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
- [ ] Unit tests
|
||||||
|
- [ ] Integration tests
|
||||||
|
- [ ] Performance tests
|
||||||
|
- [ ] Security tests
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
- [ ] Architecture documentation updated
|
||||||
|
- [ ] API documentation updated
|
||||||
|
- [ ] Developer guide updated
|
||||||
|
- [ ] Runbook created
|
||||||
|
|
||||||
|
## Migration Path
|
||||||
|
Describe how to migrate from current state to new architecture.
|
||||||
|
|
||||||
|
## Rollback Plan
|
||||||
|
Describe how to rollback if issues occur.
|
||||||
|
|
||||||
|
## Timeline
|
||||||
|
- **Proposal Date**:
|
||||||
|
- **Decision Date**:
|
||||||
|
- **Implementation Start**:
|
||||||
|
- **Expected Completion**:
|
||||||
|
|
||||||
|
## References
|
||||||
|
- Related ADRs:
|
||||||
|
- External resources:
|
||||||
|
- RFCs:
|
||||||
|
|
||||||
|
## Review Checklist
|
||||||
|
- [ ] Aligns with enterprise architecture principles
|
||||||
|
- [ ] Security implications reviewed
|
||||||
|
- [ ] Performance implications reviewed
|
||||||
|
- [ ] Cost implications reviewed
|
||||||
|
- [ ] Compliance requirements met
|
||||||
|
- [ ] Team consensus achieved
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
---
|
||||||
|
name: Bug Report
|
||||||
|
about: Report a bug or issue with the project
|
||||||
|
title: '[BUG] '
|
||||||
|
labels: 'bug'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## Bug Description
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
## Steps to Reproduce
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '...'
|
||||||
|
3. Scroll down to '...'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
## Actual Behavior
|
||||||
|
A clear and concise description of what actually happened.
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
## Environment
|
||||||
|
- **Project**: [e.g., MokoDoliTools, moko-cassiopeia]
|
||||||
|
- **Version**: [e.g., 1.2.3]
|
||||||
|
- **Platform**: [e.g., Dolibarr 18.0, Joomla 5.0]
|
||||||
|
- **PHP Version**: [e.g., 8.1]
|
||||||
|
- **Database**: [e.g., MySQL 8.0, PostgreSQL 14]
|
||||||
|
- **Browser** (if applicable): [e.g., Chrome 120, Firefox 121]
|
||||||
|
- **OS**: [e.g., Ubuntu 22.04, Windows 11]
|
||||||
|
|
||||||
|
## Additional Context
|
||||||
|
Add any other context about the problem here.
|
||||||
|
|
||||||
|
## Possible Solution
|
||||||
|
If you have suggestions on how to fix the issue, please describe them here.
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
- [ ] I have searched for similar issues before creating this one
|
||||||
|
- [ ] I have provided all the requested information
|
||||||
|
- [ ] I have tested this on the latest stable version
|
||||||
|
- [ ] I have checked the documentation and couldn't find a solution
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
---
|
||||||
|
blank_issues_enabled: true
|
||||||
|
contact_links:
|
||||||
|
- name: 💼 Enterprise Support
|
||||||
|
url: https://mokoconsulting.tech/enterprise
|
||||||
|
about: Enterprise-level support and consultation services
|
||||||
|
- name: 💬 Ask a Question
|
||||||
|
url: https://mokoconsulting.tech/
|
||||||
|
about: Get help or ask questions through our website
|
||||||
|
- name: 📚 MokoStandards Documentation
|
||||||
|
url: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
|
about: View our coding standards and best practices
|
||||||
|
- name: 🔒 Report a Security Vulnerability
|
||||||
|
url: https://git.mokoconsulting.tech/mokoconsulting-tech/.github-private/security/advisories/new
|
||||||
|
about: Report security vulnerabilities privately (for critical issues)
|
||||||
|
- name: 💡 Community Discussions
|
||||||
|
url: https://github.com/orgs/mokoconsulting-tech/discussions
|
||||||
|
about: Join community discussions and Q&A
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
---
|
||||||
|
name: Documentation Issue
|
||||||
|
about: Report an issue with documentation
|
||||||
|
title: '[DOCS] '
|
||||||
|
labels: 'documentation'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## Documentation Issue
|
||||||
|
|
||||||
|
**Location**:
|
||||||
|
<!-- Specify the file, page, or section with the issue -->
|
||||||
|
|
||||||
|
## Issue Type
|
||||||
|
<!-- Mark the relevant option with an "x" -->
|
||||||
|
- [ ] Typo or grammar error
|
||||||
|
- [ ] Outdated information
|
||||||
|
- [ ] Missing documentation
|
||||||
|
- [ ] Unclear explanation
|
||||||
|
- [ ] Broken links
|
||||||
|
- [ ] Missing examples
|
||||||
|
- [ ] Other (specify below)
|
||||||
|
|
||||||
|
## Description
|
||||||
|
<!-- Clearly describe the documentation issue -->
|
||||||
|
|
||||||
|
## Current Content
|
||||||
|
<!-- Quote or describe the current documentation (if applicable) -->
|
||||||
|
```
|
||||||
|
Current text here
|
||||||
|
```
|
||||||
|
|
||||||
|
## Suggested Improvement
|
||||||
|
<!-- Provide your suggestion for how to improve the documentation -->
|
||||||
|
```
|
||||||
|
Suggested text here
|
||||||
|
```
|
||||||
|
|
||||||
|
## Additional Context
|
||||||
|
<!-- Add any other context, screenshots, or references -->
|
||||||
|
|
||||||
|
## Standards Alignment
|
||||||
|
- [ ] Follows MokoStandards documentation guidelines
|
||||||
|
- [ ] Uses en_US/en_GB localization
|
||||||
|
- [ ] Includes proper SPDX headers where applicable
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
- [ ] I have searched for similar documentation issues
|
||||||
|
- [ ] I have provided a clear description
|
||||||
|
- [ ] I have suggested an improvement (if applicable)
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
---
|
||||||
|
name: Enterprise Support Request
|
||||||
|
about: Request enterprise-level support or consultation
|
||||||
|
title: '[ENTERPRISE] '
|
||||||
|
labels: 'enterprise, support'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## Support Request Type
|
||||||
|
- [ ] Critical Production Issue
|
||||||
|
- [ ] Performance Optimization
|
||||||
|
- [ ] Security Audit
|
||||||
|
- [ ] Architecture Review
|
||||||
|
- [ ] Custom Development
|
||||||
|
- [ ] Migration Support
|
||||||
|
- [ ] Training & Onboarding
|
||||||
|
- [ ] Other (please specify)
|
||||||
|
|
||||||
|
## Priority Level
|
||||||
|
- [ ] P0 - Critical (Production Down)
|
||||||
|
- [ ] P1 - High (Major Feature Broken)
|
||||||
|
- [ ] P2 - Medium (Non-Critical Issue)
|
||||||
|
- [ ] P3 - Low (Enhancement/Question)
|
||||||
|
|
||||||
|
## Organization Details
|
||||||
|
- **Company Name**:
|
||||||
|
- **Contact Person**:
|
||||||
|
- **Email**:
|
||||||
|
- **Phone** (for P0/P1 issues):
|
||||||
|
- **Timezone**:
|
||||||
|
|
||||||
|
## Issue Description
|
||||||
|
Provide a clear and detailed description of your request or issue.
|
||||||
|
|
||||||
|
## Business Impact
|
||||||
|
Describe the impact on your business operations:
|
||||||
|
- Number of users affected:
|
||||||
|
- Revenue impact (if applicable):
|
||||||
|
- Deadline/SLA requirements:
|
||||||
|
|
||||||
|
## Environment Details
|
||||||
|
- **Deployment Type**: [On-Premise / Cloud / Hybrid]
|
||||||
|
- **Platform**: [Joomla / Dolibarr / Custom]
|
||||||
|
- **Version**:
|
||||||
|
- **Infrastructure**: [AWS / Azure / GCP / Other]
|
||||||
|
- **Scale**: [Users / Transactions / Data Volume]
|
||||||
|
|
||||||
|
## Current Configuration
|
||||||
|
```yaml
|
||||||
|
# Paste relevant configuration (sanitize sensitive data)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Logs and Diagnostics
|
||||||
|
```
|
||||||
|
# Paste relevant logs (sanitize sensitive data)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Attempted Solutions
|
||||||
|
Describe any troubleshooting steps already taken.
|
||||||
|
|
||||||
|
## Expected Resolution
|
||||||
|
Describe your expected outcome or resolution.
|
||||||
|
|
||||||
|
## Additional Resources
|
||||||
|
- **Documentation Links**:
|
||||||
|
- **Related Issues**:
|
||||||
|
- **Screenshots/Videos**:
|
||||||
|
|
||||||
|
## Enterprise SLA
|
||||||
|
- [ ] Standard Support (initial response within 1–3 weeks)
|
||||||
|
- [ ] Premium Support (initial response within 5 business days)
|
||||||
|
- [ ] Critical Support (initial response within 72 hours)
|
||||||
|
- [ ] Custom SLA (specify):
|
||||||
|
|
||||||
|
## Compliance Requirements
|
||||||
|
- [ ] GDPR
|
||||||
|
- [ ] HIPAA
|
||||||
|
- [ ] SOC 2
|
||||||
|
- [ ] ISO 27001
|
||||||
|
- [ ] Other (specify):
|
||||||
|
|
||||||
|
---
|
||||||
|
**Note**: Enterprise support requests require an active support contract. If you don't have one, please contact us at enterprise@mokoconsulting.tech
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
---
|
||||||
|
name: Feature Request
|
||||||
|
about: Suggest a new feature or enhancement
|
||||||
|
title: '[FEATURE] '
|
||||||
|
labels: 'enhancement'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## Feature Description
|
||||||
|
A clear and concise description of the feature you'd like to see.
|
||||||
|
|
||||||
|
## Problem or Use Case
|
||||||
|
Describe the problem this feature would solve or the use case it addresses.
|
||||||
|
Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
## Proposed Solution
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
## Alternative Solutions
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
Describe how this feature would benefit users:
|
||||||
|
- Who would use this feature?
|
||||||
|
- What problems does it solve?
|
||||||
|
- What value does it add?
|
||||||
|
|
||||||
|
## Implementation Details (Optional)
|
||||||
|
If you have ideas about how this could be implemented, share them here:
|
||||||
|
- Technical approach
|
||||||
|
- Files/components that might need changes
|
||||||
|
- Any concerns or challenges you foresee
|
||||||
|
|
||||||
|
## Additional Context
|
||||||
|
Add any other context, mockups, or screenshots about the feature request here.
|
||||||
|
|
||||||
|
## Relevant Standards
|
||||||
|
Does this relate to any standards in [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards)?
|
||||||
|
- [ ] Accessibility (WCAG 2.1 AA)
|
||||||
|
- [ ] Localization (en_US/en_GB)
|
||||||
|
- [ ] Security best practices
|
||||||
|
- [ ] Code quality standards
|
||||||
|
- [ ] Other: [specify]
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
- [ ] I have searched for similar feature requests before creating this one
|
||||||
|
- [ ] I have clearly described the use case and benefits
|
||||||
|
- [ ] I have considered alternative solutions
|
||||||
|
- [ ] This feature aligns with the project's goals and scope
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
---
|
||||||
|
name: Firewall Request
|
||||||
|
about: Request firewall rule changes or access to external resources
|
||||||
|
title: '[FIREWALL] [Resource Name] - [Brief Description]'
|
||||||
|
labels: ['firewall-request', 'infrastructure', 'security']
|
||||||
|
assignees: ['jmiller']
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## Firewall Request
|
||||||
|
|
||||||
|
### Request Type
|
||||||
|
- [ ] Allow outbound access to external service/API
|
||||||
|
- [ ] Allow inbound access from external source
|
||||||
|
- [ ] Modify existing firewall rule
|
||||||
|
- [ ] Remove/revoke firewall rule
|
||||||
|
- [ ] Other (specify):
|
||||||
|
|
||||||
|
### Resource Information
|
||||||
|
**Service/Domain Name**:
|
||||||
|
**IP Address(es)**:
|
||||||
|
**Port(s)**:
|
||||||
|
**Protocol**:
|
||||||
|
- [ ] HTTP (80)
|
||||||
|
- [ ] HTTPS (443)
|
||||||
|
- [ ] SSH (22)
|
||||||
|
- [ ] FTP (21)
|
||||||
|
- [ ] SFTP (22)
|
||||||
|
- [ ] Custom (specify): _______________
|
||||||
|
|
||||||
|
### Requestor Information
|
||||||
|
**Name**:
|
||||||
|
**GitHub Username**: @
|
||||||
|
**Email**: @mokoconsulting.tech
|
||||||
|
**Team/Department**:
|
||||||
|
**Manager**: @
|
||||||
|
|
||||||
|
### Business Justification
|
||||||
|
**Why is this access needed?**
|
||||||
|
|
||||||
|
**Which project(s) require this access?**
|
||||||
|
|
||||||
|
**What functionality will break without this access?**
|
||||||
|
|
||||||
|
**Is there an alternative solution?**
|
||||||
|
- [ ] Yes (explain):
|
||||||
|
- [ ] No
|
||||||
|
|
||||||
|
### Security Considerations
|
||||||
|
**Data Classification**:
|
||||||
|
- [ ] Public
|
||||||
|
- [ ] Internal
|
||||||
|
- [ ] Confidential
|
||||||
|
- [ ] Restricted
|
||||||
|
|
||||||
|
**Sensitive Data Transmission**:
|
||||||
|
- [ ] No sensitive data will be transmitted
|
||||||
|
- [ ] Sensitive data will be transmitted (encryption required)
|
||||||
|
- [ ] Authentication credentials will be transmitted (secure storage required)
|
||||||
|
|
||||||
|
**Third-Party Service**:
|
||||||
|
- [ ] This is a trusted/verified third-party service
|
||||||
|
- [ ] This is a new/unverified service (security review required)
|
||||||
|
|
||||||
|
**Service Documentation**:
|
||||||
|
(Provide link to service documentation or API specs)
|
||||||
|
|
||||||
|
### Access Scope
|
||||||
|
**Affected Systems**:
|
||||||
|
- [ ] Development environment only
|
||||||
|
- [ ] Staging environment only
|
||||||
|
- [ ] Production environment
|
||||||
|
- [ ] All environments
|
||||||
|
|
||||||
|
**Access Duration**:
|
||||||
|
- [ ] Permanent (ongoing business need)
|
||||||
|
- [ ] Temporary (specify end date): _______________
|
||||||
|
- [ ] Testing only (specify duration): _______________
|
||||||
|
|
||||||
|
### Technical Details
|
||||||
|
**Source System(s)**:
|
||||||
|
(Which internal systems need access?)
|
||||||
|
|
||||||
|
**Destination System(s)**:
|
||||||
|
(Which external systems need to be accessed?)
|
||||||
|
|
||||||
|
**Expected Traffic Volume**:
|
||||||
|
(e.g., requests per hour/day)
|
||||||
|
|
||||||
|
**Traffic Pattern**:
|
||||||
|
- [ ] Continuous
|
||||||
|
- [ ] Periodic (specify frequency): _______________
|
||||||
|
- [ ] On-demand/manual
|
||||||
|
- [ ] Scheduled (specify schedule): _______________
|
||||||
|
|
||||||
|
### Testing Requirements
|
||||||
|
**Pre-Production Testing**:
|
||||||
|
- [ ] Request includes dev/staging access for testing
|
||||||
|
- [ ] Testing can be done with production access only
|
||||||
|
- [ ] No testing required (modify existing rule)
|
||||||
|
|
||||||
|
**Testing Plan**:
|
||||||
|
|
||||||
|
**Rollback Plan**:
|
||||||
|
(What happens if access needs to be revoked?)
|
||||||
|
|
||||||
|
### Compliance & Audit
|
||||||
|
**Compliance Requirements**:
|
||||||
|
- [ ] GDPR considerations
|
||||||
|
- [ ] SOC 2 compliance required
|
||||||
|
- [ ] PCI DSS considerations
|
||||||
|
- [ ] Other regulatory requirements: _______________
|
||||||
|
- [ ] No specific compliance requirements
|
||||||
|
|
||||||
|
**Audit/Logging Requirements**:
|
||||||
|
- [ ] Standard logging sufficient
|
||||||
|
- [ ] Enhanced logging/monitoring required
|
||||||
|
- [ ] Real-time alerting required
|
||||||
|
|
||||||
|
### Urgency
|
||||||
|
- [ ] Critical (production down, immediate access needed)
|
||||||
|
- [ ] High (needed within 24 hours)
|
||||||
|
- [ ] Normal (needed within 1 week)
|
||||||
|
- [ ] Low priority (needed within 1 month)
|
||||||
|
|
||||||
|
**If critical/high urgency, explain why:**
|
||||||
|
|
||||||
|
### Approvals
|
||||||
|
**Manager Approval**:
|
||||||
|
- [ ] Manager has been notified and approves this request
|
||||||
|
|
||||||
|
**Security Team Review Required**:
|
||||||
|
- [ ] Yes (new external service, sensitive data)
|
||||||
|
- [ ] No (minor change, established service)
|
||||||
|
|
||||||
|
### Additional Information
|
||||||
|
|
||||||
|
**Related Documentation**:
|
||||||
|
(Links to relevant docs, RFCs, tickets, etc.)
|
||||||
|
|
||||||
|
**Dependencies**:
|
||||||
|
(Other systems or changes this depends on)
|
||||||
|
|
||||||
|
**Comments/Questions**:
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## For Infrastructure/Security Team Use Only
|
||||||
|
|
||||||
|
**Do not edit below this line**
|
||||||
|
|
||||||
|
### Security Review
|
||||||
|
- [ ] Security team review completed
|
||||||
|
- [ ] Risk assessment: Low / Medium / High
|
||||||
|
- [ ] Encryption required: Yes / No
|
||||||
|
- [ ] VPN required: Yes / No
|
||||||
|
- [ ] Additional security controls: _______________
|
||||||
|
|
||||||
|
**Reviewed By**: @_______________
|
||||||
|
**Review Date**: _______________
|
||||||
|
**Review Notes**:
|
||||||
|
|
||||||
|
### Implementation
|
||||||
|
- [ ] Firewall rule created/modified
|
||||||
|
- [ ] Rule tested in dev/staging
|
||||||
|
- [ ] Rule deployed to production
|
||||||
|
- [ ] Monitoring/alerting configured
|
||||||
|
- [ ] Documentation updated
|
||||||
|
|
||||||
|
**Firewall Rule ID**: _______________
|
||||||
|
**Implementation Date**: _______________
|
||||||
|
**Implemented By**: @_______________
|
||||||
|
|
||||||
|
**Configuration Details**:
|
||||||
|
```
|
||||||
|
Source:
|
||||||
|
Destination:
|
||||||
|
Port/Protocol:
|
||||||
|
Action: Allow/Deny
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verification
|
||||||
|
- [ ] Requestor confirmed access working
|
||||||
|
- [ ] Logs reviewed (no anomalies)
|
||||||
|
- [ ] Security scan completed (if applicable)
|
||||||
|
|
||||||
|
**Verification Date**: _______________
|
||||||
|
**Verified By**: @_______________
|
||||||
|
|
||||||
|
### Notes
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
---
|
||||||
|
name: API Integration Request
|
||||||
|
about: Request integration with a new REST API or service
|
||||||
|
title: '[API] '
|
||||||
|
labels: 'enhancement, api-integration'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API Integration Request
|
||||||
|
|
||||||
|
### Target API
|
||||||
|
- **Service Name**: [e.g., Akeeba Backup, Joomla Web Services]
|
||||||
|
- **API Documentation**: [URL to API docs]
|
||||||
|
- **API Type**: [REST / GraphQL / SOAP]
|
||||||
|
- **Authentication**: [API Key / OAuth / Bearer Token / Basic Auth]
|
||||||
|
|
||||||
|
### Proposed Tools
|
||||||
|
List the MCP tools this integration would provide:
|
||||||
|
|
||||||
|
| Tool Name | HTTP Method | Endpoint | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `service_list` | GET | `/api/items` | List all items |
|
||||||
|
| `service_get` | GET | `/api/items/{id}` | Get single item |
|
||||||
|
| `service_create` | POST | `/api/items` | Create item |
|
||||||
|
|
||||||
|
### Multi-Connection
|
||||||
|
- [ ] Single instance only
|
||||||
|
- [ ] Multiple instances (production, staging, dev)
|
||||||
|
- [ ] Multi-tenant (one connection per client)
|
||||||
|
|
||||||
|
### Use Case
|
||||||
|
Describe the workflow this integration enables for AI assistants.
|
||||||
|
|
||||||
|
### Priority
|
||||||
|
- [ ] Critical — blocking current work
|
||||||
|
- [ ] High — needed soon
|
||||||
|
- [ ] Medium — would improve workflow
|
||||||
|
- [ ] Low — nice to have
|
||||||
|
|
||||||
|
### Existing Alternatives
|
||||||
|
Are there other ways to accomplish this today? If so, why is an MCP integration better?
|
||||||
|
|
||||||
|
### Checklist
|
||||||
|
- [ ] API documentation is available and accessible
|
||||||
|
- [ ] API supports the required authentication method
|
||||||
|
- [ ] I have tested the API endpoints manually
|
||||||
|
- [ ] The integration follows the Template-MCP architecture pattern
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
---
|
||||||
|
name: MCP Connection Issue
|
||||||
|
about: Report a connection, authentication, or API communication issue
|
||||||
|
title: '[CONNECTION] '
|
||||||
|
labels: 'bug, mcp-connection'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Connection Issue
|
||||||
|
|
||||||
|
### Issue Type
|
||||||
|
- [ ] Authentication failure (401/403)
|
||||||
|
- [ ] Connection refused / timeout
|
||||||
|
- [ ] TLS / SSL certificate error
|
||||||
|
- [ ] Wrong connection used (wrong environment)
|
||||||
|
- [ ] Config file not found / parse error
|
||||||
|
- [ ] API response error (4xx / 5xx)
|
||||||
|
|
||||||
|
### MCP Server
|
||||||
|
- **Server Name**: [e.g., mcp_mokowaas]
|
||||||
|
- **Server Version**: [e.g., 1.0.0]
|
||||||
|
- **Node.js Version**: [e.g., 20.x]
|
||||||
|
|
||||||
|
### Connection Details
|
||||||
|
- **Connection Name**: [e.g., production, staging, default]
|
||||||
|
- **API Base URL**: [e.g., https://api.example.com] *(do not include API keys)*
|
||||||
|
- **Insecure Mode**: [Yes / No]
|
||||||
|
|
||||||
|
### Error Message
|
||||||
|
```
|
||||||
|
Paste the exact error message here
|
||||||
|
```
|
||||||
|
|
||||||
|
### Steps to Reproduce
|
||||||
|
1. Configure connection with `npm run setup`
|
||||||
|
2. Call tool `...` with parameters `...`
|
||||||
|
3. See error
|
||||||
|
|
||||||
|
### Expected Behavior
|
||||||
|
What should have happened.
|
||||||
|
|
||||||
|
### Debugging Attempted
|
||||||
|
- [ ] Tested API directly with curl
|
||||||
|
- [ ] Verified API key is valid
|
||||||
|
- [ ] Checked config file exists and is valid JSON
|
||||||
|
- [ ] Tested with `list_connections` tool
|
||||||
|
- [ ] Ran server manually: `node dist/index.js 2> debug.log`
|
||||||
|
|
||||||
|
### Config File
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"defaultConnection": "...",
|
||||||
|
"connections": {
|
||||||
|
"connection_name": {
|
||||||
|
"baseUrl": "https://...",
|
||||||
|
"apiKey": "REDACTED"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
*(Redact all API keys and tokens)*
|
||||||
|
|
||||||
|
### Environment
|
||||||
|
- **OS**: [e.g., macOS 14, Ubuntu 22.04, Windows 11]
|
||||||
|
- **Claude Code Version**: [e.g., latest]
|
||||||
|
- **Registration**: [.mcp.json / ~/.claude.json]
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
---
|
||||||
|
name: New MCP Tool Request
|
||||||
|
about: Request a new tool to be added to this MCP server
|
||||||
|
title: '[TOOL] '
|
||||||
|
labels: 'enhancement, mcp-tool'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tool Request
|
||||||
|
|
||||||
|
### Tool Name
|
||||||
|
Proposed tool name (snake_case): `resource_action`
|
||||||
|
|
||||||
|
### Description
|
||||||
|
What should this tool do? What API endpoint(s) does it map to?
|
||||||
|
|
||||||
|
### API Endpoint(s)
|
||||||
|
- **Method**: [GET / POST / PUT / PATCH / DELETE]
|
||||||
|
- **Endpoint**: `/api/v1/...`
|
||||||
|
- **Auth**: [API Key / Token / None]
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `id` | number | Yes | Resource ID |
|
||||||
|
| `search` | string | No | Search filter |
|
||||||
|
|
||||||
|
### Expected Response
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Example"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Use Case
|
||||||
|
Describe when and why someone would use this tool from Claude or another AI assistant.
|
||||||
|
|
||||||
|
### Connection Scope
|
||||||
|
- [ ] Works with all connections
|
||||||
|
- [ ] Specific to certain API versions
|
||||||
|
- [ ] Requires additional permissions
|
||||||
|
|
||||||
|
### Checklist
|
||||||
|
- [ ] I have checked this tool does not already exist
|
||||||
|
- [ ] I have verified the API endpoint exists and is documented
|
||||||
|
- [ ] The proposed name follows the `resource_action` convention
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
---
|
||||||
|
name: Question
|
||||||
|
about: Ask a question about usage, features, or best practices
|
||||||
|
title: '[QUESTION] '
|
||||||
|
labels: ['question']
|
||||||
|
assignees: ['jmiller']
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## Question
|
||||||
|
|
||||||
|
**Your question:**
|
||||||
|
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
**What are you trying to accomplish?**
|
||||||
|
|
||||||
|
|
||||||
|
**What have you already tried?**
|
||||||
|
|
||||||
|
|
||||||
|
**Category**:
|
||||||
|
- [ ] Script usage
|
||||||
|
- [ ] Configuration
|
||||||
|
- [ ] Workflow setup
|
||||||
|
- [ ] Documentation interpretation
|
||||||
|
- [ ] Best practices
|
||||||
|
- [ ] Integration
|
||||||
|
- [ ] Other: __________
|
||||||
|
|
||||||
|
## Environment (if relevant)
|
||||||
|
|
||||||
|
**Your setup**:
|
||||||
|
- Operating System:
|
||||||
|
- Version:
|
||||||
|
|
||||||
|
## What You've Researched
|
||||||
|
|
||||||
|
**Documentation reviewed**:
|
||||||
|
- [ ] README.md
|
||||||
|
- [ ] Project documentation
|
||||||
|
- [ ] Other (specify): __________
|
||||||
|
|
||||||
|
**Similar issues/questions found**:
|
||||||
|
- #
|
||||||
|
- #
|
||||||
|
|
||||||
|
## Expected Outcome
|
||||||
|
|
||||||
|
**What result are you hoping for?**
|
||||||
|
|
||||||
|
|
||||||
|
## Code/Configuration Samples
|
||||||
|
|
||||||
|
**Relevant code or configuration** (if applicable):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Your code here
|
||||||
|
```
|
||||||
|
|
||||||
|
## Additional Context
|
||||||
|
|
||||||
|
**Any other relevant information:**
|
||||||
|
|
||||||
|
|
||||||
|
**Screenshots** (if helpful):
|
||||||
|
|
||||||
|
|
||||||
|
## Urgency
|
||||||
|
|
||||||
|
- [ ] Urgent (blocking work)
|
||||||
|
- [ ] Normal (can work on other things meanwhile)
|
||||||
|
- [ ] Low priority (just curious)
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
- [ ] I have searched existing issues and discussions
|
||||||
|
- [ ] I have reviewed relevant documentation
|
||||||
|
- [ ] I have provided sufficient context
|
||||||
|
- [ ] I have included code/configuration samples if relevant
|
||||||
|
- [ ] This is a genuine question (not a bug report or feature request)
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
---
|
||||||
|
name: Request for Comments (RFC)
|
||||||
|
about: Propose a significant change for community discussion
|
||||||
|
title: '[RFC] '
|
||||||
|
labels: 'rfc, discussion'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## RFC Summary
|
||||||
|
One-paragraph summary of the proposal.
|
||||||
|
|
||||||
|
## Motivation
|
||||||
|
Why are we doing this? What use cases does it support? What is the expected outcome?
|
||||||
|
|
||||||
|
## Detailed Design
|
||||||
|
### Overview
|
||||||
|
Provide a detailed explanation of the proposed change.
|
||||||
|
|
||||||
|
### API Changes (if applicable)
|
||||||
|
```php
|
||||||
|
// Before
|
||||||
|
function oldApi($param1) { }
|
||||||
|
|
||||||
|
// After
|
||||||
|
function newApi($param1, $param2) { }
|
||||||
|
```
|
||||||
|
|
||||||
|
### User Experience Changes
|
||||||
|
Describe how users will interact with this change.
|
||||||
|
|
||||||
|
### Implementation Approach
|
||||||
|
High-level implementation strategy.
|
||||||
|
|
||||||
|
## Drawbacks
|
||||||
|
Why should we *not* do this?
|
||||||
|
|
||||||
|
## Alternatives
|
||||||
|
What other designs have been considered? What is the impact of not doing this?
|
||||||
|
|
||||||
|
### Alternative 1
|
||||||
|
- Description
|
||||||
|
- Trade-offs
|
||||||
|
|
||||||
|
### Alternative 2
|
||||||
|
- Description
|
||||||
|
- Trade-offs
|
||||||
|
|
||||||
|
## Adoption Strategy
|
||||||
|
How will existing users adopt this? Is this a breaking change?
|
||||||
|
|
||||||
|
### Migration Guide
|
||||||
|
```bash
|
||||||
|
# Steps to migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deprecation Timeline
|
||||||
|
- **Announcement**:
|
||||||
|
- **Deprecation**:
|
||||||
|
- **Removal**:
|
||||||
|
|
||||||
|
## Unresolved Questions
|
||||||
|
- Question 1
|
||||||
|
- Question 2
|
||||||
|
|
||||||
|
## Future Possibilities
|
||||||
|
What future work does this enable?
|
||||||
|
|
||||||
|
## Impact Assessment
|
||||||
|
### Performance
|
||||||
|
Expected performance impact.
|
||||||
|
|
||||||
|
### Security
|
||||||
|
Security considerations and implications.
|
||||||
|
|
||||||
|
### Compatibility
|
||||||
|
- **Backward Compatible**: [Yes / No]
|
||||||
|
- **Breaking Changes**: [List]
|
||||||
|
|
||||||
|
### Maintenance
|
||||||
|
Long-term maintenance considerations.
|
||||||
|
|
||||||
|
## Community Input
|
||||||
|
### Stakeholders
|
||||||
|
- [ ] Core team
|
||||||
|
- [ ] Module developers
|
||||||
|
- [ ] End users
|
||||||
|
- [ ] Enterprise customers
|
||||||
|
|
||||||
|
### Feedback Period
|
||||||
|
**Duration**: [e.g., 2 weeks]
|
||||||
|
**Deadline**: [date]
|
||||||
|
|
||||||
|
## Implementation Timeline
|
||||||
|
### Phase 1: Design
|
||||||
|
- [ ] RFC discussion
|
||||||
|
- [ ] Design finalization
|
||||||
|
- [ ] Approval
|
||||||
|
|
||||||
|
### Phase 2: Implementation
|
||||||
|
- [ ] Core implementation
|
||||||
|
- [ ] Tests
|
||||||
|
- [ ] Documentation
|
||||||
|
|
||||||
|
### Phase 3: Release
|
||||||
|
- [ ] Beta release
|
||||||
|
- [ ] Feedback collection
|
||||||
|
- [ ] Stable release
|
||||||
|
|
||||||
|
## Success Metrics
|
||||||
|
How will we measure success?
|
||||||
|
- Metric 1
|
||||||
|
- Metric 2
|
||||||
|
|
||||||
|
## References
|
||||||
|
- Related RFCs:
|
||||||
|
- External documentation:
|
||||||
|
- Prior art:
|
||||||
|
|
||||||
|
## Open Questions for Community
|
||||||
|
1. Question 1?
|
||||||
|
2. Question 2?
|
||||||
|
|
||||||
|
---
|
||||||
|
**Note**: This RFC is open for community discussion. Please provide feedback in the comments below.
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
---
|
||||||
|
name: Security Vulnerability Report
|
||||||
|
about: Report a security vulnerability (use only for non-critical issues)
|
||||||
|
title: '[SECURITY] '
|
||||||
|
labels: 'security'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## ⚠️ IMPORTANT: Private Disclosure Required
|
||||||
|
|
||||||
|
**For critical security vulnerabilities, DO NOT use this template.**
|
||||||
|
Follow the process in [SECURITY.md](../SECURITY.md) for responsible disclosure.
|
||||||
|
|
||||||
|
Use this template only for:
|
||||||
|
- Security improvements
|
||||||
|
- Non-critical security suggestions
|
||||||
|
- Security documentation updates
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security Issue
|
||||||
|
|
||||||
|
**Severity**:
|
||||||
|
<!-- Low, Medium, or informational only -->
|
||||||
|
|
||||||
|
## Description
|
||||||
|
<!-- Describe the security concern or improvement suggestion -->
|
||||||
|
|
||||||
|
## Affected Components
|
||||||
|
<!-- List the affected files, features, or components -->
|
||||||
|
|
||||||
|
## Suggested Mitigation
|
||||||
|
<!-- Describe how this could be addressed -->
|
||||||
|
|
||||||
|
## Standards Reference
|
||||||
|
Does this relate to security standards in [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards)?
|
||||||
|
- [ ] SPDX license identifiers
|
||||||
|
- [ ] Secret management
|
||||||
|
- [ ] Dependency security
|
||||||
|
- [ ] Access control
|
||||||
|
- [ ] Other: [specify]
|
||||||
|
|
||||||
|
## Additional Context
|
||||||
|
<!-- Add any other context about the security concern -->
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
- [ ] This is NOT a critical vulnerability requiring private disclosure
|
||||||
|
- [ ] I have reviewed the SECURITY.md policy
|
||||||
|
- [ ] I have provided sufficient detail for evaluation
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
name: Version Bump
|
||||||
|
about: Request or track a version change
|
||||||
|
title: '[VERSION] '
|
||||||
|
labels: 'version, type: version'
|
||||||
|
assignees: 'jmiller'
|
||||||
|
---
|
||||||
|
|
||||||
|
## Version Change
|
||||||
|
|
||||||
|
**Current version**: <!-- e.g., 01.02.03 -->
|
||||||
|
**Requested version**: <!-- e.g., 01.03.00 -->
|
||||||
|
**Change type**: <!-- patch / minor / major -->
|
||||||
|
|
||||||
|
## Reason
|
||||||
|
|
||||||
|
<!-- Why is this version bump needed? -->
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
- [ ] README.md `VERSION:` field updated
|
||||||
|
- [ ] CHANGELOG.md entry added
|
||||||
|
- [ ] Module descriptor version updated (Dolibarr: `$this->version`, Joomla: `<version>`)
|
||||||
|
- [ ] All file headers will be auto-propagated by `sync-version-on-merge` workflow
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# Enforces branch merge policy:
|
||||||
|
# feature/* → dev only
|
||||||
|
# fix/* → dev only
|
||||||
|
# hotfix/* → dev or main (emergency)
|
||||||
|
# dev → main only
|
||||||
|
# alpha/* → dev only
|
||||||
|
# beta/* → dev only
|
||||||
|
# rc/* → main only
|
||||||
|
|
||||||
|
name: Branch Policy Check
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize, reopened, edited]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-target:
|
||||||
|
name: Verify merge target
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check branch policy
|
||||||
|
run: |
|
||||||
|
HEAD="${{ github.head_ref }}"
|
||||||
|
BASE="${{ github.base_ref }}"
|
||||||
|
|
||||||
|
echo "PR: ${HEAD} → ${BASE}"
|
||||||
|
|
||||||
|
ALLOWED=true
|
||||||
|
REASON=""
|
||||||
|
|
||||||
|
case "$HEAD" in
|
||||||
|
feature/*|feat/*)
|
||||||
|
if [ "$BASE" != "dev" ]; then
|
||||||
|
ALLOWED=false
|
||||||
|
REASON="Feature branches must target 'dev', not '${BASE}'"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
fix/*|bugfix/*)
|
||||||
|
if [ "$BASE" != "dev" ]; then
|
||||||
|
ALLOWED=false
|
||||||
|
REASON="Fix branches must target 'dev', not '${BASE}'"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
hotfix/*)
|
||||||
|
if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
|
||||||
|
ALLOWED=false
|
||||||
|
REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
alpha/*|beta/*)
|
||||||
|
if [ "$BASE" != "dev" ]; then
|
||||||
|
ALLOWED=false
|
||||||
|
REASON="Pre-release branches must target 'dev', not '${BASE}'"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
rc/*)
|
||||||
|
if [ "$BASE" != "main" ]; then
|
||||||
|
ALLOWED=false
|
||||||
|
REASON="Release candidate branches must target 'main', not '${BASE}'"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
dev)
|
||||||
|
if [ "$BASE" != "main" ]; then
|
||||||
|
ALLOWED=false
|
||||||
|
REASON="Dev branch can only merge into 'main', not '${BASE}'"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ "$ALLOWED" = false ]; then
|
||||||
|
echo "::error::${REASON}"
|
||||||
|
echo ""
|
||||||
|
echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "${REASON}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Branch policy: OK (${HEAD} → ${BASE})"
|
||||||
|
echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user