@mokoconsulting/mokoabs-mcp 0.0.0-stage → 4.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +41 -0
- package/.gitattributes +94 -0
- package/.gitmessage +9 -0
- package/.mokogit/ISSUE_TEMPLATE/adr.md +110 -0
- package/.mokogit/ISSUE_TEMPLATE/bug_report.md +48 -0
- package/.mokogit/ISSUE_TEMPLATE/config.yml +18 -0
- package/.mokogit/ISSUE_TEMPLATE/documentation.md +52 -0
- package/.mokogit/ISSUE_TEMPLATE/feature_request.md +51 -0
- package/.mokogit/ISSUE_TEMPLATE/mcp_api_integration.md +48 -0
- package/.mokogit/ISSUE_TEMPLATE/mcp_connection_issue.md +69 -0
- package/.mokogit/ISSUE_TEMPLATE/mcp_tool_request.md +50 -0
- package/.mokogit/ISSUE_TEMPLATE/question.md +82 -0
- package/.mokogit/ISSUE_TEMPLATE/rfc.md +126 -0
- package/.mokogit/ISSUE_TEMPLATE/security.md +51 -0
- package/.mokogit/ISSUE_TEMPLATE/version.md +24 -0
- package/.mokogit/actions/resolve-source-dir/action.yml +72 -0
- package/.mokogit/workflows/apidocs.yml +132 -0
- package/.mokogit/workflows/auto-assign.yml +76 -0
- package/.mokogit/workflows/auto-dev-issue.yml +207 -0
- package/.mokogit/workflows/auto-release.yml +1787 -0
- package/.mokogit/workflows/branch-cleanup.yml +63 -0
- package/.mokogit/workflows/cascade.yml +645 -0
- package/.mokogit/workflows/ci-issue-reporter.yml +100 -0
- package/.mokogit/workflows/cleanup.yml +112 -0
- package/.mokogit/workflows/makefile-lint.yml +240 -0
- package/.mokogit/workflows/npm-build-test.yml +507 -0
- package/.mokogit/workflows/npm-sdk-check.yml +110 -0
- package/.mokogit/workflows/npm-tool-inventory.yml +81 -0
- package/.mokogit/workflows/pr-branch-check.yml +92 -0
- package/.mokogit/workflows/pre-release.yml +1134 -0
- package/.mokogit/workflows/rc-revert.yml +75 -0
- package/AGENTS.md +48 -0
- package/CHANGELOG.md +37 -0
- package/CODE_OF_CONDUCT.md +70 -0
- package/CONTRIBUTING.md +164 -0
- package/LICENSE +696 -0
- package/Makefile +94 -0
- package/README.md +109 -2
- package/SECURITY.md +245 -0
- package/build/README.md +7 -0
- package/dist/README.md +7 -0
- package/dist/index.js +153 -0
- package/package.json +34 -5
- package/sample/config.example.json +15 -0
- package/source/client.ts +132 -0
- package/source/config.ts +139 -0
- package/source/credentialStore.ts +216 -0
- package/source/index.ts +235 -0
- package/source/tokenReader.ts +120 -0
- package/source/types.ts +63 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
# MCP Server Build & Validation
|
|
2
|
+
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
#
|
|
5
|
+
# Builds the MCP server via the centralized MokoCLI build agent
|
|
6
|
+
# (moko lint|build|test), then validates that compiled output exists and that
|
|
7
|
+
# tools are properly registered. The build agent is platform-aware via the
|
|
8
|
+
# repo's NATIVE manifest (package.json) — no metadata API call — so inline
|
|
9
|
+
# Deps are installed via `npm ci` below so `moko lint|build|test` (which run
|
|
10
|
+
# `npx --no-install tsc`/eslint) can find typescript/eslint from devDependencies.
|
|
11
|
+
#
|
|
12
|
+
# moko: the build job bootstraps mokocli at runtime — it prefers a pre-installed
|
|
13
|
+
# /opt/mokocli, else installs fresh from the public MokoGIT Composer registry (the
|
|
14
|
+
# same tokenless path the release workflows use) and puts `moko` first on PATH, so
|
|
15
|
+
# `moko lint|build|test` resolve without waiting on a runner-image rebuild.
|
|
16
|
+
|
|
17
|
+
name: "MCP: Build & Validate"
|
|
18
|
+
|
|
19
|
+
on:
|
|
20
|
+
push:
|
|
21
|
+
branches: [main, stable, dev, dev/**]
|
|
22
|
+
paths: ['source/**', 'src/**', 'package.json', 'tsconfig.json']
|
|
23
|
+
pull_request:
|
|
24
|
+
branches: [main, stable, rc, dev]
|
|
25
|
+
# No paths filter: the folded-in universal pr-check jobs (Branch Policy,
|
|
26
|
+
# Require Docs, Wiki Reminder) must run on EVERY PR.
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
permissions:
|
|
30
|
+
contents: read
|
|
31
|
+
pull-requests: write
|
|
32
|
+
issues: write # folded-in Wiki Reminder posts a sticky PR comment
|
|
33
|
+
|
|
34
|
+
env:
|
|
35
|
+
MOKOGIT_URL: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
|
|
36
|
+
GIT_ORG: ${{ vars.MOKOGIT_ORG || github.repository_owner }}
|
|
37
|
+
GIT_REPO: ${{ vars.MOKOGIT_REPO || github.event.repository.name }}
|
|
38
|
+
|
|
39
|
+
jobs:
|
|
40
|
+
build:
|
|
41
|
+
runs-on: ubuntu-latest
|
|
42
|
+
if: ${{ !startsWith(github.event.repository.name, 'Template-') }}
|
|
43
|
+
strategy:
|
|
44
|
+
matrix:
|
|
45
|
+
node-version: [20, 22]
|
|
46
|
+
|
|
47
|
+
steps:
|
|
48
|
+
- uses: actions/checkout@v4
|
|
49
|
+
|
|
50
|
+
- name: Resolve source dir
|
|
51
|
+
id: srcdir
|
|
52
|
+
uses: ./.mokogit/actions/resolve-source-dir
|
|
53
|
+
with:
|
|
54
|
+
mokogit_token: ${{ secrets.MOKOGIT_TOKEN }}
|
|
55
|
+
strict: 'false'
|
|
56
|
+
|
|
57
|
+
- name: Setup Node.js ${{ matrix.node-version }}
|
|
58
|
+
uses: actions/setup-node@v4
|
|
59
|
+
with:
|
|
60
|
+
node-version: ${{ matrix.node-version }}
|
|
61
|
+
|
|
62
|
+
- name: Install npm dependencies
|
|
63
|
+
run: |
|
|
64
|
+
if [ -f package-lock.json ]; then npm ci; else npm install --no-audit --no-fund; fi
|
|
65
|
+
|
|
66
|
+
- name: Setup MokoCLI build agent (moko on PATH)
|
|
67
|
+
env:
|
|
68
|
+
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
|
|
69
|
+
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
|
70
|
+
run: |
|
|
71
|
+
# Reach mokocli the SAME way the release workflows' "Setup MokoCLI tools"
|
|
72
|
+
# step does: prefer the pre-installed /opt/mokocli, else fall back to a
|
|
73
|
+
# fresh clone. The only difference is we expose the `moko` binary on PATH
|
|
74
|
+
# (mokocli's cli/ dir) so `moko build|test|lint` resolve — we do NOT invent
|
|
75
|
+
# a new install mechanism.
|
|
76
|
+
# Heal a non-executable pre-installed binary before trusting it: some
|
|
77
|
+
# runners carry a /opt/mokocli/cli/moko WITHOUT the execute bit, and the
|
|
78
|
+
# old `-x || -f` guard accepted it via `-f` → the next step (`moko lint`)
|
|
79
|
+
# aborts with exit 126 "Permission denied" on every such runner. chmod +x
|
|
80
|
+
# the failsafe, then require a genuine `-x` before committing to it — else
|
|
81
|
+
# fall through to the fresh tokenless bootstrap below. (Template-NPM #142;
|
|
82
|
+
# same class as MokoCLI #650 / Template-NPM #138 for the release workflows.)
|
|
83
|
+
sudo chmod +x /opt/mokocli/*/moko 2>/dev/null || true
|
|
84
|
+
if [ -x /opt/mokocli/cli/moko ]; then
|
|
85
|
+
echo Using pre-installed /opt/mokocli
|
|
86
|
+
# Refresh the shared cache so lint/build run CURRENT mokocli. The release
|
|
87
|
+
# workflows already pull /opt/mokocli; npm-build-test must too — otherwise a
|
|
88
|
+
# stale checkout keeps running old code (e.g. `npx --no-install eslint`
|
|
89
|
+
# instead of the local-bin lint that cleanly skips an absent eslint), which
|
|
90
|
+
# reds every npm repo until the host cron happens to refresh it. Best-effort:
|
|
91
|
+
# a refresh hiccup must never fail the build.
|
|
92
|
+
if git -C /opt/mokocli rev-parse --git-dir > /dev/null 2>&1; then
|
|
93
|
+
git -C /opt/mokocli pull --ff-only --quiet 2>/dev/null || echo " (mokocli refresh skipped)"
|
|
94
|
+
fi
|
|
95
|
+
echo "/opt/mokocli/cli" >> "$GITHUB_PATH"
|
|
96
|
+
else
|
|
97
|
+
echo Falling back to tokenless registry bootstrap
|
|
98
|
+
if ! command -v composer > /dev/null 2>&1; then
|
|
99
|
+
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
|
|
100
|
+
fi
|
|
101
|
+
rm -rf /tmp/mokocli
|
|
102
|
+
# Tokenless: install mokocli from the public-readable MokoGIT Composer registry
|
|
103
|
+
# (metadata + dist need no auth), which serves the CURRENT published release.
|
|
104
|
+
# A runner-local git mirror can be stale (observed: moko v04.00.15 running
|
|
105
|
+
# `npm ci`), so the registry is preferred; authenticated git clone is a
|
|
106
|
+
# last-resort fallback only if the registry bootstrap fails.
|
|
107
|
+
REG='{"type":"composer","url":"https://git.mokoconsulting.tech/api/packages/MokoConsulting/composer"}'
|
|
108
|
+
if composer create-project --no-dev --no-interaction --no-progress --quiet --repository="${REG}" mokoconsulting/mokocli /tmp/mokocli >/dev/null 2>&1 && { [ -x /tmp/mokocli/cli/moko ] || [ -f /tmp/mokocli/cli/moko ]; }; then
|
|
109
|
+
echo "Bootstrapped mokocli from the Composer registry (tokenless)."
|
|
110
|
+
else
|
|
111
|
+
echo "Registry bootstrap unavailable; falling back to authenticated git clone."
|
|
112
|
+
MOKO_CLONE_TOKEN="$(printf '%s' "${MOKO_CLONE_TOKEN}" | tr -d '[:space:]')"
|
|
113
|
+
echo "::add-mask::${MOKO_CLONE_TOKEN}"
|
|
114
|
+
if [ -z "${MOKO_CLONE_TOKEN}" ]; then
|
|
115
|
+
echo "::error::mokocli registry bootstrap failed and MOKOGIT_TOKEN is empty — cannot clone fallback." >&2
|
|
116
|
+
exit 1
|
|
117
|
+
fi
|
|
118
|
+
rm -rf /tmp/mokocli
|
|
119
|
+
git clone --depth 1 --branch stable --quiet "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git" /tmp/mokocli
|
|
120
|
+
( cd /tmp/mokocli && composer install --no-dev --no-interaction --no-progress --optimize-autoloader --quiet )
|
|
121
|
+
fi
|
|
122
|
+
# Ensure the freshly-bootstrapped binary is executable (the registry
|
|
123
|
+
# dist / git clone can land it without +x on some runners).
|
|
124
|
+
chmod +x /tmp/mokocli/*/moko 2>/dev/null || true
|
|
125
|
+
echo "/tmp/mokocli/cli" >> "$GITHUB_PATH"
|
|
126
|
+
fi
|
|
127
|
+
|
|
128
|
+
- name: Lint
|
|
129
|
+
run: moko lint
|
|
130
|
+
|
|
131
|
+
- name: Build
|
|
132
|
+
run: moko build
|
|
133
|
+
|
|
134
|
+
- name: Test
|
|
135
|
+
run: |
|
|
136
|
+
# Skip cleanly when the package defines no "test" script, so a repo
|
|
137
|
+
# with genuinely no tests does not red-fail the build. moko test runs
|
|
138
|
+
# the script when it exists.
|
|
139
|
+
if node -e "process.exit(((require('./package.json').scripts)||{}).test?0:1)" 2>/dev/null; then
|
|
140
|
+
moko test
|
|
141
|
+
else
|
|
142
|
+
echo "::notice::no \"test\" script in package.json -- skipping test step"
|
|
143
|
+
fi
|
|
144
|
+
|
|
145
|
+
- name: Verify dist output exists
|
|
146
|
+
run: |
|
|
147
|
+
# dist/index.js is the bin/main entry for every MCP server (shared
|
|
148
|
+
# package.json convention). Assert that + that the build emitted
|
|
149
|
+
# compiled output. Do NOT hard-code other filenames: source layouts
|
|
150
|
+
# differ per server (many have no client.ts/types.ts/root config.ts),
|
|
151
|
+
# which false-failed this check on every build fleet-wide.
|
|
152
|
+
test -f dist/index.js || (echo "ERROR: dist/index.js not found" && exit 1)
|
|
153
|
+
JS_COUNT=$(find dist -name '*.js' | wc -l)
|
|
154
|
+
[ "${JS_COUNT}" -gt 0 ] || (echo "ERROR: no compiled .js files in dist/" && exit 1)
|
|
155
|
+
echo "✓ dist/index.js present; ${JS_COUNT} compiled .js file(s) in dist/"
|
|
156
|
+
|
|
157
|
+
- name: Verify shebang in index.js
|
|
158
|
+
run: |
|
|
159
|
+
head -1 dist/index.js | grep -q "#!/usr/bin/env node" || echo "WARNING: Missing shebang in dist/index.js"
|
|
160
|
+
|
|
161
|
+
- name: Count registered tools
|
|
162
|
+
run: |
|
|
163
|
+
# Tools may be registered in the entry file OR in modules it imports
|
|
164
|
+
# (modular servers call server.tool( / .registerTool( inside
|
|
165
|
+
# source/tools/*.ts). Count registrations across the whole source
|
|
166
|
+
# tree, not just index.ts, else modular servers false-report zero.
|
|
167
|
+
SRC_DIR="${{ steps.srcdir.outputs.source_dir }}"
|
|
168
|
+
[ -n "${SRC_DIR}" ] && [ -d "${SRC_DIR}" ] || SRC_DIR="source"
|
|
169
|
+
[ -d "${SRC_DIR}" ] || SRC_DIR="src"
|
|
170
|
+
# `grep` exits 1 when it finds no matches; under `set -e -o pipefail`
|
|
171
|
+
# that aborts the step before the MCP-vs-non-MCP check below can run.
|
|
172
|
+
# Swallow the no-match exit so the count is a real 0, not a hard fail.
|
|
173
|
+
TOOL_COUNT=$( { grep -rE "server\.tool\(|\.registerTool\(" "${SRC_DIR}" 2>/dev/null || true; } | wc -l | tr -d ' ')
|
|
174
|
+
# Only MCP *servers* must register tools. Detect that from package.json
|
|
175
|
+
# (any @modelcontextprotocol/* dep) so non-MCP npm packages — client
|
|
176
|
+
# CLIs, libraries — aren't force-failed by an inapplicable check. When
|
|
177
|
+
# this npm CI template lands on a non-MCP repo the tool count is 0 by
|
|
178
|
+
# design; treat that as advisory, not a hard error.
|
|
179
|
+
IS_MCP=$(node -e "try{var p=require('./package.json');var d=Object.assign({},p.dependencies,p.devDependencies,p.peerDependencies);process.stdout.write(Object.keys(d).some(function(k){return k.indexOf('@modelcontextprotocol/')===0;})?'1':'0');}catch(e){process.stdout.write('0');}")
|
|
180
|
+
echo "Registered tool calls under ${SRC_DIR}: ${TOOL_COUNT} (mcp-server=${IS_MCP})"
|
|
181
|
+
if [ "${TOOL_COUNT}" -eq 0 ]; then
|
|
182
|
+
if [ "${IS_MCP}" = "1" ]; then
|
|
183
|
+
echo "ERROR: MCP server declares @modelcontextprotocol/* but registers no tools under ${SRC_DIR}"
|
|
184
|
+
exit 1
|
|
185
|
+
fi
|
|
186
|
+
echo "::warning::No MCP tool registrations found and no @modelcontextprotocol/* dependency — treating as a non-MCP npm package (advisory, not failing)."
|
|
187
|
+
fi
|
|
188
|
+
|
|
189
|
+
# NOTE: a standalone `ci-summary` job (needs: [build], if: always()) was removed.
|
|
190
|
+
# This Gitea act_runner never schedules a `needs:`+`if: always()` job — it parks in
|
|
191
|
+
# "waiting" (runner_id 0) forever and never dispatches. Under the org-governance
|
|
192
|
+
# `main` rule (status_check_contexts: ["*"], every check must be green) that stranded
|
|
193
|
+
# job was a permanent, silent block on every PR into main. The per-job status is
|
|
194
|
+
# already visible on the Actions run page and `wiki-reminder` posts a sticky PR
|
|
195
|
+
# comment on main PRs, so a separate summary job earned its keep only when it worked —
|
|
196
|
+
# which, on this runner, was never. If a build-result summary comment is wanted back,
|
|
197
|
+
# add it as a final STEP with `if: ${{ always() }}` inside the `build` job (step-level
|
|
198
|
+
# always() IS supported), not as a dependent job. (Matches ci-go.yml.)
|
|
199
|
+
|
|
200
|
+
# ══════════════════════════════════════════════════════════════════════════
|
|
201
|
+
# Universal PR-check jobs — folded in from "Universal: PR Check" so the NPM
|
|
202
|
+
# platform runs one CI workflow per PR (mirrors Template-Joomla #162).
|
|
203
|
+
# Branch-gated: Branch Policy on all PRs; docs gate + wiki reminder on main.
|
|
204
|
+
# ══════════════════════════════════════════════════════════════════════════
|
|
205
|
+
|
|
206
|
+
branch-policy:
|
|
207
|
+
name: Branch Policy
|
|
208
|
+
runs-on: ubuntu-latest
|
|
209
|
+
steps:
|
|
210
|
+
- name: Check branch merge target
|
|
211
|
+
env:
|
|
212
|
+
# Compare against the repo's real production branch, not a hardcoded
|
|
213
|
+
# 'main'. MokoGIT renamed the production channel main -> stable
|
|
214
|
+
# fleet-wide (ladder: dev -> rc -> stable); hardcoding 'main'
|
|
215
|
+
# red-failed every legitimate dev->stable / rc->stable promotion.
|
|
216
|
+
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
|
|
217
|
+
run: |
|
|
218
|
+
HEAD="${{ github.head_ref }}"
|
|
219
|
+
BASE="${{ github.base_ref }}"
|
|
220
|
+
PROD="${DEFAULT_BRANCH:-main}"
|
|
221
|
+
|
|
222
|
+
echo "PR: ${HEAD} → ${BASE} (production=${PROD})"
|
|
223
|
+
|
|
224
|
+
ALLOWED=true
|
|
225
|
+
REASON=""
|
|
226
|
+
|
|
227
|
+
case "$HEAD" in
|
|
228
|
+
feature/*|feat/*)
|
|
229
|
+
if [ "$BASE" != "dev" ]; then
|
|
230
|
+
ALLOWED=false
|
|
231
|
+
REASON="Feature branches must target 'dev', not '${BASE}'"
|
|
232
|
+
fi
|
|
233
|
+
;;
|
|
234
|
+
fix/*|bugfix/*)
|
|
235
|
+
if [ "$BASE" != "dev" ] && [ "$BASE" != "$PROD" ]; then
|
|
236
|
+
ALLOWED=false
|
|
237
|
+
REASON="Fix branches must target 'dev' or '${PROD}', not '${BASE}'"
|
|
238
|
+
fi
|
|
239
|
+
;;
|
|
240
|
+
patch/*)
|
|
241
|
+
if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ] && [ "$BASE" != "$PROD" ]; then
|
|
242
|
+
ALLOWED=false
|
|
243
|
+
REASON="Patch branches must target 'dev', 'rc', or '${PROD}', not '${BASE}'"
|
|
244
|
+
fi
|
|
245
|
+
;;
|
|
246
|
+
hotfix/*)
|
|
247
|
+
if [ "$BASE" != "dev" ] && [ "$BASE" != "$PROD" ]; then
|
|
248
|
+
ALLOWED=false
|
|
249
|
+
REASON="Hotfix branches can only target 'dev' or '${PROD}', not '${BASE}'"
|
|
250
|
+
fi
|
|
251
|
+
;;
|
|
252
|
+
rc|rc/*)
|
|
253
|
+
if [ "$BASE" != "$PROD" ]; then
|
|
254
|
+
ALLOWED=false
|
|
255
|
+
REASON="RC branch can only merge into '${PROD}', not '${BASE}'"
|
|
256
|
+
fi
|
|
257
|
+
;;
|
|
258
|
+
dev)
|
|
259
|
+
if [ "$BASE" != "rc" ] && [ "$BASE" != "$PROD" ]; then
|
|
260
|
+
ALLOWED=false
|
|
261
|
+
REASON="Dev branch can only merge into 'rc' or '${PROD}', not '${BASE}'"
|
|
262
|
+
fi
|
|
263
|
+
;;
|
|
264
|
+
esac
|
|
265
|
+
|
|
266
|
+
if [ "$ALLOWED" = false ]; then
|
|
267
|
+
echo "::error::${REASON}"
|
|
268
|
+
echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY
|
|
269
|
+
echo "" >> $GITHUB_STEP_SUMMARY
|
|
270
|
+
echo "${REASON}" >> $GITHUB_STEP_SUMMARY
|
|
271
|
+
echo "" >> $GITHUB_STEP_SUMMARY
|
|
272
|
+
echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
|
|
273
|
+
echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
|
|
274
|
+
echo "- \`fix/*\` → \`dev\` or \`${PROD}\`" >> $GITHUB_STEP_SUMMARY
|
|
275
|
+
echo "- \`patch/*\` → \`dev\`, \`rc\`, or \`${PROD}\`" >> $GITHUB_STEP_SUMMARY
|
|
276
|
+
echo "- \`hotfix/*\` → \`dev\` or \`${PROD}\`" >> $GITHUB_STEP_SUMMARY
|
|
277
|
+
echo "- \`dev\` → \`rc\` or \`${PROD}\`" >> $GITHUB_STEP_SUMMARY
|
|
278
|
+
echo "- \`rc/*\` → \`${PROD}\`" >> $GITHUB_STEP_SUMMARY
|
|
279
|
+
exit 1
|
|
280
|
+
fi
|
|
281
|
+
|
|
282
|
+
echo "Branch policy: OK (${HEAD} → ${BASE})"
|
|
283
|
+
echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
|
|
284
|
+
|
|
285
|
+
# ── Docs Update Gate (wiki-aware) ───────────────────────────────────────
|
|
286
|
+
#
|
|
287
|
+
# RULE (in order):
|
|
288
|
+
# 1. Short-circuit: if the PR diff touches CHANGELOG.md OR README.md, PASS
|
|
289
|
+
# immediately (deterministic; no wiki lookup).
|
|
290
|
+
# 2. Else consult the repo WIKI (<owner>/<repo>.wiki.git) HEAD commit date.
|
|
291
|
+
# Mode is chosen by the org/repo variable DOCS_WIKI_WINDOW (default 'pr'):
|
|
292
|
+
# - 'pr' → satisfied if wiki HEAD commit date >= this PR's
|
|
293
|
+
# created_at (docs touched since the PR opened).
|
|
294
|
+
# - '<N>h'/'<N>d' → satisfied if wiki HEAD commit is within that rolling
|
|
295
|
+
# window (now - duration). e.g. 24h, 72h, 7d.
|
|
296
|
+
# Fail-open: if the wiki is absent/unreachable or the lookup errors, do
|
|
297
|
+
# NOT hard-fail — treat as "unsatisfied → warn" per step 3.
|
|
298
|
+
# 3. Verdict: if neither is satisfied → emit ::warning:: (advisory) and PASS
|
|
299
|
+
# on non-main branches; hard-fail (exit 1) ONLY when the PR base is 'main'
|
|
300
|
+
# (release hygiene on the stable line).
|
|
301
|
+
#
|
|
302
|
+
# The job runs on EVERY PR so the required status context "Require Docs Update"
|
|
303
|
+
# always reports; the base-branch gate is enforced inside the step, not via a
|
|
304
|
+
# job-level `if:` that would leave the context missing on non-main PRs.
|
|
305
|
+
require-docs:
|
|
306
|
+
name: Require Docs Update
|
|
307
|
+
runs-on: ubuntu-latest
|
|
308
|
+
steps:
|
|
309
|
+
- name: Checkout
|
|
310
|
+
uses: actions/checkout@v4
|
|
311
|
+
with:
|
|
312
|
+
fetch-depth: 0
|
|
313
|
+
|
|
314
|
+
- name: Docs gate (docs diff, else wiki freshness)
|
|
315
|
+
env:
|
|
316
|
+
BASE_REF: ${{ github.base_ref }}
|
|
317
|
+
HEAD_REF: ${{ github.head_ref }}
|
|
318
|
+
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
|
319
|
+
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
|
320
|
+
PR_CREATED_AT: ${{ github.event.pull_request.created_at }}
|
|
321
|
+
SERVER: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
|
|
322
|
+
REPO: ${{ github.repository }}
|
|
323
|
+
TOKEN: ${{ github.token }}
|
|
324
|
+
DOCS_WIKI_WINDOW: ${{ vars.DOCS_WIKI_WINDOW || 'pr' }}
|
|
325
|
+
run: |
|
|
326
|
+
set -uo pipefail
|
|
327
|
+
|
|
328
|
+
# ── Step 1: docs diff short-circuit ─────────────────────────────
|
|
329
|
+
CHANGED="$(git diff --name-only "$BASE_SHA" "$HEAD_SHA" 2>/dev/null || true)"
|
|
330
|
+
if [ -z "$CHANGED" ]; then
|
|
331
|
+
git fetch -q origin "$BASE_REF" 2>/dev/null || true
|
|
332
|
+
CHANGED="$(git diff --name-only "origin/${BASE_REF}...HEAD" 2>/dev/null || true)"
|
|
333
|
+
fi
|
|
334
|
+
echo "Changed files in PR:"
|
|
335
|
+
echo "$CHANGED"
|
|
336
|
+
if echo "$CHANGED" | grep -qxE 'CHANGELOG\.md|README\.md'; then
|
|
337
|
+
echo "Docs diff present (CHANGELOG.md or README.md) — gate satisfied."
|
|
338
|
+
echo "## Docs Update: Passed (docs diff)" >> "$GITHUB_STEP_SUMMARY"
|
|
339
|
+
exit 0
|
|
340
|
+
fi
|
|
341
|
+
|
|
342
|
+
# ── Step 2: wiki freshness (fail-open) ──────────────────────────
|
|
343
|
+
# Resolve wiki HEAD commit epoch via a shallow clone of the wiki repo.
|
|
344
|
+
# Any failure here is non-fatal: SATISFIED stays 0 → handled by step 3.
|
|
345
|
+
SATISFIED=0
|
|
346
|
+
WIKI_EPOCH=""
|
|
347
|
+
TOK="$(printf '%s' "${TOKEN:-}" | tr -d '[:space:]')"
|
|
348
|
+
HOSTPATH="${SERVER#https://}"; HOSTPATH="${HOSTPATH#http://}"
|
|
349
|
+
WIKI_URL="https://x-access-token:${TOK}@${HOSTPATH}/${REPO}.wiki.git"
|
|
350
|
+
echo "::add-mask::${TOK}"
|
|
351
|
+
rm -rf /tmp/docs-wiki
|
|
352
|
+
if git clone --depth 1 --quiet "${WIKI_URL}" /tmp/docs-wiki 2>/dev/null; then
|
|
353
|
+
WIKI_EPOCH="$(git -C /tmp/docs-wiki log -1 --format=%ct 2>/dev/null || true)"
|
|
354
|
+
fi
|
|
355
|
+
|
|
356
|
+
if [ -z "$WIKI_EPOCH" ]; then
|
|
357
|
+
echo "::warning::wiki absent/unreachable — treating docs gate as unsatisfied (fail-open)"
|
|
358
|
+
else
|
|
359
|
+
MODE="$(printf '%s' "${DOCS_WIKI_WINDOW}" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')"
|
|
360
|
+
[ -n "$MODE" ] || MODE="pr"
|
|
361
|
+
if [ "$MODE" = "pr" ]; then
|
|
362
|
+
# PASS if wiki HEAD commit date >= PR created_at.
|
|
363
|
+
PR_EPOCH="$(date -u -d "${PR_CREATED_AT}" +%s 2>/dev/null || true)"
|
|
364
|
+
if [ -z "$PR_EPOCH" ]; then
|
|
365
|
+
echo "::warning::could not parse PR created_at ('${PR_CREATED_AT}') — docs gate unsatisfied (fail-open)"
|
|
366
|
+
elif [ "$WIKI_EPOCH" -ge "$PR_EPOCH" ]; then
|
|
367
|
+
SATISFIED=1
|
|
368
|
+
echo "Wiki updated since PR opened (wiki=${WIKI_EPOCH} >= pr=${PR_EPOCH})."
|
|
369
|
+
else
|
|
370
|
+
echo "Wiki not updated since PR opened (wiki=${WIKI_EPOCH} < pr=${PR_EPOCH})."
|
|
371
|
+
fi
|
|
372
|
+
else
|
|
373
|
+
# Duration window: <N>h / <N>d (also bare <N> = seconds).
|
|
374
|
+
NUM="$(printf '%s' "$MODE" | sed -E 's/[^0-9].*$//')"
|
|
375
|
+
UNIT="$(printf '%s' "$MODE" | sed -E 's/^[0-9]+//')"
|
|
376
|
+
SECS=0
|
|
377
|
+
case "$UNIT" in
|
|
378
|
+
h) SECS=$(( NUM * 3600 )) ;;
|
|
379
|
+
d) SECS=$(( NUM * 86400 )) ;;
|
|
380
|
+
m) SECS=$(( NUM * 60 )) ;;
|
|
381
|
+
s|"") SECS=$(( NUM )) ;;
|
|
382
|
+
*) echo "::warning::unrecognized DOCS_WIKI_WINDOW unit '${UNIT}'; falling back to 'pr'-style (>= PR created_at)"
|
|
383
|
+
PR_EPOCH="$(date -u -d "${PR_CREATED_AT}" +%s 2>/dev/null || echo 0)"
|
|
384
|
+
SECS=0 ;;
|
|
385
|
+
esac
|
|
386
|
+
if [ "$SECS" -gt 0 ]; then
|
|
387
|
+
NOW="$(date -u +%s)"
|
|
388
|
+
CUTOFF=$(( NOW - SECS ))
|
|
389
|
+
if [ "$WIKI_EPOCH" -ge "$CUTOFF" ]; then
|
|
390
|
+
SATISFIED=1
|
|
391
|
+
echo "Wiki updated within window ${MODE} (wiki=${WIKI_EPOCH} >= cutoff=${CUTOFF})."
|
|
392
|
+
else
|
|
393
|
+
echo "Wiki stale beyond window ${MODE} (wiki=${WIKI_EPOCH} < cutoff=${CUTOFF})."
|
|
394
|
+
fi
|
|
395
|
+
elif [ "${PR_EPOCH:-0}" -gt 0 ] && [ "$WIKI_EPOCH" -ge "${PR_EPOCH}" ]; then
|
|
396
|
+
SATISFIED=1
|
|
397
|
+
fi
|
|
398
|
+
fi
|
|
399
|
+
fi
|
|
400
|
+
|
|
401
|
+
# ── Step 3: verdict ─────────────────────────────────────────────
|
|
402
|
+
if [ "$SATISFIED" = "1" ]; then
|
|
403
|
+
echo "## Docs Update: Passed (wiki fresh)" >> "$GITHUB_STEP_SUMMARY"
|
|
404
|
+
exit 0
|
|
405
|
+
fi
|
|
406
|
+
|
|
407
|
+
if [ "$BASE_REF" = "main" ]; then
|
|
408
|
+
echo "::error::Docs gate unsatisfied: no CHANGELOG.md/README.md change and wiki not fresh (DOCS_WIKI_WINDOW='${DOCS_WIKI_WINDOW}'). Required on PRs into main."
|
|
409
|
+
{
|
|
410
|
+
echo "## Docs Update Required"
|
|
411
|
+
echo ""
|
|
412
|
+
echo "PRs merging into \`main\` must either update **CHANGELOG.md** or **README.md**, or the repo **wiki** must be fresh (mode: \`${DOCS_WIKI_WINDOW}\`)."
|
|
413
|
+
} >> "$GITHUB_STEP_SUMMARY"
|
|
414
|
+
exit 1
|
|
415
|
+
fi
|
|
416
|
+
|
|
417
|
+
echo "::warning::Docs gate unsatisfied (advisory on non-main branch '${BASE_REF}'): update CHANGELOG.md/README.md or the wiki."
|
|
418
|
+
{
|
|
419
|
+
echo "## Docs Update: Advisory"
|
|
420
|
+
echo ""
|
|
421
|
+
echo "No docs diff and wiki not fresh (mode: \`${DOCS_WIKI_WINDOW}\`). Non-blocking on \`${BASE_REF}\`; will hard-fail on \`main\`."
|
|
422
|
+
} >> "$GITHUB_STEP_SUMMARY"
|
|
423
|
+
exit 0
|
|
424
|
+
|
|
425
|
+
wiki-reminder:
|
|
426
|
+
name: Wiki Update Reminder
|
|
427
|
+
runs-on: ubuntu-latest
|
|
428
|
+
if: ${{ github.base_ref == github.event.repository.default_branch }}
|
|
429
|
+
steps:
|
|
430
|
+
- name: Remind to update the wiki
|
|
431
|
+
env:
|
|
432
|
+
TOKEN: ${{ github.token }}
|
|
433
|
+
SERVER: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
|
|
434
|
+
REPO: ${{ github.repository }}
|
|
435
|
+
PR: ${{ github.event.pull_request.number }}
|
|
436
|
+
run: |
|
|
437
|
+
set -uo pipefail
|
|
438
|
+
{
|
|
439
|
+
echo "## Wiki Update Reminder"
|
|
440
|
+
echo ""
|
|
441
|
+
echo "Docs are **wiki-first** at MokoConsulting. If this change affects behavior, usage, configuration, or standards, update the repo wiki:"
|
|
442
|
+
echo ""
|
|
443
|
+
echo "- ${SERVER}/${REPO}/wiki"
|
|
444
|
+
echo ""
|
|
445
|
+
echo "_Non-blocking reminder._"
|
|
446
|
+
} >> "$GITHUB_STEP_SUMMARY"
|
|
447
|
+
# Sticky PR comment: create-or-UPDATE a single comment keyed by a hidden
|
|
448
|
+
# marker (no duplicate reminders across pushes). Best-effort, never fails.
|
|
449
|
+
LIST="${SERVER}/api/v1/repos/${REPO}/issues/${PR}/comments"
|
|
450
|
+
EDIT="${SERVER}/api/v1/repos/${REPO}/issues/comments"
|
|
451
|
+
MARKER="<!-- wiki-reminder -->"
|
|
452
|
+
BODY="$(printf '%s\n\n%s' "${MARKER}" "**Wiki reminder:** docs are wiki-first -- if this PR changes behavior, usage, config, or standards, please update the repo wiki before/after merge. _(non-blocking)_")"
|
|
453
|
+
if [ -n "${TOKEN:-}" ] && [ -n "${PR:-}" ]; then
|
|
454
|
+
CID="$(curl -sf -H "Authorization: token ${TOKEN}" "${LIST}?limit=100" 2>/dev/null \
|
|
455
|
+
| python3 -c "import sys,json; c=[x for x in json.load(sys.stdin) if '${MARKER}' in (x.get('body') or '')]; print(c[-1]['id'] if c else '')" 2>/dev/null || true)"
|
|
456
|
+
PAYLOAD="$(python3 -c "import json,sys; print(json.dumps({'body': sys.stdin.read()}))" <<<"${BODY}")"
|
|
457
|
+
if [ -n "${CID}" ]; then
|
|
458
|
+
curl -sf -X PATCH -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" -d "${PAYLOAD}" "${EDIT}/${CID}" >/dev/null 2>&1 || true
|
|
459
|
+
else
|
|
460
|
+
curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" -d "${PAYLOAD}" "${LIST}" >/dev/null 2>&1 || true
|
|
461
|
+
fi
|
|
462
|
+
fi
|
|
463
|
+
echo "Wiki reminder emitted (non-blocking, sticky)."
|
|
464
|
+
|
|
465
|
+
# ── Universal PR gate checks (folded from pr-check's Validate PR) ─────────
|
|
466
|
+
pr-gate-checks:
|
|
467
|
+
name: PR Gate Checks
|
|
468
|
+
runs-on: ubuntu-latest
|
|
469
|
+
steps:
|
|
470
|
+
- name: Checkout
|
|
471
|
+
uses: actions/checkout@v4
|
|
472
|
+
|
|
473
|
+
- name: Check for merge conflict markers
|
|
474
|
+
run: |
|
|
475
|
+
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --exclude-dir='.git' --exclude-dir='.mokogit' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
|
|
476
|
+
if [ -n "$CONFLICTS" ]; then
|
|
477
|
+
echo "::error::Merge conflict markers found in source files"
|
|
478
|
+
echo "## Conflict Markers Found" >> $GITHUB_STEP_SUMMARY
|
|
479
|
+
echo '```' >> $GITHUB_STEP_SUMMARY
|
|
480
|
+
echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
|
|
481
|
+
echo '```' >> $GITHUB_STEP_SUMMARY
|
|
482
|
+
exit 1
|
|
483
|
+
fi
|
|
484
|
+
echo "No conflict markers found"
|
|
485
|
+
|
|
486
|
+
- name: Reject .gitea and .github workflow dirs
|
|
487
|
+
run: |
|
|
488
|
+
# .gitea/ and .github/ carry their own workflows that CONFLICT with the
|
|
489
|
+
# MokoGIT .mokogit/workflows/ system (duplicate / competing runs). They
|
|
490
|
+
# must never exist in-repo. Fail the PR if either directory is tracked so
|
|
491
|
+
# it gets deleted.
|
|
492
|
+
HITS=$(git ls-files | grep -E '^(\.gitea|\.github)/workflows/' || true)
|
|
493
|
+
if [ -n "$HITS" ]; then
|
|
494
|
+
echo "::error::.gitea/ and .github/ are not allowed - their workflows conflict with .mokogit/workflows/. Delete these directories."
|
|
495
|
+
printf '%s\n' "$HITS" | while IFS= read -r f; do
|
|
496
|
+
[ -n "$f" ] && echo "::error file=${f}::Remove ${f} (.gitea/.github workflows conflict with .mokogit/)"
|
|
497
|
+
done
|
|
498
|
+
{
|
|
499
|
+
echo "## Blocked: .gitea/.github present"
|
|
500
|
+
echo "These directories conflict with the MokoGIT \`.mokogit/workflows/\` system and must be deleted:"
|
|
501
|
+
echo '```'
|
|
502
|
+
printf '%s\n' "$HITS"
|
|
503
|
+
echo '```'
|
|
504
|
+
} >> "$GITHUB_STEP_SUMMARY"
|
|
505
|
+
exit 1
|
|
506
|
+
fi
|
|
507
|
+
echo "No .gitea/.github dirs - OK"
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# MCP SDK Version Check
|
|
2
|
+
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
#
|
|
5
|
+
# Weekly check for MCP SDK updates. Creates an issue when a new version
|
|
6
|
+
# of @modelcontextprotocol/sdk is available.
|
|
7
|
+
|
|
8
|
+
name: "MCP: SDK Version Check"
|
|
9
|
+
|
|
10
|
+
on:
|
|
11
|
+
schedule:
|
|
12
|
+
- cron: '0 9 * * 1' # Every Monday at 9am UTC
|
|
13
|
+
workflow_dispatch:
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
permissions:
|
|
17
|
+
contents: read
|
|
18
|
+
|
|
19
|
+
jobs:
|
|
20
|
+
check-sdk:
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
if: ${{ !startsWith(github.event.repository.name, 'Template-') }}
|
|
23
|
+
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@v4
|
|
26
|
+
|
|
27
|
+
- name: Setup Node.js
|
|
28
|
+
uses: actions/setup-node@v4
|
|
29
|
+
with:
|
|
30
|
+
node-version: 20
|
|
31
|
+
|
|
32
|
+
- name: Check for SDK updates
|
|
33
|
+
id: sdk-check
|
|
34
|
+
run: |
|
|
35
|
+
CURRENT=$(node -p "require('./package.json').dependencies['@modelcontextprotocol/sdk']" | sed 's/[\^~]//')
|
|
36
|
+
LATEST=$(npm view @modelcontextprotocol/sdk version 2>/dev/null || echo "unknown")
|
|
37
|
+
|
|
38
|
+
echo "current=${CURRENT}" >> $GITHUB_OUTPUT
|
|
39
|
+
echo "latest=${LATEST}" >> $GITHUB_OUTPUT
|
|
40
|
+
|
|
41
|
+
if [ "${CURRENT}" != "${LATEST}" ] && [ "${LATEST}" != "unknown" ]; then
|
|
42
|
+
echo "update_available=true" >> $GITHUB_OUTPUT
|
|
43
|
+
echo "MCP SDK update available: ${CURRENT} → ${LATEST}"
|
|
44
|
+
else
|
|
45
|
+
echo "update_available=false" >> $GITHUB_OUTPUT
|
|
46
|
+
echo "MCP SDK is up to date: ${CURRENT}"
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
- name: Check for Zod updates
|
|
50
|
+
id: zod-check
|
|
51
|
+
run: |
|
|
52
|
+
CURRENT=$(node -p "require('./package.json').dependencies['zod']" | sed 's/[\^~]//')
|
|
53
|
+
LATEST=$(npm view zod version 2>/dev/null || echo "unknown")
|
|
54
|
+
|
|
55
|
+
echo "current=${CURRENT}" >> $GITHUB_OUTPUT
|
|
56
|
+
echo "latest=${LATEST}" >> $GITHUB_OUTPUT
|
|
57
|
+
|
|
58
|
+
if [ "${CURRENT}" != "${LATEST}" ] && [ "${LATEST}" != "unknown" ]; then
|
|
59
|
+
echo "update_available=true" >> $GITHUB_OUTPUT
|
|
60
|
+
else
|
|
61
|
+
echo "update_available=false" >> $GITHUB_OUTPUT
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
- name: Create update issue
|
|
65
|
+
if: steps.sdk-check.outputs.update_available == 'true'
|
|
66
|
+
uses: actions/github-script@v7
|
|
67
|
+
with:
|
|
68
|
+
script: |
|
|
69
|
+
const title = `chore(deps): update @modelcontextprotocol/sdk ${process.env.CURRENT} → ${process.env.LATEST}`;
|
|
70
|
+
const body = [
|
|
71
|
+
'## MCP SDK Update Available',
|
|
72
|
+
'',
|
|
73
|
+
`| Package | Current | Latest |`,
|
|
74
|
+
`|---------|---------|--------|`,
|
|
75
|
+
`| @modelcontextprotocol/sdk | ${process.env.CURRENT} | ${process.env.LATEST} |`,
|
|
76
|
+
`| zod | ${process.env.ZOD_CURRENT} | ${process.env.ZOD_LATEST} |`,
|
|
77
|
+
'',
|
|
78
|
+
'### Steps',
|
|
79
|
+
'1. Update package.json',
|
|
80
|
+
'2. Run `npm install`',
|
|
81
|
+
'3. Run `npm run build` to verify compilation',
|
|
82
|
+
'4. Test all tools against target API',
|
|
83
|
+
'',
|
|
84
|
+
'### Changelog',
|
|
85
|
+
`https://github.com/modelcontextprotocol/typescript-sdk/releases`,
|
|
86
|
+
].join('\n');
|
|
87
|
+
|
|
88
|
+
// Check for existing open issue
|
|
89
|
+
const existing = await github.rest.issues.listForRepo({
|
|
90
|
+
owner: context.repo.owner,
|
|
91
|
+
repo: context.repo.repo,
|
|
92
|
+
state: 'open',
|
|
93
|
+
labels: 'api-change',
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const alreadyExists = existing.data.some(i => i.title.includes('@modelcontextprotocol/sdk'));
|
|
97
|
+
if (!alreadyExists) {
|
|
98
|
+
await github.rest.issues.create({
|
|
99
|
+
owner: context.repo.owner,
|
|
100
|
+
repo: context.repo.repo,
|
|
101
|
+
title,
|
|
102
|
+
body,
|
|
103
|
+
labels: ['api-change', 'chore'],
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
env:
|
|
107
|
+
CURRENT: ${{ steps.sdk-check.outputs.current }}
|
|
108
|
+
LATEST: ${{ steps.sdk-check.outputs.latest }}
|
|
109
|
+
ZOD_CURRENT: ${{ steps.zod-check.outputs.current }}
|
|
110
|
+
ZOD_LATEST: ${{ steps.zod-check.outputs.latest }}
|