Template
release: source-dir standardization + npm two-zip release (dev -> main) #39
@@ -0,0 +1,108 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
name: "Resolve source dir"
|
||||
description: >-
|
||||
Resolve the repository source directory from the MokoGIT metadata API
|
||||
(entry_point) and cross-check the Makefile SRC_DIR, erroring on conflict.
|
||||
Falls back to source/ when neither is available.
|
||||
|
||||
inputs:
|
||||
mokogit_token:
|
||||
description: "MokoGIT API token used for the metadata lookup (secrets are not accessible inside composite actions, so pass it in)."
|
||||
required: false
|
||||
default: ""
|
||||
strict:
|
||||
description: >-
|
||||
When 'true', an unreachable/invalid metadata API is a hard error
|
||||
(refuse to guess). When 'false', warn and treat entry_point as absent.
|
||||
A genuine metadata-vs-Makefile conflict always errors regardless.
|
||||
required: false
|
||||
default: "true"
|
||||
|
||||
outputs:
|
||||
source_dir:
|
||||
description: "Resolved source directory with trailing slash (e.g. source/)"
|
||||
value: ${{ steps.resolve.outputs.source_dir }}
|
||||
source_glob:
|
||||
description: "Recursive glob for the source directory (e.g. source/**)"
|
||||
value: ${{ steps.resolve.outputs.source_glob }}
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- id: resolve
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
STRICT='${{ inputs.strict }}'
|
||||
|
||||
normalize() {
|
||||
# Trim whitespace, strip surrounding quotes, ensure single trailing slash.
|
||||
# A resolved value of "." / "./" (repo root) is treated as invalid → empty.
|
||||
local v="$1"
|
||||
v="$(printf '%s' "$v" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
|
||||
v="${v%\"}"; v="${v#\"}"
|
||||
v="${v%\'}"; v="${v#\'}"
|
||||
v="$(printf '%s' "$v" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
|
||||
[ -z "$v" ] && { printf ''; return; }
|
||||
v="${v%/}/"
|
||||
if [ "$v" = "./" ] || [ "$v" = "." ]; then
|
||||
printf ''
|
||||
return
|
||||
fi
|
||||
printf '%s' "$v"
|
||||
}
|
||||
|
||||
# --- Metadata entry_point --------------------------------------------
|
||||
ENTRY_POINT=""
|
||||
META_DIR=""
|
||||
if [ -n "${MOKOGIT_URL:-}" ] && [ -n "${GIT_ORG:-}" ] && [ -n "${GIT_REPO:-}" ]; then
|
||||
URL="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}/metadata"
|
||||
CURL_RC=0
|
||||
META=$(curl -sf -H "Authorization: token ${{ inputs.mokogit_token }}" "$URL") || CURL_RC=$?
|
||||
if [ "$CURL_RC" -ne 0 ]; then
|
||||
if [ "$STRICT" = "true" ]; then
|
||||
echo "::error::metadata API unreachable (curl $CURL_RC); refusing to guess source dir"
|
||||
exit 1
|
||||
else
|
||||
echo "::warning::metadata unreachable; treating entry_point as absent"
|
||||
META_DIR=""
|
||||
fi
|
||||
else
|
||||
ENTRY_POINT=$(printf '%s' "$META" | python3 -c "import json,sys;print(json.load(sys.stdin).get('entry_point','') or '')") || { echo "::error::metadata not valid JSON"; exit 1; }
|
||||
META_DIR="$(normalize "$ENTRY_POINT")"
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- Makefile SRC_DIR -------------------------------------------------
|
||||
MAKE_DIR=""
|
||||
if [ -f Makefile ]; then
|
||||
RAW=$(awk -F':= *' '/^SRC_DIR[[:space:]]*:=/{print $2}' Makefile | head -n1)
|
||||
if [ -z "$RAW" ]; then
|
||||
RAW=$(awk -F'\\?= *' '/^SRC_DIR[[:space:]]*\?=/{print $2}' Makefile | head -n1)
|
||||
fi
|
||||
MAKE_DIR="$(normalize "$RAW")"
|
||||
fi
|
||||
|
||||
# --- Reconcile --------------------------------------------------------
|
||||
# A genuine conflict ALWAYS errors, regardless of strict.
|
||||
SOURCE_DIR=""
|
||||
if [ -n "$META_DIR" ] && [ -n "$MAKE_DIR" ]; then
|
||||
if [ "$META_DIR" != "$MAKE_DIR" ]; then
|
||||
echo "::error::source dir conflict: metadata entry_point='${META_DIR}' vs Makefile SRC_DIR='${MAKE_DIR}'"
|
||||
exit 1
|
||||
fi
|
||||
SOURCE_DIR="$META_DIR"
|
||||
elif [ -n "$META_DIR" ]; then
|
||||
SOURCE_DIR="$META_DIR"
|
||||
elif [ -n "$MAKE_DIR" ]; then
|
||||
SOURCE_DIR="$MAKE_DIR"
|
||||
else
|
||||
SOURCE_DIR="source/"
|
||||
fi
|
||||
|
||||
echo "Resolved source_dir: ${SOURCE_DIR}"
|
||||
echo "source_dir=${SOURCE_DIR}" >> "$GITHUB_OUTPUT"
|
||||
echo "source_glob=${SOURCE_DIR}**" >> "$GITHUB_OUTPUT"
|
||||
@@ -3,8 +3,8 @@
|
||||
# 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.
|
||||
# attaches a full-repository zip and a source-directory zip as release
|
||||
# artifacts, and creates a GitHub Release with tool inventory in the notes.
|
||||
#
|
||||
# This replaces the generic auto-release.yml for MCP server repos.
|
||||
|
||||
@@ -15,6 +15,9 @@ on:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
# Transitional: match both the new 'source/' layout and legacy 'src/'.
|
||||
# This static YAML filter cannot use the resolve-source-dir output.
|
||||
- 'source/**'
|
||||
- 'src/**'
|
||||
- 'package.json'
|
||||
- 'tsconfig.json'
|
||||
@@ -51,6 +54,18 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
# ── Resolve source directory ─────────────────────────────────────
|
||||
# Resolves the canonical source dir (e.g. 'source/' or 'src/') via the
|
||||
# shared composite action. Its 'source_dir' output has a trailing slash.
|
||||
# Note: the action lives on feature/ci-resolve-source-dir; this path
|
||||
# resolves once that branch merges.
|
||||
- name: Resolve source directory
|
||||
id: srcdir
|
||||
uses: ./.mokogit/actions/resolve-source-dir
|
||||
with:
|
||||
mokogit_token: ${{ secrets.MOKOGIT_TOKEN }}
|
||||
strict: 'true'
|
||||
|
||||
- name: TypeScript compile check
|
||||
run: npx tsc --noEmit
|
||||
|
||||
@@ -68,17 +83,30 @@ jobs:
|
||||
- name: Generate tool inventory
|
||||
id: tools
|
||||
run: |
|
||||
TOOL_COUNT=$(grep -c "server\.tool(" src/index.ts || echo "0")
|
||||
# Resolve the entrypoint from the source dir output, fall back to
|
||||
# legacy src/index.ts if the resolved file is missing.
|
||||
SRC="${{ steps.srcdir.outputs.source_dir }}"
|
||||
INDEX="${SRC}index.ts"
|
||||
if [ ! -f "$INDEX" ]; then
|
||||
echo "::warning::resolved entrypoint $INDEX missing; falling back to src/index.ts"
|
||||
INDEX="src/index.ts"
|
||||
fi
|
||||
|
||||
# Fail loud on an unreadable entry file so it isn't silently
|
||||
# counted as 0 tools.
|
||||
[ -r "$INDEX" ] || { echo "::error::entry file unreadable"; exit 1; }
|
||||
|
||||
TOOL_COUNT=$(grep -c "server\.tool(" "$INDEX" || 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)
|
||||
TOOL_LIST=$(grep -oE "'[a-z_]+'" "$INDEX" | 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
|
||||
grep -B0 -A1 "server\.tool(" "$INDEX" | 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/,$//')
|
||||
@@ -139,15 +167,6 @@ jobs:
|
||||
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: >-
|
||||
@@ -208,6 +227,44 @@ jobs:
|
||||
git push origin "$TAG"
|
||||
fi
|
||||
|
||||
# ── Release Artifacts ────────────────────────────────────────────
|
||||
# Built AFTER the release commit/tag so both zips reflect the final
|
||||
# released tree. Mirrors the Joomla two-artifact model:
|
||||
# 1. full-repo zip — tracked files only (git archive excludes
|
||||
# .git/node_modules/dist automatically).
|
||||
# 2. source zip — just the resolved source subtree. The git
|
||||
# tree-ish path must NOT have a trailing slash, hence ${SRC%/}.
|
||||
- name: Package release artifacts
|
||||
id: package
|
||||
if: steps.version.outputs.skip != 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
REPO_NAME="${{ github.event.repository.name }}"
|
||||
|
||||
REPO_ZIP="/tmp/${REPO_NAME}-${VERSION}.zip"
|
||||
SOURCE_ZIP="/tmp/${REPO_NAME}-${VERSION}-source.zip"
|
||||
|
||||
# Full-repository "standard" zip (tracked files only).
|
||||
git archive --format=zip -o "$REPO_ZIP" HEAD
|
||||
|
||||
# Source-directory subtree zip.
|
||||
SRC="${{ steps.srcdir.outputs.source_dir }}"
|
||||
SRC="${SRC%/}"
|
||||
if [ -z "$SRC" ] || [ "$SRC" = "." ]; then
|
||||
echo "::error::source_dir did not resolve to a real subdirectory (got '${SRC}'); refusing to build a misleading source zip"
|
||||
exit 1
|
||||
fi
|
||||
git archive --format=zip -o "$SOURCE_ZIP" "HEAD:${SRC}"
|
||||
|
||||
# Fail closed if either archive is missing or empty.
|
||||
for z in "$REPO_ZIP" "$SOURCE_ZIP"; do
|
||||
[ -s "$z" ] || { echo "::error::archive $z missing or empty"; exit 1; }
|
||||
done
|
||||
|
||||
echo "repo_zip=${REPO_ZIP}" >> "$GITHUB_OUTPUT"
|
||||
echo "source_zip=${SOURCE_ZIP}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: GitHub Release
|
||||
if: >-
|
||||
steps.version.outputs.skip != 'true' &&
|
||||
@@ -215,6 +272,7 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
||||
MAJOR="${{ steps.version.outputs.major }}"
|
||||
@@ -223,8 +281,8 @@ jobs:
|
||||
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}"
|
||||
NOTES=$(php /tmp/mokostandards/api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null || true)
|
||||
if [ -z "$NOTES" ]; then NOTES="Release ${VERSION}"; fi
|
||||
|
||||
{
|
||||
echo "$NOTES"
|
||||
@@ -241,20 +299,22 @@ jobs:
|
||||
|
||||
EXISTING=$(gh release view "$RELEASE_TAG" --json tagName -q .tagName 2>/dev/null || true)
|
||||
|
||||
ARTIFACT="/tmp/${REPO_NAME}-${VERSION}.tar.gz"
|
||||
# Two artifacts: full-repo zip and source zip (from Package step).
|
||||
REPO_ZIP="${{ steps.package.outputs.repo_zip }}"
|
||||
SOURCE_ZIP="${{ steps.package.outputs.source_zip }}"
|
||||
|
||||
if [ -z "$EXISTING" ]; then
|
||||
gh release create "$RELEASE_TAG" \
|
||||
--title "v${MAJOR} (latest: ${VERSION})" \
|
||||
--notes-file /tmp/release_notes.md \
|
||||
--target "$BRANCH" \
|
||||
"$ARTIFACT"
|
||||
"$REPO_ZIP" "$SOURCE_ZIP"
|
||||
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
|
||||
gh release upload "$RELEASE_TAG" "$REPO_ZIP" "$SOURCE_ZIP" --clobber
|
||||
echo "Release updated: ${RELEASE_TAG} -> ${VERSION}" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
|
||||
@@ -10,15 +10,20 @@ name: "MCP: Build & Validate"
|
||||
on:
|
||||
push:
|
||||
branches: [main, dev/**]
|
||||
paths: ['src/**', 'package.json', 'tsconfig.json']
|
||||
paths: ['source/**', 'src/**', 'package.json', 'tsconfig.json']
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths: ['src/**', 'package.json', 'tsconfig.json']
|
||||
paths: ['source/**', 'src/**', 'package.json', 'tsconfig.json']
|
||||
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
MOKOGIT_URL: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
|
||||
GIT_ORG: ${{ vars.MOKOGIT_ORG || github.repository_owner }}
|
||||
GIT_REPO: ${{ vars.MOKOGIT_REPO || github.event.repository.name }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -30,6 +35,13 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Resolve source dir
|
||||
id: srcdir
|
||||
uses: ./.mokogit/actions/resolve-source-dir
|
||||
with:
|
||||
mokogit_token: ${{ secrets.MOKOGIT_TOKEN }}
|
||||
strict: 'false'
|
||||
|
||||
- name: Setup Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
@@ -58,9 +70,14 @@ jobs:
|
||||
|
||||
- name: Count registered tools
|
||||
run: |
|
||||
TOOL_COUNT=$(grep -c "server\.tool(" src/index.ts || true)
|
||||
echo "Registered tools: ${TOOL_COUNT}"
|
||||
INDEX_FILE="${{ steps.srcdir.outputs.source_dir }}index.ts"
|
||||
if [ ! -f "${INDEX_FILE}" ]; then
|
||||
echo "::warning::resolved entrypoint ${INDEX_FILE} missing; falling back to src/index.ts"
|
||||
INDEX_FILE="src/index.ts"
|
||||
fi
|
||||
TOOL_COUNT=$(grep -c "server\.tool(" "${INDEX_FILE}" || true)
|
||||
echo "Registered tools in ${INDEX_FILE}: ${TOOL_COUNT}"
|
||||
if [ "${TOOL_COUNT}" -eq 0 ]; then
|
||||
echo "ERROR: No tools registered in src/index.ts"
|
||||
echo "ERROR: No tools registered in ${INDEX_FILE}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -24,20 +24,70 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Resolve source dir
|
||||
id: srcdir
|
||||
uses: ./.mokogit/actions/resolve-source-dir
|
||||
with:
|
||||
mokogit_token: ${{ secrets.MOKOGIT_TOKEN }}
|
||||
strict: 'true'
|
||||
|
||||
- name: Detect publishable changes
|
||||
id: detect
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# workflow_dispatch has no diff base — always treat as changed.
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
echo "::notice::workflow_dispatch — forcing changed=true"
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Build the source-dir alternative from the resolved dir (escape ERE metachars).
|
||||
# Class lists ] first and [ last to avoid a [. collating-symbol parse; the
|
||||
# \\\\& replacement reaches sed as \\& = literal backslash + matched char.
|
||||
SRC_DIR='${{ steps.srcdir.outputs.source_dir }}'
|
||||
SRC_RE=$(printf '%s' "$SRC_DIR" | sed 's/[].^$*+?(){}|[]/\\\\&/g')
|
||||
PATTERN="^(${SRC_RE}|package\.json$|package-lock\.json$|tsconfig\.json$|README\.md$)"
|
||||
echo "Detect pattern: ${PATTERN}"
|
||||
|
||||
BEFORE="${{ github.event.before }}"
|
||||
if [ -z "${BEFORE}" ] || ! git cat-file -e "${BEFORE}^{commit}" 2>/dev/null; then
|
||||
BEFORE=$(git rev-parse HEAD^ 2>/dev/null || echo "")
|
||||
fi
|
||||
if [ -n "${BEFORE}" ]; then
|
||||
FILES=$(git diff --name-only "${BEFORE}" HEAD)
|
||||
else
|
||||
FILES=$(git show --name-only --pretty=format: HEAD)
|
||||
fi
|
||||
|
||||
if printf '%s\n' "${FILES}" | grep -Eq "${PATTERN}"; then
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "::notice::No publishable changes under '${SRC_DIR}'. Files: ${FILES}"
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Setup Node.js
|
||||
if: steps.detect.outputs.changed == 'true'
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.detect.outputs.changed == 'true'
|
||||
run: npm install
|
||||
|
||||
- name: Build
|
||||
if: steps.detect.outputs.changed == 'true'
|
||||
run: npm run build
|
||||
|
||||
- name: Auto-bump patch version
|
||||
if: steps.detect.outputs.changed == 'true'
|
||||
run: |
|
||||
API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
|
||||
TOKEN="${{ secrets.MOKOGIT_TOKEN }}"
|
||||
@@ -109,6 +159,7 @@ jobs:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Publish
|
||||
if: steps.detect.outputs.changed == 'true'
|
||||
run: npm publish --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
@@ -10,13 +10,18 @@ name: "MCP: Tool Inventory"
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths: ['src/index.ts']
|
||||
paths: ['source/index.ts', 'src/index.ts']
|
||||
workflow_dispatch:
|
||||
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
MOKOGIT_URL: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
|
||||
GIT_ORG: ${{ vars.MOKOGIT_ORG || github.repository_owner }}
|
||||
GIT_REPO: ${{ vars.MOKOGIT_REPO || github.event.repository.name }}
|
||||
|
||||
jobs:
|
||||
inventory:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -24,15 +29,29 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Resolve source dir
|
||||
id: srcdir
|
||||
uses: ./.mokogit/actions/resolve-source-dir
|
||||
with:
|
||||
mokogit_token: ${{ secrets.MOKOGIT_TOKEN }}
|
||||
strict: 'false'
|
||||
|
||||
- name: Generate tool inventory
|
||||
run: |
|
||||
INDEX_FILE="${{ steps.srcdir.outputs.source_dir }}index.ts"
|
||||
if [ ! -f "${INDEX_FILE}" ]; then
|
||||
echo "::warning::resolved entrypoint ${INDEX_FILE} missing; falling back to src/index.ts"
|
||||
INDEX_FILE="src/index.ts"
|
||||
fi
|
||||
[ -r "${INDEX_FILE}" ] || { echo "::error::entry file ${INDEX_FILE} unreadable"; exit 1; }
|
||||
|
||||
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 "Auto-generated from \`${INDEX_FILE}\` 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)
|
||||
TOOL_COUNT=$(grep -c "server\.tool(" "${INDEX_FILE}" || true)
|
||||
echo "**Total tools: ${TOOL_COUNT}**" >> TOOLS.md
|
||||
echo "" >> TOOLS.md
|
||||
|
||||
@@ -40,10 +59,10 @@ jobs:
|
||||
echo "| Tool | Description |" >> TOOLS.md
|
||||
echo "|------|-------------|" >> TOOLS.md
|
||||
|
||||
grep -A1 "server\.tool(" src/index.ts | grep -E "^\s*'" | while read -r line; do
|
||||
grep -A1 "server\.tool(" "${INDEX_FILE}" | 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 "")
|
||||
DESC=$(grep -A2 "'${TOOL_NAME}'" "${INDEX_FILE}" | grep -E "^\s*'" | tail -1 | sed "s/.*'\([^']*\)'.*/\1/" || echo "")
|
||||
echo "| \`${TOOL_NAME}\` | ${DESC} |" >> TOOLS.md
|
||||
done
|
||||
|
||||
|
||||
@@ -45,6 +45,17 @@ env:
|
||||
GIT_REPO: ${{ vars.MOKOGIT_REPO || github.event.repository.name }}
|
||||
|
||||
jobs:
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
# NOTE (npm/MCP repos): This is the PHP/Joomla pre-release pipeline. It is
|
||||
# NOT a Node-aware release path — it has no setup-node / npm ci / npm build.
|
||||
# npm and MCP repos MUST NOT rely on this workflow for releasing; they use
|
||||
# 'npm-auto-release.yml' instead. The "Check platform eligibility" step
|
||||
# already sets proceed=false for non-Joomla platforms, so the PHP build
|
||||
# steps are skipped for npm repos. In addition, the PHP toolchain install
|
||||
# below is guarded on the ABSENCE of package.json so npm repos don't pull
|
||||
# in php-cli/composer. TODO(#35): once resolve-source-dir lands everywhere,
|
||||
# split this into platform-specific reusable workflows.
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
build:
|
||||
name: "Build Pre-Release (${{ inputs.stability || github.ref_name }})"
|
||||
runs-on: release
|
||||
@@ -76,7 +87,11 @@ jobs:
|
||||
echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV
|
||||
else
|
||||
echo Falling back to fresh clone
|
||||
if ! command -v composer > /dev/null 2>&1; then
|
||||
# Skip the PHP/composer toolchain install for npm/MCP repos
|
||||
# (detected via package.json). Those repos release through
|
||||
# npm-auto-release.yml and the Joomla-only steps below are gated
|
||||
# off via the eligibility check, so the toolchain is unneeded.
|
||||
if [ ! -f package.json ] && ! command -v composer > /dev/null 2>&1; then
|
||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
|
||||
fi
|
||||
rm -rf /tmp/mokocli
|
||||
@@ -86,8 +101,23 @@ jobs:
|
||||
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
# Early clean-exit for npm/MCP repos: this is the Joomla pre-release
|
||||
# pipeline and does not apply. Detecting here (before any php call)
|
||||
# avoids invoking platform_detect.php on runners where php may be
|
||||
# absent (npm repos take the fresh-clone path that skips the PHP
|
||||
# toolchain install).
|
||||
- name: Detect npm/MCP repo
|
||||
id: npmcheck
|
||||
run: |
|
||||
if [ -f package.json ]; then
|
||||
echo "is_npm=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "is_npm=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Detect platform
|
||||
id: platform
|
||||
if: steps.npmcheck.outputs.is_npm != 'true'
|
||||
run: |
|
||||
# Auto-detect and update platform if not set in manifest
|
||||
php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true
|
||||
@@ -96,7 +126,18 @@ jobs:
|
||||
- name: Check platform eligibility (Joomla only)
|
||||
id: eligibility
|
||||
run: |
|
||||
# npm/MCP repos: Joomla pre-release does not apply — clean exit.
|
||||
if [ "${{ steps.npmcheck.outputs.is_npm }}" = "true" ]; then
|
||||
echo "proceed=false" >> "$GITHUB_OUTPUT"
|
||||
echo "::notice::npm/MCP repo — pre-release (Joomla) not applicable; use npm-auto-release.yml"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
PLATFORM="${{ steps.platform.outputs.platform }}"
|
||||
# Do not silently no-op an eligible Joomla repo if detection failed.
|
||||
if [ -z "$PLATFORM" ]; then
|
||||
echo "::warning::platform detection returned empty"
|
||||
fi
|
||||
if [[ "$PLATFORM" == joomla* ]] || [[ "$PLATFORM" == "joomla" ]]; then
|
||||
echo "proceed=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
|
||||
@@ -35,9 +35,9 @@ npm run dev # Development mode
|
||||
## Architecture
|
||||
|
||||
This is an MCP (Model Context Protocol) server. Key files:
|
||||
- `src/index.ts` -- server entry point and tool registration
|
||||
- `src/config.ts` -- configuration loading
|
||||
- `src/tools/` -- individual tool implementations
|
||||
- `source/index.ts` -- server entry point and tool registration
|
||||
- `source/config.ts` -- configuration loading
|
||||
- `source/tools/` -- individual tool implementations
|
||||
- `dist/` -- compiled output (gitignored)
|
||||
|
||||
## Rules
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
PROJECT_NAME := {{PROJECT_NAME}}
|
||||
PROJECT_VERSION := 1.0.0
|
||||
|
||||
# Source directory (single source of truth cross-checked by the MokoGIT CI resolver)
|
||||
SRC_DIR := source
|
||||
|
||||
NPM := npm
|
||||
|
||||
COLOR_RESET := \033[0m
|
||||
|
||||
@@ -44,7 +44,7 @@ Template repository for creating MokoCLI-compliant Model Context Protocol (MCP)
|
||||
|
||||
| Path | Purpose |
|
||||
|---|---|
|
||||
| `src/` | TypeScript source files (server entry, tool definitions, API client) |
|
||||
| `source/` | TypeScript source files (server entry, tool definitions, API client) |
|
||||
| `config.example.json` | Example multi-connection configuration schema |
|
||||
| `config.json` | Local connection configuration (gitignored) |
|
||||
| `package.json` | npm dependencies and scripts |
|
||||
|
||||
@@ -21,7 +21,7 @@ AI Assistant <--> MCP (stdio) <--> ApiClient <--> REST API
|
||||
|
||||
## Components
|
||||
|
||||
### `src/index.ts` — Server Entry Point
|
||||
### `source/index.ts` — Server Entry Point
|
||||
|
||||
Registers all MCP tools with `McpServer` from `@modelcontextprotocol/sdk`. Each tool maps to one or more API endpoints. Uses Zod schemas for input validation.
|
||||
|
||||
@@ -30,18 +30,18 @@ Includes shared helpers:
|
||||
- `paginationQuery()` — builds pagination query params
|
||||
- `ConnectionParam` / `PaginationParams` — reusable Zod parameter spreads
|
||||
|
||||
### `src/client.ts` — HTTP Client
|
||||
### `source/client.ts` — HTTP Client
|
||||
|
||||
The `ApiClient` class handles all HTTP communication:
|
||||
- Uses `node:https` / `node:http` (not `fetch`) for reliable self-signed cert support
|
||||
- Supports GET, POST, PUT, PATCH, DELETE
|
||||
- JSON serialization/deserialization with error handling
|
||||
|
||||
### `src/config.ts` — Configuration Loader
|
||||
### `source/config.ts` — Configuration Loader
|
||||
|
||||
Loads connection details from `~/.<project>.json`. Supports multiple named connections with a configurable default.
|
||||
|
||||
### `src/types.ts` — Type Definitions
|
||||
### `source/types.ts` — Type Definitions
|
||||
|
||||
TypeScript interfaces for `ApiConnection`, `ApiConfig`, and `ApiResponse`.
|
||||
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
"start": "node dist/index.js",
|
||||
"lint": "eslint src/",
|
||||
"lint": "eslint source/",
|
||||
"setup": "node scripts/setup.mjs",
|
||||
"clean": "rm -rf dist/"
|
||||
},
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* DEFGROUP: {{PROJECT_NAME}}.Client
|
||||
* INGROUP: {{PROJECT_NAME}}
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/{{PROJECT_NAME}}
|
||||
* PATH: /src/client.ts
|
||||
* PATH: /source/client.ts
|
||||
* VERSION: 01.00.00
|
||||
* BRIEF: HTTP client for {{DISPLAY_NAME}} API
|
||||
*/
|
||||
@@ -8,7 +8,7 @@
|
||||
* DEFGROUP: {{PROJECT_NAME}}.Config
|
||||
* INGROUP: {{PROJECT_NAME}}
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/{{PROJECT_NAME}}
|
||||
* PATH: /src/config.ts
|
||||
* PATH: /source/config.ts
|
||||
* VERSION: 01.00.00
|
||||
* BRIEF: Configuration loader for {{DISPLAY_NAME}} MCP connections
|
||||
*/
|
||||
@@ -9,7 +9,7 @@
|
||||
* DEFGROUP: {{PROJECT_NAME}}.Server
|
||||
* INGROUP: {{PROJECT_NAME}}
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/{{PROJECT_NAME}}
|
||||
* PATH: /src/index.ts
|
||||
* PATH: /source/index.ts
|
||||
* VERSION: 01.00.00
|
||||
* BRIEF: MCP server entry point — registers all {{DISPLAY_NAME}} API tools
|
||||
*/
|
||||
@@ -8,7 +8,7 @@
|
||||
* DEFGROUP: {{PROJECT_NAME}}.Types
|
||||
* INGROUP: {{PROJECT_NAME}}
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/{{PROJECT_NAME}}
|
||||
* PATH: /src/types.ts
|
||||
* PATH: /source/types.ts
|
||||
* VERSION: 01.00.00
|
||||
* BRIEF: TypeScript type definitions for {{DISPLAY_NAME}} MCP server
|
||||
*/
|
||||
+2
-2
@@ -4,7 +4,7 @@
|
||||
"module": "Node16",
|
||||
"moduleResolution": "Node16",
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"rootDir": "./source",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
@@ -14,6 +14,6 @@
|
||||
"declarationMap": true,
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"include": ["source/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user