chore: cascade main → dev (3f89873) [skip ci]
#3
+142
-113
@@ -7,18 +7,18 @@
|
||||
# INGROUP: MokoStandards.Maintenance
|
||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
|
||||
# PATH: /templates/workflows/cascade-dev.yml.template
|
||||
# VERSION: 01.00.00
|
||||
# BRIEF: Forward-merge main → dev after every push to main
|
||||
# VERSION: 02.00.00
|
||||
# BRIEF: Forward-merge main → all open branches after every push to main
|
||||
#
|
||||
# +========================================================================+
|
||||
# | CASCADE MAIN → DEV |
|
||||
# | CASCADE MAIN → ALL BRANCHES |
|
||||
# +========================================================================+
|
||||
# | |
|
||||
# | Triggers on every push to main (PR merges, bot commits, etc.) |
|
||||
# | |
|
||||
# | 1. Check if a 'dev' branch exists |
|
||||
# | 2. Create a PR (main → dev) via Gitea API |
|
||||
# | 3. Auto-merge if clean; leave open for manual resolution on conflict |
|
||||
# | 1. List all branches matching: dev, rc/*, beta/*, alpha/* |
|
||||
# | 2. For each: create PR (main → branch), auto-merge if clean |
|
||||
# | 3. On conflict: leave PR open for manual resolution |
|
||||
# | |
|
||||
# +========================================================================+
|
||||
|
||||
@@ -42,143 +42,172 @@ permissions:
|
||||
|
||||
jobs:
|
||||
cascade:
|
||||
name: Merge main → dev
|
||||
name: Cascade main → branches
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
!contains(github.event.head_commit.message, '[skip ci]') &&
|
||||
!contains(github.event.head_commit.message, '[skip cascade]')
|
||||
|
||||
steps:
|
||||
- name: Check dev branch exists
|
||||
id: check
|
||||
- name: Discover target branches
|
||||
id: branches
|
||||
env:
|
||||
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||
run: |
|
||||
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
|
||||
STATUS=$(curl -sS -o /dev/null -w "%{http_code}" \
|
||||
-H "Authorization: token ${GA_TOKEN}" \
|
||||
"${API}/branches/dev")
|
||||
# Fetch all branches (paginated)
|
||||
PAGE=1
|
||||
ALL_BRANCHES=""
|
||||
while true; do
|
||||
BATCH=$(curl -sS \
|
||||
-H "Authorization: token ${GA_TOKEN}" \
|
||||
"${API}/branches?page=${PAGE}&limit=50" \
|
||||
| jq -r '.[].name // empty')
|
||||
[ -z "$BATCH" ] && break
|
||||
ALL_BRANCHES="$ALL_BRANCHES $BATCH"
|
||||
PAGE=$((PAGE + 1))
|
||||
done
|
||||
|
||||
if [ "$STATUS" = "200" ]; then
|
||||
echo "exists=true" >> "$GITHUB_OUTPUT"
|
||||
echo "✅ dev branch exists"
|
||||
# Filter to cascade targets: dev, dev/*, rc/*, beta/*, alpha/*
|
||||
TARGETS=""
|
||||
for BRANCH in $ALL_BRANCHES; do
|
||||
case "$BRANCH" in
|
||||
dev|dev/*|rc/*|beta/*|alpha/*)
|
||||
TARGETS="$TARGETS $BRANCH"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
TARGETS=$(echo "$TARGETS" | xargs) # trim whitespace
|
||||
|
||||
if [ -z "$TARGETS" ]; then
|
||||
echo "targets=" >> "$GITHUB_OUTPUT"
|
||||
echo "ℹ️ No cascade target branches found"
|
||||
else
|
||||
echo "exists=false" >> "$GITHUB_OUTPUT"
|
||||
echo "ℹ️ No dev branch found (HTTP ${STATUS}) — skipping cascade"
|
||||
echo "targets=$TARGETS" >> "$GITHUB_OUTPUT"
|
||||
COUNT=$(echo "$TARGETS" | wc -w)
|
||||
echo "📋 Found ${COUNT} target branch(es): ${TARGETS}"
|
||||
fi
|
||||
|
||||
- name: Check if dev is already up to date
|
||||
if: steps.check.outputs.exists == 'true'
|
||||
id: diff
|
||||
env:
|
||||
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||
run: |
|
||||
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
|
||||
# Compare main..dev — if ahead_by is 0 there's nothing to cascade
|
||||
RESPONSE=$(curl -sS \
|
||||
-H "Authorization: token ${GA_TOKEN}" \
|
||||
"${API}/compare/dev...main")
|
||||
|
||||
AHEAD=$(echo "$RESPONSE" | jq '.total_commits // 0')
|
||||
|
||||
if [ "$AHEAD" -eq 0 ]; then
|
||||
echo "needs_merge=false" >> "$GITHUB_OUTPUT"
|
||||
echo "✅ dev is already up to date with main"
|
||||
else
|
||||
echo "needs_merge=true" >> "$GITHUB_OUTPUT"
|
||||
echo "ℹ️ main is ${AHEAD} commit(s) ahead of dev"
|
||||
fi
|
||||
|
||||
- name: Create cascade PR
|
||||
if: steps.diff.outputs.needs_merge == 'true'
|
||||
id: pr
|
||||
- name: Cascade to all target branches
|
||||
if: steps.branches.outputs.targets != ''
|
||||
env:
|
||||
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||
run: |
|
||||
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
SHORT_SHA="${GITHUB_SHA:0:7}"
|
||||
TARGETS="${{ steps.branches.outputs.targets }}"
|
||||
|
||||
# Check if a cascade PR already exists (main → dev)
|
||||
EXISTING=$(curl -sS \
|
||||
-H "Authorization: token ${GA_TOKEN}" \
|
||||
"${API}/pulls?state=open&head=${GITEA_ORG}:main&base=dev&limit=1")
|
||||
SUCCESS=0
|
||||
CONFLICTS=0
|
||||
SKIPPED=0
|
||||
FAILED=0
|
||||
|
||||
EXISTING_COUNT=$(echo "$EXISTING" | jq 'length')
|
||||
for BRANCH in $TARGETS; do
|
||||
echo ""
|
||||
echo "═══ main → ${BRANCH} ═══"
|
||||
|
||||
if [ "$EXISTING_COUNT" -gt 0 ]; then
|
||||
PR_NUMBER=$(echo "$EXISTING" | jq -r '.[0].number')
|
||||
PR_URL=$(echo "$EXISTING" | jq -r '.[0].html_url')
|
||||
echo "pr_number=${PR_NUMBER}" >> "$GITHUB_OUTPUT"
|
||||
echo "pr_exists=true" >> "$GITHUB_OUTPUT"
|
||||
echo "ℹ️ Cascade PR already exists: ${PR_URL}"
|
||||
else
|
||||
RESPONSE=$(curl -sS -w "\n%{http_code}" \
|
||||
# Check if branch is already up to date
|
||||
ENCODED_BRANCH=$(echo "$BRANCH" | sed 's|/|%2F|g')
|
||||
RESPONSE=$(curl -sS \
|
||||
-H "Authorization: token ${GA_TOKEN}" \
|
||||
"${API}/compare/${ENCODED_BRANCH}...main")
|
||||
|
||||
AHEAD=$(echo "$RESPONSE" | jq '.total_commits // 0')
|
||||
|
||||
if [ "$AHEAD" -eq 0 ]; then
|
||||
echo " ✅ Already up to date"
|
||||
SKIPPED=$((SKIPPED + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
echo " ℹ️ main is ${AHEAD} commit(s) ahead"
|
||||
|
||||
# Check for existing cascade PR
|
||||
EXISTING=$(curl -sS \
|
||||
-H "Authorization: token ${GA_TOKEN}" \
|
||||
"${API}/pulls?state=open&head=${GITEA_ORG}:main&base=${ENCODED_BRANCH}&limit=1")
|
||||
|
||||
EXISTING_COUNT=$(echo "$EXISTING" | jq 'length')
|
||||
PR_NUMBER=""
|
||||
|
||||
if [ "$EXISTING_COUNT" -gt 0 ]; then
|
||||
PR_NUMBER=$(echo "$EXISTING" | jq -r '.[0].number')
|
||||
echo " ℹ️ Reusing existing PR #${PR_NUMBER}"
|
||||
else
|
||||
# Create cascade PR
|
||||
PR_RESPONSE=$(curl -sS -w "\n%{http_code}" \
|
||||
-X POST \
|
||||
-H "Authorization: token ${GA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"title\": \"chore: cascade main → ${BRANCH} (${SHORT_SHA}) [skip ci]\",
|
||||
\"body\": \"## Automatic cascade\\n\\nForward-merging \`main\` (${SHORT_SHA}) into \`${BRANCH}\`.\\n\\nIf conflicts exist, resolve manually and merge.\\n\\n> Auto-created by **Cascade Main → Dev**.\",
|
||||
\"head\": \"main\",
|
||||
\"base\": \"${BRANCH}\"
|
||||
}" \
|
||||
"${API}/pulls")
|
||||
|
||||
HTTP_CODE=$(echo "$PR_RESPONSE" | tail -1)
|
||||
BODY=$(echo "$PR_RESPONSE" | sed '$d')
|
||||
PR_NUMBER=$(echo "$BODY" | jq -r '.number // empty')
|
||||
|
||||
if [ "$HTTP_CODE" != "201" ] || [ -z "$PR_NUMBER" ]; then
|
||||
MSG=$(echo "$BODY" | jq -r '.message // .' 2>/dev/null | head -1)
|
||||
echo " ❌ Failed to create PR (HTTP ${HTTP_CODE}): ${MSG}"
|
||||
FAILED=$((FAILED + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
echo " ✅ Created PR #${PR_NUMBER}"
|
||||
fi
|
||||
|
||||
# Try auto-merge
|
||||
PR_DATA=$(curl -sS \
|
||||
-H "Authorization: token ${GA_TOKEN}" \
|
||||
"${API}/pulls/${PR_NUMBER}")
|
||||
|
||||
MERGEABLE=$(echo "$PR_DATA" | jq -r '.mergeable // false')
|
||||
|
||||
if [ "$MERGEABLE" != "true" ]; then
|
||||
echo " ⚠️ Conflicts — PR #${PR_NUMBER} left open"
|
||||
CONFLICTS=$((CONFLICTS + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
MERGE_RESPONSE=$(curl -sS -w "\n%{http_code}" \
|
||||
-X POST \
|
||||
-H "Authorization: token ${GA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"title\": \"chore: cascade main → dev (${SHORT_SHA}) [skip ci]\",
|
||||
\"body\": \"## Automatic cascade\n\nForward-merging \`main\` (${SHORT_SHA}) into \`dev\` to keep branches in sync.\n\nIf this PR has conflicts, please resolve them manually and merge.\n\n> Auto-created by the **Cascade Main → Dev** workflow.\",
|
||||
\"head\": \"main\",
|
||||
\"base\": \"dev\"
|
||||
\"Do\": \"merge\",
|
||||
\"merge_message_field\": \"chore: cascade main → ${BRANCH} [skip ci]\",
|
||||
\"delete_branch_after_merge\": false
|
||||
}" \
|
||||
"${API}/pulls")
|
||||
"${API}/pulls/${PR_NUMBER}/merge")
|
||||
|
||||
HTTP_CODE=$(echo "$RESPONSE" | tail -1)
|
||||
BODY=$(echo "$RESPONSE" | sed '$d')
|
||||
PR_NUMBER=$(echo "$BODY" | jq -r '.number // empty')
|
||||
PR_URL=$(echo "$BODY" | jq -r '.html_url // empty')
|
||||
MERGE_HTTP=$(echo "$MERGE_RESPONSE" | tail -1)
|
||||
|
||||
if [ "$HTTP_CODE" = "201" ] && [ -n "$PR_NUMBER" ]; then
|
||||
echo "pr_number=${PR_NUMBER}" >> "$GITHUB_OUTPUT"
|
||||
echo "pr_exists=false" >> "$GITHUB_OUTPUT"
|
||||
echo "✅ Created cascade PR #${PR_NUMBER}: ${PR_URL}"
|
||||
if [ "$MERGE_HTTP" = "200" ] || [ "$MERGE_HTTP" = "204" ]; then
|
||||
echo " ✅ Merged — ${BRANCH} is in sync"
|
||||
SUCCESS=$((SUCCESS + 1))
|
||||
else
|
||||
echo "❌ Failed to create PR (HTTP ${HTTP_CODE}): ${BODY}"
|
||||
exit 1
|
||||
MERGE_BODY=$(echo "$MERGE_RESPONSE" | sed '$d')
|
||||
echo " ⚠️ Merge failed (HTTP ${MERGE_HTTP}) — PR #${PR_NUMBER} left open"
|
||||
CONFLICTS=$((CONFLICTS + 1))
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: Auto-merge cascade PR
|
||||
if: steps.pr.outputs.pr_number != ''
|
||||
env:
|
||||
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||
run: |
|
||||
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
PR_NUMBER="${{ steps.pr.outputs.pr_number }}"
|
||||
|
||||
# Check if PR is mergeable
|
||||
PR_DATA=$(curl -sS \
|
||||
-H "Authorization: token ${GA_TOKEN}" \
|
||||
"${API}/pulls/${PR_NUMBER}")
|
||||
|
||||
MERGEABLE=$(echo "$PR_DATA" | jq -r '.mergeable // false')
|
||||
|
||||
if [ "$MERGEABLE" != "true" ]; then
|
||||
echo "⚠️ PR #${PR_NUMBER} has conflicts — leaving open for manual resolution"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Merge the PR
|
||||
RESPONSE=$(curl -sS -w "\n%{http_code}" \
|
||||
-X POST \
|
||||
-H "Authorization: token ${GA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"Do\": \"merge\",
|
||||
\"merge_message_field\": \"chore: cascade main → dev [skip ci]\",
|
||||
\"delete_branch_after_merge\": false
|
||||
}" \
|
||||
"${API}/pulls/${PR_NUMBER}/merge")
|
||||
|
||||
HTTP_CODE=$(echo "$RESPONSE" | tail -1)
|
||||
|
||||
if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "204" ]; then
|
||||
echo "✅ Cascade PR #${PR_NUMBER} merged — dev is now in sync with main"
|
||||
else
|
||||
BODY=$(echo "$RESPONSE" | sed '$d')
|
||||
echo "⚠️ Merge failed (HTTP ${HTTP_CODE}): ${BODY}"
|
||||
echo "PR #${PR_NUMBER} left open for manual resolution"
|
||||
done
|
||||
|
||||
# Summary
|
||||
echo ""
|
||||
echo "════════════════════════════════════════"
|
||||
echo " ✅ Merged: ${SUCCESS}"
|
||||
echo " ⚠️ Conflicts: ${CONFLICTS}"
|
||||
echo " ⏭️ Up to date: ${SKIPPED}"
|
||||
echo " ❌ Failed: ${FAILED}"
|
||||
echo "════════════════════════════════════════"
|
||||
|
||||
if [ "$FAILED" -gt 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -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,278 @@
|
||||
# MCP Server Auto-Release
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# MCP-specific release pipeline that builds TypeScript, runs validation,
|
||||
# attaches the compiled dist/ as a release artifact, and creates a GitHub
|
||||
# Release with tool inventory in the release notes.
|
||||
#
|
||||
# This replaces the generic auto-release.yml for MCP server repos.
|
||||
|
||||
name: MCP Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'src/**'
|
||||
- 'package.json'
|
||||
- 'tsconfig.json'
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
build-and-release:
|
||||
name: Build, Validate & Release
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
!contains(github.event.head_commit.message, '[skip ci]') &&
|
||||
github.actor != 'github-actions[bot]'
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GH_TOKEN || github.token }}
|
||||
fetch-depth: 0
|
||||
|
||||
# ── Build ────────────────────────────────────────────────────────
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: TypeScript compile check
|
||||
run: npx tsc --noEmit
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Verify dist output
|
||||
run: |
|
||||
for f in index.js client.js config.js types.js; do
|
||||
test -f "dist/${f}" || (echo "ERROR: dist/${f} not found" && exit 1)
|
||||
done
|
||||
echo "✓ All dist files present"
|
||||
|
||||
# ── Tool Inventory ───────────────────────────────────────────────
|
||||
- name: Generate tool inventory
|
||||
id: tools
|
||||
run: |
|
||||
TOOL_COUNT=$(grep -c "server\.tool(" src/index.ts || echo "0")
|
||||
echo "count=${TOOL_COUNT}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Extract tool names
|
||||
TOOL_LIST=$(grep -oE "'[a-z_]+'" src/index.ts | head -100 | tr -d "'" | sort -u)
|
||||
echo "Tools registered: ${TOOL_COUNT}"
|
||||
|
||||
# Generate inventory for release notes
|
||||
echo "## Tool Inventory (${TOOL_COUNT} tools)" > /tmp/tool-inventory.md
|
||||
echo "" >> /tmp/tool-inventory.md
|
||||
grep -B0 -A1 "server\.tool(" src/index.ts | grep -oE "'[^']+'" | while IFS= read -r name; do
|
||||
read -r desc 2>/dev/null || true
|
||||
CLEAN_NAME=$(echo "$name" | tr -d "'")
|
||||
CLEAN_DESC=$(echo "$desc" | tr -d "'" | sed 's/,$//')
|
||||
if [ -n "$CLEAN_NAME" ] && [ -n "$CLEAN_DESC" ]; then
|
||||
echo "- \`${CLEAN_NAME}\` — ${CLEAN_DESC}" >> /tmp/tool-inventory.md
|
||||
fi
|
||||
done
|
||||
|
||||
# ── Version ──────────────────────────────────────────────────────
|
||||
- name: Setup MokoStandards tools
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
|
||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}'
|
||||
run: |
|
||||
git clone --depth 1 --branch version/04 --quiet \
|
||||
"https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \
|
||||
/tmp/mokostandards
|
||||
cd /tmp/mokostandards
|
||||
composer install --no-dev --no-interaction --quiet
|
||||
|
||||
- name: Read version from README.md
|
||||
id: version
|
||||
run: |
|
||||
VERSION=$(php /tmp/mokostandards/api/cli/version_read.php --path . 2>/dev/null)
|
||||
if [ -z "$VERSION" ]; then
|
||||
echo "No VERSION in README.md — skipping release"
|
||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
MAJOR=$(echo "$VERSION" | awk -F. '{print $1}')
|
||||
MINOR=$(echo "$VERSION" | awk -F. '{printf "%s.%s", $1, $2}')
|
||||
PATCH=$(echo "$VERSION" | awk -F. '{print $3}')
|
||||
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "branch=version/${MAJOR}" >> "$GITHUB_OUTPUT"
|
||||
echo "major=$MAJOR" >> "$GITHUB_OUTPUT"
|
||||
echo "minor=$MINOR" >> "$GITHUB_OUTPUT"
|
||||
echo "release_tag=v${MAJOR}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
if [ "$PATCH" = "00" ]; then
|
||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||
if [ "$PATCH" = "01" ]; then
|
||||
echo "is_first=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "is_first=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: Check if already released
|
||||
if: steps.version.outputs.skip != 'true'
|
||||
id: check
|
||||
run: |
|
||||
TAG="${{ steps.version.outputs.release_tag }}"
|
||||
TAG_EXISTS=false
|
||||
git rev-parse "$TAG" >/dev/null 2>&1 && TAG_EXISTS=true
|
||||
echo "tag_exists=$TAG_EXISTS" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# ── Release Artifact ─────────────────────────────────────────────
|
||||
- name: Package dist
|
||||
if: steps.version.outputs.skip != 'true'
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
REPO_NAME="${{ github.event.repository.name }}"
|
||||
tar -czf "/tmp/${REPO_NAME}-${VERSION}.tar.gz" -C dist .
|
||||
echo "artifact=/tmp/${REPO_NAME}-${VERSION}.tar.gz" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# ── Version Updates ──────────────────────────────────────────────
|
||||
- name: Set platform version
|
||||
if: >-
|
||||
steps.version.outputs.skip != 'true' &&
|
||||
steps.check.outputs.tag_exists != 'true'
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
php /tmp/mokostandards/api/cli/version_set_platform.php \
|
||||
--path . --version "$VERSION" --branch main
|
||||
|
||||
- name: Update version badges
|
||||
if: >-
|
||||
steps.version.outputs.skip != 'true' &&
|
||||
steps.check.outputs.tag_exists != 'true'
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
find . -name "*.md" ! -path "./.git/*" ! -path "./vendor/*" | while read -r f; do
|
||||
if grep -q '\[VERSION:' "$f" 2>/dev/null; then
|
||||
sed -i "s/\[VERSION:[[:space:]]*[0-9]\{2\}\.[0-9]\{2\}\.[0-9]\{2\}\]/[VERSION: ${VERSION}]/" "$f"
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Commit release changes
|
||||
if: >-
|
||||
steps.version.outputs.skip != 'true' &&
|
||||
steps.check.outputs.tag_exists != 'true'
|
||||
run: |
|
||||
if git diff --quiet && git diff --cached --quiet; then
|
||||
echo "No changes to commit"
|
||||
exit 0
|
||||
fi
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git config --local user.name "github-actions[bot]"
|
||||
git add -A
|
||||
git commit -m "chore(release): build ${VERSION} [skip ci]" \
|
||||
--author="github-actions[bot] <github-actions[bot]@users.noreply.github.com>"
|
||||
git push
|
||||
|
||||
# ── Version Branch ───────────────────────────────────────────────
|
||||
- name: Archive version branch
|
||||
if: steps.check.outputs.tag_exists != 'true'
|
||||
run: |
|
||||
BRANCH="${{ steps.version.outputs.branch }}"
|
||||
git push origin HEAD:"$BRANCH" --force
|
||||
echo "Updated archive branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# ── Tag & Release ────────────────────────────────────────────────
|
||||
- name: Create git tag
|
||||
if: >-
|
||||
steps.version.outputs.skip != 'true' &&
|
||||
steps.check.outputs.tag_exists != 'true' &&
|
||||
steps.version.outputs.is_first == 'true'
|
||||
run: |
|
||||
TAG="${{ steps.version.outputs.release_tag }}"
|
||||
if ! git rev-parse "$TAG" >/dev/null 2>&1; then
|
||||
git tag "$TAG"
|
||||
git push origin "$TAG"
|
||||
fi
|
||||
|
||||
- name: GitHub Release
|
||||
if: >-
|
||||
steps.version.outputs.skip != 'true' &&
|
||||
steps.check.outputs.tag_exists != 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
||||
MAJOR="${{ steps.version.outputs.major }}"
|
||||
BRANCH="${{ steps.version.outputs.branch }}"
|
||||
TOOL_COUNT="${{ steps.tools.outputs.count }}"
|
||||
REPO_NAME="${{ github.event.repository.name }}"
|
||||
|
||||
# Build release notes
|
||||
NOTES=$(php /tmp/mokostandards/api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null)
|
||||
[ -z "$NOTES" ] && NOTES="Release ${VERSION}"
|
||||
|
||||
{
|
||||
echo "$NOTES"
|
||||
echo ""
|
||||
echo "---"
|
||||
echo ""
|
||||
echo "### MCP Server Info"
|
||||
echo "- **Tools registered**: ${TOOL_COUNT}"
|
||||
echo "- **Node.js**: 20+"
|
||||
echo "- **MCP SDK**: $(node -p \"require('./package.json').dependencies['@modelcontextprotocol/sdk']\" 2>/dev/null || echo 'unknown')"
|
||||
echo ""
|
||||
cat /tmp/tool-inventory.md 2>/dev/null || true
|
||||
} > /tmp/release_notes.md
|
||||
|
||||
EXISTING=$(gh release view "$RELEASE_TAG" --json tagName -q .tagName 2>/dev/null || true)
|
||||
|
||||
ARTIFACT="/tmp/${REPO_NAME}-${VERSION}.tar.gz"
|
||||
|
||||
if [ -z "$EXISTING" ]; then
|
||||
gh release create "$RELEASE_TAG" \
|
||||
--title "v${MAJOR} (latest: ${VERSION})" \
|
||||
--notes-file /tmp/release_notes.md \
|
||||
--target "$BRANCH" \
|
||||
"$ARTIFACT"
|
||||
echo "Release created: ${RELEASE_TAG} (${VERSION})" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
gh release edit "$RELEASE_TAG" \
|
||||
--title "v${MAJOR} (latest: ${VERSION})" \
|
||||
--notes-file /tmp/release_notes.md
|
||||
gh release upload "$RELEASE_TAG" "$ARTIFACT" --clobber 2>/dev/null || true
|
||||
echo "Release updated: ${RELEASE_TAG} -> ${VERSION}" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
# ── Summary ──────────────────────────────────────────────────────
|
||||
- name: Pipeline Summary
|
||||
if: always()
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
TOOL_COUNT="${{ steps.tools.outputs.count }}"
|
||||
if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
|
||||
echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "## MCP Release Complete" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Detail | Value |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|--------|-------|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Tools | ${TOOL_COUNT} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Tag | \`${{ steps.version.outputs.release_tag }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
@@ -0,0 +1,61 @@
|
||||
# MCP Server Build & Validation
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# Builds the MCP server, validates TypeScript compilation, and checks
|
||||
# that tools are properly registered with valid Zod schemas.
|
||||
|
||||
name: MCP Build & Validate
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, dev/**]
|
||||
paths: ['src/**', 'package.json', 'tsconfig.json']
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths: ['src/**', 'package.json', 'tsconfig.json']
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [20, 22]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: TypeScript compile
|
||||
run: npx tsc --noEmit
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Verify dist output exists
|
||||
run: |
|
||||
test -f dist/index.js || (echo "ERROR: dist/index.js not found" && exit 1)
|
||||
test -f dist/client.js || (echo "ERROR: dist/client.js not found" && exit 1)
|
||||
test -f dist/config.js || (echo "ERROR: dist/config.js not found" && exit 1)
|
||||
test -f dist/types.js || (echo "ERROR: dist/types.js not found" && exit 1)
|
||||
echo "✓ All required dist files present"
|
||||
|
||||
- name: Verify shebang in index.js
|
||||
run: |
|
||||
head -1 dist/index.js | grep -q "#!/usr/bin/env node" || echo "WARNING: Missing shebang in dist/index.js"
|
||||
|
||||
- name: Count registered tools
|
||||
run: |
|
||||
TOOL_COUNT=$(grep -c "server\.tool(" src/index.ts || true)
|
||||
echo "Registered tools: ${TOOL_COUNT}"
|
||||
if [ "${TOOL_COUNT}" -eq 0 ]; then
|
||||
echo "ERROR: No tools registered in src/index.ts"
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,105 @@
|
||||
# MCP SDK Version Check
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# Weekly check for MCP SDK updates. Creates an issue when a new version
|
||||
# of @modelcontextprotocol/sdk is available.
|
||||
|
||||
name: MCP SDK Version Check
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 9 * * 1' # Every Monday at 9am UTC
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
check-sdk:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Check for SDK updates
|
||||
id: sdk-check
|
||||
run: |
|
||||
CURRENT=$(node -p "require('./package.json').dependencies['@modelcontextprotocol/sdk']" | sed 's/[\^~]//')
|
||||
LATEST=$(npm view @modelcontextprotocol/sdk version 2>/dev/null || echo "unknown")
|
||||
|
||||
echo "current=${CURRENT}" >> $GITHUB_OUTPUT
|
||||
echo "latest=${LATEST}" >> $GITHUB_OUTPUT
|
||||
|
||||
if [ "${CURRENT}" != "${LATEST}" ] && [ "${LATEST}" != "unknown" ]; then
|
||||
echo "update_available=true" >> $GITHUB_OUTPUT
|
||||
echo "MCP SDK update available: ${CURRENT} → ${LATEST}"
|
||||
else
|
||||
echo "update_available=false" >> $GITHUB_OUTPUT
|
||||
echo "MCP SDK is up to date: ${CURRENT}"
|
||||
fi
|
||||
|
||||
- name: Check for Zod updates
|
||||
id: zod-check
|
||||
run: |
|
||||
CURRENT=$(node -p "require('./package.json').dependencies['zod']" | sed 's/[\^~]//')
|
||||
LATEST=$(npm view zod version 2>/dev/null || echo "unknown")
|
||||
|
||||
echo "current=${CURRENT}" >> $GITHUB_OUTPUT
|
||||
echo "latest=${LATEST}" >> $GITHUB_OUTPUT
|
||||
|
||||
if [ "${CURRENT}" != "${LATEST}" ] && [ "${LATEST}" != "unknown" ]; then
|
||||
echo "update_available=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "update_available=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Create update issue
|
||||
if: steps.sdk-check.outputs.update_available == 'true'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const title = `chore(deps): update @modelcontextprotocol/sdk ${process.env.CURRENT} → ${process.env.LATEST}`;
|
||||
const body = [
|
||||
'## MCP SDK Update Available',
|
||||
'',
|
||||
`| Package | Current | Latest |`,
|
||||
`|---------|---------|--------|`,
|
||||
`| @modelcontextprotocol/sdk | ${process.env.CURRENT} | ${process.env.LATEST} |`,
|
||||
`| zod | ${process.env.ZOD_CURRENT} | ${process.env.ZOD_LATEST} |`,
|
||||
'',
|
||||
'### Steps',
|
||||
'1. Update package.json',
|
||||
'2. Run `npm install`',
|
||||
'3. Run `npm run build` to verify compilation',
|
||||
'4. Test all tools against target API',
|
||||
'',
|
||||
'### Changelog',
|
||||
`https://github.com/modelcontextprotocol/typescript-sdk/releases`,
|
||||
].join('\n');
|
||||
|
||||
// Check for existing open issue
|
||||
const existing = await github.rest.issues.listForRepo({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: 'open',
|
||||
labels: 'api-change',
|
||||
});
|
||||
|
||||
const alreadyExists = existing.data.some(i => i.title.includes('@modelcontextprotocol/sdk'));
|
||||
if (!alreadyExists) {
|
||||
await github.rest.issues.create({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
title,
|
||||
body,
|
||||
labels: ['api-change', 'chore'],
|
||||
});
|
||||
}
|
||||
env:
|
||||
CURRENT: ${{ steps.sdk-check.outputs.current }}
|
||||
LATEST: ${{ steps.sdk-check.outputs.latest }}
|
||||
ZOD_CURRENT: ${{ steps.zod-check.outputs.current }}
|
||||
ZOD_LATEST: ${{ steps.zod-check.outputs.latest }}
|
||||
@@ -0,0 +1,57 @@
|
||||
# MCP Tool Inventory
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# Generates a tool inventory report on each push to main.
|
||||
# Extracts tool names, descriptions, and parameter counts from src/index.ts.
|
||||
|
||||
name: MCP Tool Inventory
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths: ['src/index.ts']
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
inventory:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Generate tool inventory
|
||||
run: |
|
||||
echo "# MCP Tool Inventory" > TOOLS.md
|
||||
echo "" >> TOOLS.md
|
||||
echo "Auto-generated from \`src/index.ts\` on $(date -u +%Y-%m-%dT%H:%M:%SZ)" >> TOOLS.md
|
||||
echo "" >> TOOLS.md
|
||||
|
||||
# Count tools
|
||||
TOOL_COUNT=$(grep -c "server\.tool(" src/index.ts || true)
|
||||
echo "**Total tools: ${TOOL_COUNT}**" >> TOOLS.md
|
||||
echo "" >> TOOLS.md
|
||||
|
||||
# Extract tool names and descriptions
|
||||
echo "| Tool | Description |" >> TOOLS.md
|
||||
echo "|------|-------------|" >> TOOLS.md
|
||||
|
||||
grep -A1 "server\.tool(" src/index.ts | grep -E "^\s*'" | while read -r line; do
|
||||
TOOL_NAME=$(echo "$line" | sed "s/.*'\([^']*\)'.*/\1/")
|
||||
# Get next line for description
|
||||
DESC=$(grep -A2 "'${TOOL_NAME}'" src/index.ts | grep -E "^\s*'" | tail -1 | sed "s/.*'\([^']*\)'.*/\1/" || echo "")
|
||||
echo "| \`${TOOL_NAME}\` | ${DESC} |" >> TOOLS.md
|
||||
done
|
||||
|
||||
echo "" >> TOOLS.md
|
||||
echo "---" >> TOOLS.md
|
||||
echo "*Generated by MCP Tool Inventory workflow*" >> TOOLS.md
|
||||
|
||||
cat TOOLS.md
|
||||
|
||||
- name: Upload inventory artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: tool-inventory
|
||||
path: TOOLS.md
|
||||
retention-days: 90
|
||||
@@ -130,7 +130,7 @@ Each connection needs the Joomla site's base URL and an API token (generated in
|
||||
| `apiToken` | Yes | Joomla API token (Bearer auth) |
|
||||
| `insecure` | No | Skip TLS verification for self-signed certs |
|
||||
|
||||
## Tools
|
||||
## Tools (67)
|
||||
|
||||
### Articles
|
||||
|
||||
@@ -162,6 +162,16 @@ Each connection needs the Joomla site's base URL and an API token (generated in
|
||||
| `joomla_user_delete` | Delete a user |
|
||||
| `joomla_user_groups_list` | List user groups |
|
||||
|
||||
### Contacts
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `joomla_contacts_list` | List contacts with optional search |
|
||||
| `joomla_contact_get` | Get a single contact by ID |
|
||||
| `joomla_contact_create` | Create a new contact |
|
||||
| `joomla_contact_update` | Update an existing contact |
|
||||
| `joomla_contact_delete` | Delete a contact |
|
||||
|
||||
### Menus
|
||||
|
||||
| Tool | Description |
|
||||
@@ -169,6 +179,73 @@ Each connection needs the Joomla site's base URL and an API token (generated in
|
||||
| `joomla_menus_list` | List menu types |
|
||||
| `joomla_menu_items_list` | List menu items for a menu type |
|
||||
| `joomla_menu_item_get` | Get a single menu item by ID |
|
||||
| `joomla_menu_item_create` | Create a new menu item |
|
||||
| `joomla_menu_item_update` | Update a menu item |
|
||||
| `joomla_menu_item_delete` | Delete a menu item |
|
||||
|
||||
### Tags
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `joomla_tags_list` | List tags with optional search |
|
||||
| `joomla_tag_get` | Get a single tag by ID |
|
||||
| `joomla_tag_create` | Create a tag |
|
||||
| `joomla_tag_update` | Update a tag |
|
||||
| `joomla_tag_delete` | Delete a tag |
|
||||
|
||||
### Custom Fields
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `joomla_fields_list` | List custom fields for a context |
|
||||
| `joomla_field_get` | Get a single custom field by ID |
|
||||
| `joomla_field_create` | Create a custom field |
|
||||
| `joomla_field_delete` | Delete a custom field |
|
||||
|
||||
### Banners
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `joomla_banners_list` | List banners |
|
||||
| `joomla_banner_get` | Get a single banner by ID |
|
||||
| `joomla_banner_create` | Create a new banner |
|
||||
| `joomla_banner_delete` | Delete a banner |
|
||||
| `joomla_banner_clients_list` | List banner clients |
|
||||
|
||||
### Newsfeeds
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `joomla_newsfeeds_list` | List newsfeeds |
|
||||
| `joomla_newsfeed_get` | Get a single newsfeed by ID |
|
||||
| `joomla_newsfeed_create` | Create a new newsfeed |
|
||||
| `joomla_newsfeed_delete` | Delete a newsfeed |
|
||||
|
||||
### Messages
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `joomla_messages_list` | List private messages |
|
||||
| `joomla_message_get` | Get a single private message |
|
||||
| `joomla_message_send` | Send a private message to a user |
|
||||
| `joomla_message_delete` | Delete a private message |
|
||||
|
||||
### Media
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `joomla_media_list` | List media files in a folder |
|
||||
| `joomla_media_file_get` | Get metadata for a specific media file |
|
||||
| `joomla_media_file_delete` | Delete a media file |
|
||||
| `joomla_media_folder_create` | Create a new media folder |
|
||||
|
||||
### Redirects
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `joomla_redirects_list` | List URL redirects |
|
||||
| `joomla_redirect_create` | Create a URL redirect |
|
||||
| `joomla_redirect_delete` | Delete a URL redirect |
|
||||
|
||||
### Plugins
|
||||
|
||||
@@ -184,15 +261,9 @@ Each connection needs the Joomla site's base URL and an API token (generated in
|
||||
| `joomla_modules_list` | List site or admin modules |
|
||||
| `joomla_templates_list` | List site or admin templates |
|
||||
| `joomla_languages_list` | List installed content languages |
|
||||
| `joomla_tags_list` | List tags |
|
||||
| `joomla_tag_create` | Create a tag |
|
||||
| `joomla_fields_list` | List custom fields for a context |
|
||||
| `joomla_contacts_list` | List contacts |
|
||||
| `joomla_banners_list` | List banners |
|
||||
| `joomla_newsfeeds_list` | List newsfeeds |
|
||||
| `joomla_messages_list` | List private messages |
|
||||
| `joomla_message_get` | Get a single private message |
|
||||
| `joomla_media_list` | List media files in a folder |
|
||||
| `joomla_content_history_list` | List version history for a content item |
|
||||
| `joomla_checkin` | Check in (unlock) a checked-out content item |
|
||||
| `joomla_associations_list` | List multilingual associations for a content item |
|
||||
| `joomla_config_get` | Get application configuration |
|
||||
| `joomla_config_update` | Update application configuration values |
|
||||
| `joomla_api_request` | Raw API request to any Joomla endpoint |
|
||||
|
||||
+323
-13
@@ -10,8 +10,8 @@ DEFGROUP: joomla-api-mcp.Documentation
|
||||
INGROUP: joomla-api-mcp
|
||||
REPO: https://git.mokoconsulting.tech/MokoConsulting/joomla-api-mcp
|
||||
PATH: /docs/API.md
|
||||
VERSION: 00.00.01
|
||||
BRIEF: MCP tool reference documentation
|
||||
VERSION: 01.01.00
|
||||
BRIEF: MCP tool reference documentation — 67 tools
|
||||
-->
|
||||
|
||||
# API Reference
|
||||
@@ -156,6 +156,59 @@ Delete a user.
|
||||
### `joomla_user_groups_list`
|
||||
List all user groups. No parameters.
|
||||
|
||||
## Contacts
|
||||
|
||||
### `joomla_contacts_list`
|
||||
List contacts with optional search.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `search` | string | No | Search in name |
|
||||
|
||||
### `joomla_contact_get`
|
||||
Get a single contact by ID.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `id` | number | Yes | Contact ID |
|
||||
|
||||
### `joomla_contact_create`
|
||||
Create a new contact.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `name` | string | Yes | Contact name |
|
||||
| `alias` | string | No | URL alias |
|
||||
| `catid` | number | No | Category ID |
|
||||
| `email_to` | string | No | Email address |
|
||||
| `telephone` | string | No | Phone number |
|
||||
| `address` | string | No | Street address |
|
||||
| `suburb` | string | No | City/suburb |
|
||||
| `state` | string | No | State/province |
|
||||
| `postcode` | string | No | Postal code |
|
||||
| `country_id` | number | No | Country ID |
|
||||
| `published` | number | No | 1=published, 0=unpublished |
|
||||
| `language` | string | No | Language code (default `"*"`) |
|
||||
|
||||
### `joomla_contact_update`
|
||||
Update an existing contact.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `id` | number | Yes | Contact ID |
|
||||
| `name` | string | No | Contact name |
|
||||
| `email_to` | string | No | Email address |
|
||||
| `telephone` | string | No | Phone number |
|
||||
| `address` | string | No | Street address |
|
||||
| `published` | number | No | 1=published, 0=unpublished |
|
||||
|
||||
### `joomla_contact_delete`
|
||||
Delete a contact.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `id` | number | Yes | Contact ID |
|
||||
|
||||
## Menus
|
||||
|
||||
### `joomla_menus_list`
|
||||
@@ -171,6 +224,38 @@ List menu items for a menu type.
|
||||
### `joomla_menu_item_get`
|
||||
Get a single menu item by ID.
|
||||
|
||||
### `joomla_menu_item_create`
|
||||
Create a new menu item.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `title` | string | Yes | Menu item title |
|
||||
| `menutype` | string | Yes | Menu type alias (e.g. `"mainmenu"`) |
|
||||
| `type` | string | Yes | Item type (`"component"`, `"url"`, `"alias"`, `"separator"`, `"heading"`) |
|
||||
| `link` | string | No | URL or component link |
|
||||
| `parent_id` | number | No | Parent menu item ID (default 1 = root) |
|
||||
| `published` | number | No | 1=published, 0=unpublished |
|
||||
| `access` | number | No | Access level ID |
|
||||
| `language` | string | No | Language code (default `"*"`) |
|
||||
|
||||
### `joomla_menu_item_update`
|
||||
Update a menu item.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `id` | number | Yes | Menu item ID |
|
||||
| `title` | string | No | New title |
|
||||
| `link` | string | No | New link URL |
|
||||
| `published` | number | No | 1=published, 0=unpublished |
|
||||
| `parent_id` | number | No | New parent ID |
|
||||
|
||||
### `joomla_menu_item_delete`
|
||||
Delete a menu item.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `id` | number | Yes | Menu item ID |
|
||||
|
||||
## Plugins
|
||||
|
||||
### `joomla_plugins_list`
|
||||
@@ -208,14 +293,22 @@ List site or admin templates.
|
||||
|-----------|------|----------|-------------|
|
||||
| `client_id` | `"0"` / `"1"` | No | 0=site, 1=admin |
|
||||
|
||||
## Other Tools
|
||||
|
||||
### `joomla_languages_list`
|
||||
List installed content languages.
|
||||
## Tags
|
||||
|
||||
### `joomla_tags_list`
|
||||
List tags with optional search.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `search` | string | No | Search in title |
|
||||
|
||||
### `joomla_tag_get`
|
||||
Get a single tag by ID.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `id` | number | Yes | Tag ID |
|
||||
|
||||
### `joomla_tag_create`
|
||||
Create a tag.
|
||||
|
||||
@@ -224,24 +317,153 @@ Create a tag.
|
||||
| `title` | string | Yes | Tag title |
|
||||
| `parent_id` | number | No | Parent tag ID |
|
||||
|
||||
### `joomla_tag_update`
|
||||
Update a tag.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `id` | number | Yes | Tag ID |
|
||||
| `title` | string | No | New tag title |
|
||||
| `published` | number | No | 1=published, 0=unpublished |
|
||||
|
||||
### `joomla_tag_delete`
|
||||
Delete a tag.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `id` | number | Yes | Tag ID |
|
||||
|
||||
## Custom Fields
|
||||
|
||||
### `joomla_fields_list`
|
||||
List custom fields for a context (default `"com_content.article"`).
|
||||
|
||||
### `joomla_contacts_list`
|
||||
List contacts with optional search.
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `context` | string | No | Context (default `"com_content.article"`) |
|
||||
|
||||
### `joomla_field_get`
|
||||
Get a single custom field by ID.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `id` | number | Yes | Field ID |
|
||||
|
||||
### `joomla_field_create`
|
||||
Create a custom field.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `title` | string | Yes | Field title |
|
||||
| `name` | string | Yes | Field name (system identifier) |
|
||||
| `type` | string | Yes | Field type (text, textarea, list, radio, checkboxes, etc.) |
|
||||
| `context` | string | No | Context (default `"com_content.article"`) |
|
||||
| `label` | string | No | Display label |
|
||||
| `description` | string | No | Field description |
|
||||
| `required` | number | No | 1=required, 0=optional |
|
||||
| `state` | number | No | 1=published, 0=unpublished |
|
||||
|
||||
### `joomla_field_delete`
|
||||
Delete a custom field.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `id` | number | Yes | Field ID |
|
||||
|
||||
## Banners
|
||||
|
||||
### `joomla_banners_list`
|
||||
List banners.
|
||||
List banners. No parameters.
|
||||
|
||||
### `joomla_banner_get`
|
||||
Get a single banner by ID.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `id` | number | Yes | Banner ID |
|
||||
|
||||
### `joomla_banner_create`
|
||||
Create a new banner.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `name` | string | Yes | Banner name |
|
||||
| `catid` | number | No | Category ID |
|
||||
| `clickurl` | string | No | Click URL |
|
||||
| `custombannercode` | string | No | Custom HTML/code |
|
||||
| `state` | number | No | 1=published, 0=unpublished |
|
||||
|
||||
### `joomla_banner_delete`
|
||||
Delete a banner.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `id` | number | Yes | Banner ID |
|
||||
|
||||
### `joomla_banner_clients_list`
|
||||
List banner clients. No parameters.
|
||||
|
||||
## Newsfeeds
|
||||
|
||||
### `joomla_newsfeeds_list`
|
||||
List newsfeeds.
|
||||
List newsfeeds. No parameters.
|
||||
|
||||
### `joomla_newsfeed_get`
|
||||
Get a single newsfeed by ID.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `id` | number | Yes | Newsfeed ID |
|
||||
|
||||
### `joomla_newsfeed_create`
|
||||
Create a new newsfeed.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `name` | string | Yes | Feed name |
|
||||
| `link` | string | Yes | Feed URL |
|
||||
| `catid` | number | Yes | Category ID |
|
||||
| `numarticles` | number | No | Number of articles to display |
|
||||
| `published` | number | No | 1=published, 0=unpublished |
|
||||
| `language` | string | No | Language code (default `"*"`) |
|
||||
|
||||
### `joomla_newsfeed_delete`
|
||||
Delete a newsfeed.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `id` | number | Yes | Newsfeed ID |
|
||||
|
||||
## Messages
|
||||
|
||||
### `joomla_messages_list`
|
||||
List private messages.
|
||||
List private messages. No parameters.
|
||||
|
||||
### `joomla_message_get`
|
||||
Get a single private message by ID.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `id` | number | Yes | Message ID |
|
||||
|
||||
### `joomla_message_send`
|
||||
Send a private message to a Joomla user.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `user_id_to` | number | Yes | Recipient user ID |
|
||||
| `subject` | string | Yes | Message subject |
|
||||
| `message` | string | Yes | Message body |
|
||||
|
||||
### `joomla_message_delete`
|
||||
Delete a private message.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `id` | number | Yes | Message ID |
|
||||
|
||||
## Media
|
||||
|
||||
### `joomla_media_list`
|
||||
List media files in a folder.
|
||||
|
||||
@@ -249,8 +471,88 @@ List media files in a folder.
|
||||
|-----------|------|----------|-------------|
|
||||
| `path` | string | No | Folder path relative to media root |
|
||||
|
||||
### `joomla_media_file_get`
|
||||
Get metadata for a specific media file.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `path` | string | Yes | File path relative to media root (e.g. `"images/logo.png"`) |
|
||||
|
||||
### `joomla_media_file_delete`
|
||||
Delete a media file.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `path` | string | Yes | File path relative to media root |
|
||||
|
||||
### `joomla_media_folder_create`
|
||||
Create a new media folder.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `path` | string | Yes | Full folder path to create (e.g. `"images/photos/2026"`) |
|
||||
|
||||
## Redirects
|
||||
|
||||
### `joomla_redirects_list`
|
||||
List URL redirects.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `search` | string | No | Search in old URL |
|
||||
| `state` | `"0"` / `"1"` / `"2"` / `"-2"` | No | 0=disabled, 1=enabled, 2=archived, -2=trashed |
|
||||
|
||||
### `joomla_redirect_create`
|
||||
Create a URL redirect.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `old_url` | string | Yes | Source URL to redirect from |
|
||||
| `new_url` | string | Yes | Destination URL to redirect to |
|
||||
| `status_code` | `"301"` / `"302"` | No | 301=permanent, 302=temporary (default 301) |
|
||||
| `published` | number | No | 1=enabled, 0=disabled |
|
||||
|
||||
### `joomla_redirect_delete`
|
||||
Delete a URL redirect.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `id` | number | Yes | Redirect ID |
|
||||
|
||||
## Content History
|
||||
|
||||
### `joomla_content_history_list`
|
||||
List version history for a content item.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `type_alias` | string | Yes | Content type alias (e.g. `"com_content.article"`) |
|
||||
| `item_id` | number | Yes | Item ID |
|
||||
|
||||
## Checkin
|
||||
|
||||
### `joomla_checkin`
|
||||
Check in (unlock) a content item that is checked out.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `context` | string | Yes | Context (e.g. `"com_content.article"`) |
|
||||
| `id` | number | Yes | Item ID to check in |
|
||||
|
||||
## Associations
|
||||
|
||||
### `joomla_associations_list`
|
||||
List multilingual associations for a content item.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `context` | string | Yes | Context (e.g. `"com_content.article"`) |
|
||||
| `id` | number | Yes | Item ID to get associations for |
|
||||
|
||||
## Configuration
|
||||
|
||||
### `joomla_config_get`
|
||||
Get application configuration.
|
||||
Get application configuration. No parameters.
|
||||
|
||||
### `joomla_config_update`
|
||||
Update application configuration values.
|
||||
@@ -259,6 +561,13 @@ Update application configuration values.
|
||||
|-----------|------|----------|-------------|
|
||||
| `settings` | object | Yes | Key-value pairs of settings to update |
|
||||
|
||||
## Languages
|
||||
|
||||
### `joomla_languages_list`
|
||||
List installed content languages. No parameters.
|
||||
|
||||
## Generic
|
||||
|
||||
### `joomla_api_request`
|
||||
Make a raw API request to any Joomla Web Services endpoint.
|
||||
|
||||
@@ -276,4 +585,5 @@ List all configured connections. No parameters.
|
||||
|
||||
| Date | Version | Author | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| 2026-04-23 | 0.0.1 | jmiller | Initial API reference |
|
||||
| 2026-04-23 | 0.0.1 | jmiller | Initial API reference (36 tools) |
|
||||
| 2026-05-07 | 0.1.0 | jmiller | Expanded to 67 tools — full CRUD for contacts, banners, newsfeeds, tags, fields, menu items, messages, media, redirects, associations, checkin, content history |
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:recommended",
|
||||
"schedule:weekly",
|
||||
":disableDependencyDashboard"
|
||||
],
|
||||
"labels": ["dependencies"],
|
||||
"automerge": false,
|
||||
"platformAutomerge": false,
|
||||
"rangeStrategy": "bump",
|
||||
"packageRules": [
|
||||
{
|
||||
"matchUpdateTypes": ["patch"],
|
||||
"automerge": true
|
||||
},
|
||||
{
|
||||
"matchManagers": ["composer"],
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"matchManagers": ["npm"],
|
||||
"enabled": true
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user