@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.
Files changed (51) hide show
  1. package/.editorconfig +41 -0
  2. package/.gitattributes +94 -0
  3. package/.gitmessage +9 -0
  4. package/.mokogit/ISSUE_TEMPLATE/adr.md +110 -0
  5. package/.mokogit/ISSUE_TEMPLATE/bug_report.md +48 -0
  6. package/.mokogit/ISSUE_TEMPLATE/config.yml +18 -0
  7. package/.mokogit/ISSUE_TEMPLATE/documentation.md +52 -0
  8. package/.mokogit/ISSUE_TEMPLATE/feature_request.md +51 -0
  9. package/.mokogit/ISSUE_TEMPLATE/mcp_api_integration.md +48 -0
  10. package/.mokogit/ISSUE_TEMPLATE/mcp_connection_issue.md +69 -0
  11. package/.mokogit/ISSUE_TEMPLATE/mcp_tool_request.md +50 -0
  12. package/.mokogit/ISSUE_TEMPLATE/question.md +82 -0
  13. package/.mokogit/ISSUE_TEMPLATE/rfc.md +126 -0
  14. package/.mokogit/ISSUE_TEMPLATE/security.md +51 -0
  15. package/.mokogit/ISSUE_TEMPLATE/version.md +24 -0
  16. package/.mokogit/actions/resolve-source-dir/action.yml +72 -0
  17. package/.mokogit/workflows/apidocs.yml +132 -0
  18. package/.mokogit/workflows/auto-assign.yml +76 -0
  19. package/.mokogit/workflows/auto-dev-issue.yml +207 -0
  20. package/.mokogit/workflows/auto-release.yml +1787 -0
  21. package/.mokogit/workflows/branch-cleanup.yml +63 -0
  22. package/.mokogit/workflows/cascade.yml +645 -0
  23. package/.mokogit/workflows/ci-issue-reporter.yml +100 -0
  24. package/.mokogit/workflows/cleanup.yml +112 -0
  25. package/.mokogit/workflows/makefile-lint.yml +240 -0
  26. package/.mokogit/workflows/npm-build-test.yml +507 -0
  27. package/.mokogit/workflows/npm-sdk-check.yml +110 -0
  28. package/.mokogit/workflows/npm-tool-inventory.yml +81 -0
  29. package/.mokogit/workflows/pr-branch-check.yml +92 -0
  30. package/.mokogit/workflows/pre-release.yml +1134 -0
  31. package/.mokogit/workflows/rc-revert.yml +75 -0
  32. package/AGENTS.md +48 -0
  33. package/CHANGELOG.md +37 -0
  34. package/CODE_OF_CONDUCT.md +70 -0
  35. package/CONTRIBUTING.md +164 -0
  36. package/LICENSE +696 -0
  37. package/Makefile +94 -0
  38. package/README.md +109 -2
  39. package/SECURITY.md +245 -0
  40. package/build/README.md +7 -0
  41. package/dist/README.md +7 -0
  42. package/dist/index.js +153 -0
  43. package/package.json +34 -5
  44. package/sample/config.example.json +15 -0
  45. package/source/client.ts +132 -0
  46. package/source/config.ts +139 -0
  47. package/source/credentialStore.ts +216 -0
  48. package/source/index.ts +235 -0
  49. package/source/tokenReader.ts +120 -0
  50. package/source/types.ts +63 -0
  51. 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 }}