diff --git a/.gitea/workflows/cascade-dev.yml b/.gitea/workflows/cascade-dev.yml index 54f9c37..d4780b1 100644 --- a/.gitea/workflows/cascade-dev.yml +++ b/.gitea/workflows/cascade-dev.yml @@ -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 diff --git a/.gitea/workflows/gitleaks.yml b/.gitea/workflows/gitleaks.yml new file mode 100644 index 0000000..b29f881 --- /dev/null +++ b/.gitea/workflows/gitleaks.yml @@ -0,0 +1,96 @@ +# Copyright (C) 2026 Moko Consulting +# +# 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 diff --git a/.gitea/workflows/mcp-auto-release.yml b/.gitea/workflows/mcp-auto-release.yml new file mode 100644 index 0000000..74daa33 --- /dev/null +++ b/.gitea/workflows/mcp-auto-release.yml @@ -0,0 +1,278 @@ +# MCP Server Auto-Release +# Copyright (C) 2026 Moko Consulting +# SPDX-License-Identifier: GPL-3.0-or-later +# +# MCP-specific release pipeline that builds TypeScript, runs validation, +# attaches the compiled dist/ as a release artifact, and creates a GitHub +# Release with tool inventory in the release notes. +# +# This replaces the generic auto-release.yml for MCP server repos. + +name: MCP Release + +on: + push: + branches: + - main + paths: + - 'src/**' + - 'package.json' + - 'tsconfig.json' + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +permissions: + contents: write + issues: write + +jobs: + build-and-release: + name: Build, Validate & Release + runs-on: ubuntu-latest + if: >- + !contains(github.event.head_commit.message, '[skip ci]') && + github.actor != 'github-actions[bot]' + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + token: ${{ secrets.GH_TOKEN || github.token }} + fetch-depth: 0 + + # ── Build ──────────────────────────────────────────────────────── + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install dependencies + run: npm ci + + - name: TypeScript compile check + run: npx tsc --noEmit + + - name: Build + run: npm run build + + - name: Verify dist output + run: | + for f in index.js client.js config.js types.js; do + test -f "dist/${f}" || (echo "ERROR: dist/${f} not found" && exit 1) + done + echo "✓ All dist files present" + + # ── Tool Inventory ─────────────────────────────────────────────── + - name: Generate tool inventory + id: tools + run: | + TOOL_COUNT=$(grep -c "server\.tool(" src/index.ts || echo "0") + echo "count=${TOOL_COUNT}" >> "$GITHUB_OUTPUT" + + # Extract tool names + TOOL_LIST=$(grep -oE "'[a-z_]+'" src/index.ts | head -100 | tr -d "'" | sort -u) + echo "Tools registered: ${TOOL_COUNT}" + + # Generate inventory for release notes + echo "## Tool Inventory (${TOOL_COUNT} tools)" > /tmp/tool-inventory.md + echo "" >> /tmp/tool-inventory.md + grep -B0 -A1 "server\.tool(" src/index.ts | grep -oE "'[^']+'" | while IFS= read -r name; do + read -r desc 2>/dev/null || true + CLEAN_NAME=$(echo "$name" | tr -d "'") + CLEAN_DESC=$(echo "$desc" | tr -d "'" | sed 's/,$//') + if [ -n "$CLEAN_NAME" ] && [ -n "$CLEAN_DESC" ]; then + echo "- \`${CLEAN_NAME}\` — ${CLEAN_DESC}" >> /tmp/tool-inventory.md + fi + done + + # ── Version ────────────────────────────────────────────────────── + - name: Setup MokoStandards tools + env: + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' + run: | + git clone --depth 1 --branch version/04 --quiet \ + "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ + /tmp/mokostandards + cd /tmp/mokostandards + composer install --no-dev --no-interaction --quiet + + - name: Read version from README.md + id: version + run: | + VERSION=$(php /tmp/mokostandards/api/cli/version_read.php --path . 2>/dev/null) + if [ -z "$VERSION" ]; then + echo "No VERSION in README.md — skipping release" + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + MAJOR=$(echo "$VERSION" | awk -F. '{print $1}') + MINOR=$(echo "$VERSION" | awk -F. '{printf "%s.%s", $1, $2}') + PATCH=$(echo "$VERSION" | awk -F. '{print $3}') + + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "branch=version/${MAJOR}" >> "$GITHUB_OUTPUT" + echo "major=$MAJOR" >> "$GITHUB_OUTPUT" + echo "minor=$MINOR" >> "$GITHUB_OUTPUT" + echo "release_tag=v${MAJOR}" >> "$GITHUB_OUTPUT" + + if [ "$PATCH" = "00" ]; then + echo "skip=true" >> "$GITHUB_OUTPUT" + else + echo "skip=false" >> "$GITHUB_OUTPUT" + if [ "$PATCH" = "01" ]; then + echo "is_first=true" >> "$GITHUB_OUTPUT" + else + echo "is_first=false" >> "$GITHUB_OUTPUT" + fi + fi + + - name: Check if already released + if: steps.version.outputs.skip != 'true' + id: check + run: | + TAG="${{ steps.version.outputs.release_tag }}" + TAG_EXISTS=false + git rev-parse "$TAG" >/dev/null 2>&1 && TAG_EXISTS=true + echo "tag_exists=$TAG_EXISTS" >> "$GITHUB_OUTPUT" + + # ── Release Artifact ───────────────────────────────────────────── + - name: Package dist + if: steps.version.outputs.skip != 'true' + run: | + VERSION="${{ steps.version.outputs.version }}" + REPO_NAME="${{ github.event.repository.name }}" + tar -czf "/tmp/${REPO_NAME}-${VERSION}.tar.gz" -C dist . + echo "artifact=/tmp/${REPO_NAME}-${VERSION}.tar.gz" >> "$GITHUB_OUTPUT" + + # ── Version Updates ────────────────────────────────────────────── + - name: Set platform version + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.tag_exists != 'true' + run: | + VERSION="${{ steps.version.outputs.version }}" + php /tmp/mokostandards/api/cli/version_set_platform.php \ + --path . --version "$VERSION" --branch main + + - name: Update version badges + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.tag_exists != 'true' + run: | + VERSION="${{ steps.version.outputs.version }}" + find . -name "*.md" ! -path "./.git/*" ! -path "./vendor/*" | while read -r f; do + if grep -q '\[VERSION:' "$f" 2>/dev/null; then + sed -i "s/\[VERSION:[[:space:]]*[0-9]\{2\}\.[0-9]\{2\}\.[0-9]\{2\}\]/[VERSION: ${VERSION}]/" "$f" + fi + done + + - name: Commit release changes + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.tag_exists != 'true' + run: | + if git diff --quiet && git diff --cached --quiet; then + echo "No changes to commit" + exit 0 + fi + VERSION="${{ steps.version.outputs.version }}" + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + git add -A + git commit -m "chore(release): build ${VERSION} [skip ci]" \ + --author="github-actions[bot] " + git push + + # ── Version Branch ─────────────────────────────────────────────── + - name: Archive version branch + if: steps.check.outputs.tag_exists != 'true' + run: | + BRANCH="${{ steps.version.outputs.branch }}" + git push origin HEAD:"$BRANCH" --force + echo "Updated archive branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY + + # ── Tag & Release ──────────────────────────────────────────────── + - name: Create git tag + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.tag_exists != 'true' && + steps.version.outputs.is_first == 'true' + run: | + TAG="${{ steps.version.outputs.release_tag }}" + if ! git rev-parse "$TAG" >/dev/null 2>&1; then + git tag "$TAG" + git push origin "$TAG" + fi + + - name: GitHub Release + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.tag_exists != 'true' + env: + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + run: | + VERSION="${{ steps.version.outputs.version }}" + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + MAJOR="${{ steps.version.outputs.major }}" + BRANCH="${{ steps.version.outputs.branch }}" + TOOL_COUNT="${{ steps.tools.outputs.count }}" + REPO_NAME="${{ github.event.repository.name }}" + + # Build release notes + NOTES=$(php /tmp/mokostandards/api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null) + [ -z "$NOTES" ] && NOTES="Release ${VERSION}" + + { + echo "$NOTES" + echo "" + echo "---" + echo "" + echo "### MCP Server Info" + echo "- **Tools registered**: ${TOOL_COUNT}" + echo "- **Node.js**: 20+" + echo "- **MCP SDK**: $(node -p \"require('./package.json').dependencies['@modelcontextprotocol/sdk']\" 2>/dev/null || echo 'unknown')" + echo "" + cat /tmp/tool-inventory.md 2>/dev/null || true + } > /tmp/release_notes.md + + EXISTING=$(gh release view "$RELEASE_TAG" --json tagName -q .tagName 2>/dev/null || true) + + ARTIFACT="/tmp/${REPO_NAME}-${VERSION}.tar.gz" + + if [ -z "$EXISTING" ]; then + gh release create "$RELEASE_TAG" \ + --title "v${MAJOR} (latest: ${VERSION})" \ + --notes-file /tmp/release_notes.md \ + --target "$BRANCH" \ + "$ARTIFACT" + echo "Release created: ${RELEASE_TAG} (${VERSION})" >> $GITHUB_STEP_SUMMARY + else + gh release edit "$RELEASE_TAG" \ + --title "v${MAJOR} (latest: ${VERSION})" \ + --notes-file /tmp/release_notes.md + gh release upload "$RELEASE_TAG" "$ARTIFACT" --clobber 2>/dev/null || true + echo "Release updated: ${RELEASE_TAG} -> ${VERSION}" >> $GITHUB_STEP_SUMMARY + fi + + # ── Summary ────────────────────────────────────────────────────── + - name: Pipeline Summary + if: always() + run: | + VERSION="${{ steps.version.outputs.version }}" + TOOL_COUNT="${{ steps.tools.outputs.count }}" + if [ "${{ steps.version.outputs.skip }}" = "true" ]; then + echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "## MCP Release Complete" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Detail | Value |" >> $GITHUB_STEP_SUMMARY + echo "|--------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Tools | ${TOOL_COUNT} |" >> $GITHUB_STEP_SUMMARY + echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Tag | \`${{ steps.version.outputs.release_tag }}\` |" >> $GITHUB_STEP_SUMMARY + fi diff --git a/.gitea/workflows/mcp-build-test.yml b/.gitea/workflows/mcp-build-test.yml new file mode 100644 index 0000000..cb631c7 --- /dev/null +++ b/.gitea/workflows/mcp-build-test.yml @@ -0,0 +1,61 @@ +# MCP Server Build & Validation +# Copyright (C) 2026 Moko Consulting +# SPDX-License-Identifier: GPL-3.0-or-later +# +# Builds the MCP server, validates TypeScript compilation, and checks +# that tools are properly registered with valid Zod schemas. + +name: MCP Build & Validate + +on: + push: + branches: [main, dev/**] + paths: ['src/**', 'package.json', 'tsconfig.json'] + pull_request: + branches: [main] + paths: ['src/**', 'package.json', 'tsconfig.json'] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [20, 22] + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - name: Install dependencies + run: npm ci + + - name: TypeScript compile + run: npx tsc --noEmit + + - name: Build + run: npm run build + + - name: Verify dist output exists + run: | + test -f dist/index.js || (echo "ERROR: dist/index.js not found" && exit 1) + test -f dist/client.js || (echo "ERROR: dist/client.js not found" && exit 1) + test -f dist/config.js || (echo "ERROR: dist/config.js not found" && exit 1) + test -f dist/types.js || (echo "ERROR: dist/types.js not found" && exit 1) + echo "✓ All required dist files present" + + - name: Verify shebang in index.js + run: | + head -1 dist/index.js | grep -q "#!/usr/bin/env node" || echo "WARNING: Missing shebang in dist/index.js" + + - name: Count registered tools + run: | + TOOL_COUNT=$(grep -c "server\.tool(" src/index.ts || true) + echo "Registered tools: ${TOOL_COUNT}" + if [ "${TOOL_COUNT}" -eq 0 ]; then + echo "ERROR: No tools registered in src/index.ts" + exit 1 + fi diff --git a/.gitea/workflows/mcp-sdk-check.yml b/.gitea/workflows/mcp-sdk-check.yml new file mode 100644 index 0000000..b926cd3 --- /dev/null +++ b/.gitea/workflows/mcp-sdk-check.yml @@ -0,0 +1,105 @@ +# MCP SDK Version Check +# Copyright (C) 2026 Moko Consulting +# SPDX-License-Identifier: GPL-3.0-or-later +# +# Weekly check for MCP SDK updates. Creates an issue when a new version +# of @modelcontextprotocol/sdk is available. + +name: MCP SDK Version Check + +on: + schedule: + - cron: '0 9 * * 1' # Every Monday at 9am UTC + workflow_dispatch: + +jobs: + check-sdk: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Check for SDK updates + id: sdk-check + run: | + CURRENT=$(node -p "require('./package.json').dependencies['@modelcontextprotocol/sdk']" | sed 's/[\^~]//') + LATEST=$(npm view @modelcontextprotocol/sdk version 2>/dev/null || echo "unknown") + + echo "current=${CURRENT}" >> $GITHUB_OUTPUT + echo "latest=${LATEST}" >> $GITHUB_OUTPUT + + if [ "${CURRENT}" != "${LATEST}" ] && [ "${LATEST}" != "unknown" ]; then + echo "update_available=true" >> $GITHUB_OUTPUT + echo "MCP SDK update available: ${CURRENT} → ${LATEST}" + else + echo "update_available=false" >> $GITHUB_OUTPUT + echo "MCP SDK is up to date: ${CURRENT}" + fi + + - name: Check for Zod updates + id: zod-check + run: | + CURRENT=$(node -p "require('./package.json').dependencies['zod']" | sed 's/[\^~]//') + LATEST=$(npm view zod version 2>/dev/null || echo "unknown") + + echo "current=${CURRENT}" >> $GITHUB_OUTPUT + echo "latest=${LATEST}" >> $GITHUB_OUTPUT + + if [ "${CURRENT}" != "${LATEST}" ] && [ "${LATEST}" != "unknown" ]; then + echo "update_available=true" >> $GITHUB_OUTPUT + else + echo "update_available=false" >> $GITHUB_OUTPUT + fi + + - name: Create update issue + if: steps.sdk-check.outputs.update_available == 'true' + uses: actions/github-script@v7 + with: + script: | + const title = `chore(deps): update @modelcontextprotocol/sdk ${process.env.CURRENT} → ${process.env.LATEST}`; + const body = [ + '## MCP SDK Update Available', + '', + `| Package | Current | Latest |`, + `|---------|---------|--------|`, + `| @modelcontextprotocol/sdk | ${process.env.CURRENT} | ${process.env.LATEST} |`, + `| zod | ${process.env.ZOD_CURRENT} | ${process.env.ZOD_LATEST} |`, + '', + '### Steps', + '1. Update package.json', + '2. Run `npm install`', + '3. Run `npm run build` to verify compilation', + '4. Test all tools against target API', + '', + '### Changelog', + `https://github.com/modelcontextprotocol/typescript-sdk/releases`, + ].join('\n'); + + // Check for existing open issue + const existing = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + labels: 'api-change', + }); + + const alreadyExists = existing.data.some(i => i.title.includes('@modelcontextprotocol/sdk')); + if (!alreadyExists) { + await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title, + body, + labels: ['api-change', 'chore'], + }); + } + env: + CURRENT: ${{ steps.sdk-check.outputs.current }} + LATEST: ${{ steps.sdk-check.outputs.latest }} + ZOD_CURRENT: ${{ steps.zod-check.outputs.current }} + ZOD_LATEST: ${{ steps.zod-check.outputs.latest }} diff --git a/.gitea/workflows/mcp-tool-inventory.yml b/.gitea/workflows/mcp-tool-inventory.yml new file mode 100644 index 0000000..f935b0c --- /dev/null +++ b/.gitea/workflows/mcp-tool-inventory.yml @@ -0,0 +1,57 @@ +# MCP Tool Inventory +# Copyright (C) 2026 Moko Consulting +# SPDX-License-Identifier: GPL-3.0-or-later +# +# Generates a tool inventory report on each push to main. +# Extracts tool names, descriptions, and parameter counts from src/index.ts. + +name: MCP Tool Inventory + +on: + push: + branches: [main] + paths: ['src/index.ts'] + workflow_dispatch: + +jobs: + inventory: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Generate tool inventory + run: | + echo "# MCP Tool Inventory" > TOOLS.md + echo "" >> TOOLS.md + echo "Auto-generated from \`src/index.ts\` on $(date -u +%Y-%m-%dT%H:%M:%SZ)" >> TOOLS.md + echo "" >> TOOLS.md + + # Count tools + TOOL_COUNT=$(grep -c "server\.tool(" src/index.ts || true) + echo "**Total tools: ${TOOL_COUNT}**" >> TOOLS.md + echo "" >> TOOLS.md + + # Extract tool names and descriptions + echo "| Tool | Description |" >> TOOLS.md + echo "|------|-------------|" >> TOOLS.md + + grep -A1 "server\.tool(" src/index.ts | grep -E "^\s*'" | while read -r line; do + TOOL_NAME=$(echo "$line" | sed "s/.*'\([^']*\)'.*/\1/") + # Get next line for description + DESC=$(grep -A2 "'${TOOL_NAME}'" src/index.ts | grep -E "^\s*'" | tail -1 | sed "s/.*'\([^']*\)'.*/\1/" || echo "") + echo "| \`${TOOL_NAME}\` | ${DESC} |" >> TOOLS.md + done + + echo "" >> TOOLS.md + echo "---" >> TOOLS.md + echo "*Generated by MCP Tool Inventory workflow*" >> TOOLS.md + + cat TOOLS.md + + - name: Upload inventory artifact + uses: actions/upload-artifact@v4 + with: + name: tool-inventory + path: TOOLS.md + retention-days: 90 diff --git a/README.md b/README.md index c21a8ad..a62c418 100644 --- a/README.md +++ b/README.md @@ -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 | diff --git a/docs/API.md b/docs/API.md index 7dae8a5..c6a591e 100644 --- a/docs/API.md +++ b/docs/API.md @@ -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 | diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..15c6a10 --- /dev/null +++ b/renovate.json @@ -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 + } + ] +}