@mokoenterprises/mokoabs-mcp 0.0.0-stage → 4.2.1

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 (53) 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 +141 -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 +1972 -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 +365 -0
  26. package/.mokogit/workflows/npm-build-test.yml +544 -0
  27. package/.mokogit/workflows/npm-sdk-check.yml +110 -0
  28. package/.mokogit/workflows/npm-tool-inventory.yml +82 -0
  29. package/.mokogit/workflows/pr-branch-check.yml +92 -0
  30. package/.mokogit/workflows/pre-release.yml +1192 -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 +159 -0
  43. package/package.json +34 -4
  44. package/sample/config.example.json +15 -0
  45. package/scripts/deploy-vendor-mcps.ps1 +103 -0
  46. package/source/client.ts +162 -0
  47. package/source/config.ts +139 -0
  48. package/source/credentialStore.ts +216 -0
  49. package/source/index.ts +242 -0
  50. package/source/tokenReader.ts +120 -0
  51. package/source/types.ts +63 -0
  52. package/tools/deploy.mjs +27 -0
  53. package/tsconfig.json +19 -0
@@ -0,0 +1,544 @@
1
+ # MCP Server Build & Validation
2
+ # Copyright (C) 2026 Moko Enterprises <hello@mokoenterprises.us>
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.mokoenterprises.us' }}
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: Reject base64-encoded source files
51
+ run: |
52
+ set -uo pipefail
53
+ # A tracked source file that is a single line of pure base64 (no code
54
+ # punctuation) is almost always a double-encoding accident: it breaks the
55
+ # TypeScript/PHP build at EOF and reads as an opaque blob. Fail fast so it
56
+ # is caught at commit time and never hand-decoded later. The strict
57
+ # ^[A-Za-z0-9+/]{200,}={0,2}$ full-line match cannot hit minified JS or
58
+ # JSON (both carry punctuation within the first 200 chars); an embedded
59
+ # base64 literal assigned in code is also safe (it is preceded by
60
+ # `=`/`"`/`export`, so the file does not START with base64).
61
+ bad=0
62
+ while IFS= read -r f; do
63
+ case "$f" in
64
+ *.ts|*.tsx|*.js|*.mjs|*.cjs|*.jsx|*.php) ;;
65
+ *) continue ;;
66
+ esac
67
+ case "$f" in *.min.js|*/dist/*|*/vendor/*|*/node_modules/*) continue ;; esac
68
+ [ -s "$f" ] || continue
69
+ if [ "$(wc -l < "$f")" -le 1 ] \
70
+ && head -c 400 "$f" | LC_ALL=C grep -qE '^[A-Za-z0-9+/]{200,}={0,2}$'; then
71
+ echo "::error file=${f}::Source file looks base64-encoded (single base64-only line) — decode it before committing (e.g. base64 -d)."
72
+ bad=$((bad + 1))
73
+ fi
74
+ done < <(git ls-files)
75
+ if [ "$bad" -gt 0 ]; then
76
+ echo "${bad} base64-encoded source file(s) found — see annotations above." >&2
77
+ exit 1
78
+ fi
79
+ echo "No base64-encoded source files."
80
+
81
+ - name: Resolve source dir
82
+ id: srcdir
83
+ uses: ./.mokogit/actions/resolve-source-dir
84
+ with:
85
+ mokogit_token: ${{ secrets.MOKOGIT_TOKEN }}
86
+ strict: 'false'
87
+
88
+ - name: Setup Node.js ${{ matrix.node-version }}
89
+ uses: actions/setup-node@v4
90
+ with:
91
+ node-version: ${{ matrix.node-version }}
92
+
93
+ - name: Install npm dependencies
94
+ run: |
95
+ if [ -f package-lock.json ]; then npm ci; else npm install --no-audit --no-fund; fi
96
+
97
+ - name: Setup MokoCLI build agent (moko on PATH)
98
+ env:
99
+ MOKO_CLONE_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
100
+ MOKO_CLONE_HOST: git.mokoenterprises.us/MokoEnterprises
101
+ run: |
102
+ # Reach mokocli deterministically and expose `moko` on PATH (mokocli's cli/ dir)
103
+ # so `moko build|test|lint` resolve. Prefer a pre-installed /opt/mokocli ONLY
104
+ # when it is a live git checkout we can fast-forward (i.e. genuinely current).
105
+ # A `cp -a` snapshot (host mokocli-sync) or a checkout that will not ff is NOT
106
+ # trusted — we fall through to the CURRENT public Composer registry instead.
107
+ # This removes the dependency on the host mokocli-sync service being healthy:
108
+ # a stale /opt snapshot previously ran frozen mokocli (observed v04.00.15)
109
+ # because the git-pull refresh is a silent no-op against a non-git snapshot,
110
+ # which reds every npm repo (the local-bin lint skip / repo lint-script path).
111
+ # (MokoCLI #650 rotting-/opt failsafe; #1028/#1029 the lint fix that exposed
112
+ # the staleness; non-executable-/opt exit-126 heal retained — Template-NPM
113
+ # #142 / #138.)
114
+
115
+ # Install the CURRENT published mokocli from the tokenless public Composer
116
+ # registry (metadata + dist need no auth); authenticated git clone of stable
117
+ # is the last-resort fallback. Exposes `moko` on PATH via /tmp/mokocli/cli.
118
+ bootstrap_registry() {
119
+ if ! command -v composer > /dev/null 2>&1; then
120
+ 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
121
+ fi
122
+ rm -rf /tmp/mokocli
123
+ REG='{"type":"composer","url":"https://git.mokoenterprises.us/api/packages/MokoEnterprises/composer"}'
124
+ if composer create-project --no-dev --no-interaction --no-progress --quiet --repository="${REG}" mokoenterprises/mokocli /tmp/mokocli >/dev/null 2>&1 && { [ -x /tmp/mokocli/cli/moko ] || [ -f /tmp/mokocli/cli/moko ]; }; then
125
+ echo "Bootstrapped mokocli from the Composer registry (tokenless)."
126
+ else
127
+ echo "Registry bootstrap unavailable; falling back to authenticated git clone."
128
+ MOKO_CLONE_TOKEN="$(printf '%s' "${MOKO_CLONE_TOKEN}" | tr -d '[:space:]')"
129
+ echo "::add-mask::${MOKO_CLONE_TOKEN}"
130
+ if [ -z "${MOKO_CLONE_TOKEN}" ]; then
131
+ echo "::error::mokocli registry bootstrap failed and MOKOGIT_TOKEN is empty — cannot clone fallback." >&2
132
+ return 1
133
+ fi
134
+ rm -rf /tmp/mokocli
135
+ git clone --depth 1 --branch stable --quiet "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git" /tmp/mokocli
136
+ ( cd /tmp/mokocli && composer install --no-dev --no-interaction --no-progress --optimize-autoloader --quiet )
137
+ fi
138
+ # Ensure the freshly-bootstrapped binary is executable (registry dist / git
139
+ # clone can land it without +x on some runners).
140
+ chmod +x /tmp/mokocli/*/moko 2>/dev/null || true
141
+ echo "/tmp/mokocli/cli" >> "$GITHUB_PATH"
142
+ }
143
+
144
+ # Heal a non-executable pre-installed binary before testing it (exit-126 class).
145
+ sudo chmod +x /opt/mokocli/*/moko 2>/dev/null || true
146
+
147
+ OPT_OK=0
148
+ if [ -x /opt/mokocli/cli/moko ] && git -C /opt/mokocli rev-parse --git-dir > /dev/null 2>&1; then
149
+ # Live git checkout: trust it ONLY if it fast-forwards cleanly to current.
150
+ if git -C /opt/mokocli pull --ff-only --quiet 2>/dev/null; then
151
+ echo "Using pre-installed /opt/mokocli (git-refreshed to current)."
152
+ echo "/opt/mokocli/cli" >> "$GITHUB_PATH"
153
+ OPT_OK=1
154
+ else
155
+ echo "/opt/mokocli could not fast-forward — using the current registry instead."
156
+ fi
157
+ elif [ -x /opt/mokocli/cli/moko ]; then
158
+ echo "/opt/mokocli is a non-git snapshot (freshness unverifiable) — using the current registry instead."
159
+ fi
160
+
161
+ if [ "$OPT_OK" != "1" ]; then
162
+ bootstrap_registry || exit 1
163
+ fi
164
+
165
+ - name: Lint
166
+ run: moko lint
167
+
168
+ - name: Build
169
+ run: moko build
170
+
171
+ - name: Test
172
+ run: |
173
+ # Skip cleanly when the package defines no "test" script, so a repo
174
+ # with genuinely no tests does not red-fail the build. moko test runs
175
+ # the script when it exists.
176
+ if node -e "process.exit(((require('./package.json').scripts)||{}).test?0:1)" 2>/dev/null; then
177
+ moko test
178
+ else
179
+ echo "::notice::no \"test\" script in package.json -- skipping test step"
180
+ fi
181
+
182
+ - name: Verify dist output exists
183
+ run: |
184
+ # dist/index.js is the bin/main entry for every MCP server (shared
185
+ # package.json convention). Assert that + that the build emitted
186
+ # compiled output. Do NOT hard-code other filenames: source layouts
187
+ # differ per server (many have no client.ts/types.ts/root config.ts),
188
+ # which false-failed this check on every build fleet-wide.
189
+ test -f dist/index.js || (echo "ERROR: dist/index.js not found" && exit 1)
190
+ JS_COUNT=$(find dist -name '*.js' | wc -l)
191
+ [ "${JS_COUNT}" -gt 0 ] || (echo "ERROR: no compiled .js files in dist/" && exit 1)
192
+ echo "✓ dist/index.js present; ${JS_COUNT} compiled .js file(s) in dist/"
193
+
194
+ - name: Verify shebang in index.js
195
+ run: |
196
+ head -1 dist/index.js | grep -q "#!/usr/bin/env node" || echo "WARNING: Missing shebang in dist/index.js"
197
+
198
+ - name: Count registered tools
199
+ run: |
200
+ # Tools may be registered in the entry file OR in modules it imports
201
+ # (modular servers call server.tool( / .registerTool( inside
202
+ # source/tools/*.ts). Count registrations across the whole source
203
+ # tree, not just index.ts, else modular servers false-report zero.
204
+ SRC_DIR="${{ steps.srcdir.outputs.source_dir }}"
205
+ [ -n "${SRC_DIR}" ] && [ -d "${SRC_DIR}" ] || SRC_DIR="source"
206
+ [ -d "${SRC_DIR}" ] || SRC_DIR="src"
207
+ # `grep` exits 1 when it finds no matches; under `set -e -o pipefail`
208
+ # that aborts the step before the MCP-vs-non-MCP check below can run.
209
+ # Swallow the no-match exit so the count is a real 0, not a hard fail.
210
+ TOOL_COUNT=$( { grep -rE "server\.tool\(|\.registerTool\(" "${SRC_DIR}" 2>/dev/null || true; } | wc -l | tr -d ' ')
211
+ # Only MCP *servers* must register tools. Detect that from package.json
212
+ # (any @modelcontextprotocol/* dep) so non-MCP npm packages — client
213
+ # CLIs, libraries — aren't force-failed by an inapplicable check. When
214
+ # this npm CI template lands on a non-MCP repo the tool count is 0 by
215
+ # design; treat that as advisory, not a hard error.
216
+ 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');}")
217
+ echo "Registered tool calls under ${SRC_DIR}: ${TOOL_COUNT} (mcp-server=${IS_MCP})"
218
+ if [ "${TOOL_COUNT}" -eq 0 ]; then
219
+ if [ "${IS_MCP}" = "1" ]; then
220
+ echo "ERROR: MCP server declares @modelcontextprotocol/* but registers no tools under ${SRC_DIR}"
221
+ exit 1
222
+ fi
223
+ echo "::warning::No MCP tool registrations found and no @modelcontextprotocol/* dependency — treating as a non-MCP npm package (advisory, not failing)."
224
+ fi
225
+
226
+ # NOTE: a standalone `ci-summary` job (needs: [build], if: always()) was removed.
227
+ # This Gitea act_runner never schedules a `needs:`+`if: always()` job — it parks in
228
+ # "waiting" (runner_id 0) forever and never dispatches. Under the org-governance
229
+ # `main` rule (status_check_contexts: ["*"], every check must be green) that stranded
230
+ # job was a permanent, silent block on every PR into main. The per-job status is
231
+ # already visible on the Actions run page and `wiki-reminder` posts a sticky PR
232
+ # comment on main PRs, so a separate summary job earned its keep only when it worked —
233
+ # which, on this runner, was never. If a build-result summary comment is wanted back,
234
+ # add it as a final STEP with `if: ${{ always() }}` inside the `build` job (step-level
235
+ # always() IS supported), not as a dependent job. (Matches ci-go.yml.)
236
+
237
+ # ══════════════════════════════════════════════════════════════════════════
238
+ # Universal PR-check jobs — folded in from "Universal: PR Check" so the NPM
239
+ # platform runs one CI workflow per PR (mirrors Template-Joomla #162).
240
+ # Branch-gated: Branch Policy on all PRs; docs gate + wiki reminder on main.
241
+ # ══════════════════════════════════════════════════════════════════════════
242
+
243
+ branch-policy:
244
+ name: Branch Policy
245
+ runs-on: ubuntu-latest
246
+ steps:
247
+ - name: Check branch merge target
248
+ env:
249
+ # Compare against the repo's real production branch, not a hardcoded
250
+ # 'main'. MokoGIT renamed the production channel main -> stable
251
+ # fleet-wide (ladder: dev -> rc -> stable); hardcoding 'main'
252
+ # red-failed every legitimate dev->stable / rc->stable promotion.
253
+ DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
254
+ run: |
255
+ HEAD="${{ github.head_ref }}"
256
+ BASE="${{ github.base_ref }}"
257
+ PROD="${DEFAULT_BRANCH:-main}"
258
+
259
+ echo "PR: ${HEAD} → ${BASE} (production=${PROD})"
260
+
261
+ ALLOWED=true
262
+ REASON=""
263
+
264
+ case "$HEAD" in
265
+ feature/*|feat/*)
266
+ if [ "$BASE" != "dev" ]; then
267
+ ALLOWED=false
268
+ REASON="Feature branches must target 'dev', not '${BASE}'"
269
+ fi
270
+ ;;
271
+ fix/*|bugfix/*)
272
+ if [ "$BASE" != "dev" ] && [ "$BASE" != "$PROD" ]; then
273
+ ALLOWED=false
274
+ REASON="Fix branches must target 'dev' or '${PROD}', not '${BASE}'"
275
+ fi
276
+ ;;
277
+ patch/*)
278
+ if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ] && [ "$BASE" != "$PROD" ]; then
279
+ ALLOWED=false
280
+ REASON="Patch branches must target 'dev', 'rc', or '${PROD}', not '${BASE}'"
281
+ fi
282
+ ;;
283
+ hotfix/*)
284
+ if [ "$BASE" != "dev" ] && [ "$BASE" != "$PROD" ]; then
285
+ ALLOWED=false
286
+ REASON="Hotfix branches can only target 'dev' or '${PROD}', not '${BASE}'"
287
+ fi
288
+ ;;
289
+ rc|rc/*)
290
+ if [ "$BASE" != "$PROD" ]; then
291
+ ALLOWED=false
292
+ REASON="RC branch can only merge into '${PROD}', not '${BASE}'"
293
+ fi
294
+ ;;
295
+ dev)
296
+ if [ "$BASE" != "rc" ] && [ "$BASE" != "$PROD" ]; then
297
+ ALLOWED=false
298
+ REASON="Dev branch can only merge into 'rc' or '${PROD}', not '${BASE}'"
299
+ fi
300
+ ;;
301
+ esac
302
+
303
+ if [ "$ALLOWED" = false ]; then
304
+ echo "::error::${REASON}"
305
+ echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY
306
+ echo "" >> $GITHUB_STEP_SUMMARY
307
+ echo "${REASON}" >> $GITHUB_STEP_SUMMARY
308
+ echo "" >> $GITHUB_STEP_SUMMARY
309
+ echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
310
+ echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
311
+ echo "- \`fix/*\` → \`dev\` or \`${PROD}\`" >> $GITHUB_STEP_SUMMARY
312
+ echo "- \`patch/*\` → \`dev\`, \`rc\`, or \`${PROD}\`" >> $GITHUB_STEP_SUMMARY
313
+ echo "- \`hotfix/*\` → \`dev\` or \`${PROD}\`" >> $GITHUB_STEP_SUMMARY
314
+ echo "- \`dev\` → \`rc\` or \`${PROD}\`" >> $GITHUB_STEP_SUMMARY
315
+ echo "- \`rc/*\` → \`${PROD}\`" >> $GITHUB_STEP_SUMMARY
316
+ exit 1
317
+ fi
318
+
319
+ echo "Branch policy: OK (${HEAD} → ${BASE})"
320
+ echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
321
+
322
+ # ── Docs Update Gate (wiki-aware) ───────────────────────────────────────
323
+ #
324
+ # RULE (in order):
325
+ # 1. Short-circuit: if the PR diff touches CHANGELOG.md OR README.md, PASS
326
+ # immediately (deterministic; no wiki lookup).
327
+ # 2. Else consult the repo WIKI (<owner>/<repo>.wiki.git) HEAD commit date.
328
+ # Mode is chosen by the org/repo variable DOCS_WIKI_WINDOW (default 'pr'):
329
+ # - 'pr' → satisfied if wiki HEAD commit date >= this PR's
330
+ # created_at (docs touched since the PR opened).
331
+ # - '<N>h'/'<N>d' → satisfied if wiki HEAD commit is within that rolling
332
+ # window (now - duration). e.g. 24h, 72h, 7d.
333
+ # Fail-open: if the wiki is absent/unreachable or the lookup errors, do
334
+ # NOT hard-fail — treat as "unsatisfied → warn" per step 3.
335
+ # 3. Verdict: if neither is satisfied → emit ::warning:: (advisory) and PASS
336
+ # on non-main branches; hard-fail (exit 1) ONLY when the PR base is 'main'
337
+ # (release hygiene on the stable line).
338
+ #
339
+ # The job runs on EVERY PR so the required status context "Require Docs Update"
340
+ # always reports; the base-branch gate is enforced inside the step, not via a
341
+ # job-level `if:` that would leave the context missing on non-main PRs.
342
+ require-docs:
343
+ name: Require Docs Update
344
+ runs-on: ubuntu-latest
345
+ steps:
346
+ - name: Checkout
347
+ uses: actions/checkout@v4
348
+ with:
349
+ fetch-depth: 0
350
+
351
+ - name: Docs gate (docs diff, else wiki freshness)
352
+ env:
353
+ BASE_REF: ${{ github.base_ref }}
354
+ HEAD_REF: ${{ github.head_ref }}
355
+ BASE_SHA: ${{ github.event.pull_request.base.sha }}
356
+ HEAD_SHA: ${{ github.event.pull_request.head.sha }}
357
+ PR_CREATED_AT: ${{ github.event.pull_request.created_at }}
358
+ SERVER: ${{ vars.MOKOGIT_URL || 'https://git.mokoenterprises.us' }}
359
+ REPO: ${{ github.repository }}
360
+ TOKEN: ${{ github.token }}
361
+ DOCS_WIKI_WINDOW: ${{ vars.DOCS_WIKI_WINDOW || 'pr' }}
362
+ run: |
363
+ set -uo pipefail
364
+
365
+ # ── Step 1: docs diff short-circuit ─────────────────────────────
366
+ CHANGED="$(git diff --name-only "$BASE_SHA" "$HEAD_SHA" 2>/dev/null || true)"
367
+ if [ -z "$CHANGED" ]; then
368
+ git fetch -q origin "$BASE_REF" 2>/dev/null || true
369
+ CHANGED="$(git diff --name-only "origin/${BASE_REF}...HEAD" 2>/dev/null || true)"
370
+ fi
371
+ echo "Changed files in PR:"
372
+ echo "$CHANGED"
373
+ if echo "$CHANGED" | grep -qxE 'CHANGELOG\.md|README\.md'; then
374
+ echo "Docs diff present (CHANGELOG.md or README.md) — gate satisfied."
375
+ echo "## Docs Update: Passed (docs diff)" >> "$GITHUB_STEP_SUMMARY"
376
+ exit 0
377
+ fi
378
+
379
+ # ── Step 2: wiki freshness (fail-open) ──────────────────────────
380
+ # Resolve wiki HEAD commit epoch via a shallow clone of the wiki repo.
381
+ # Any failure here is non-fatal: SATISFIED stays 0 → handled by step 3.
382
+ SATISFIED=0
383
+ WIKI_EPOCH=""
384
+ TOK="$(printf '%s' "${TOKEN:-}" | tr -d '[:space:]')"
385
+ HOSTPATH="${SERVER#https://}"; HOSTPATH="${HOSTPATH#http://}"
386
+ WIKI_URL="https://x-access-token:${TOK}@${HOSTPATH}/${REPO}.wiki.git"
387
+ echo "::add-mask::${TOK}"
388
+ rm -rf /tmp/docs-wiki
389
+ if git clone --depth 1 --quiet "${WIKI_URL}" /tmp/docs-wiki 2>/dev/null; then
390
+ WIKI_EPOCH="$(git -C /tmp/docs-wiki log -1 --format=%ct 2>/dev/null || true)"
391
+ fi
392
+
393
+ if [ -z "$WIKI_EPOCH" ]; then
394
+ echo "::warning::wiki absent/unreachable — treating docs gate as unsatisfied (fail-open)"
395
+ else
396
+ MODE="$(printf '%s' "${DOCS_WIKI_WINDOW}" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')"
397
+ [ -n "$MODE" ] || MODE="pr"
398
+ if [ "$MODE" = "pr" ]; then
399
+ # PASS if wiki HEAD commit date >= PR created_at.
400
+ PR_EPOCH="$(date -u -d "${PR_CREATED_AT}" +%s 2>/dev/null || true)"
401
+ if [ -z "$PR_EPOCH" ]; then
402
+ echo "::warning::could not parse PR created_at ('${PR_CREATED_AT}') — docs gate unsatisfied (fail-open)"
403
+ elif [ "$WIKI_EPOCH" -ge "$PR_EPOCH" ]; then
404
+ SATISFIED=1
405
+ echo "Wiki updated since PR opened (wiki=${WIKI_EPOCH} >= pr=${PR_EPOCH})."
406
+ else
407
+ echo "Wiki not updated since PR opened (wiki=${WIKI_EPOCH} < pr=${PR_EPOCH})."
408
+ fi
409
+ else
410
+ # Duration window: <N>h / <N>d (also bare <N> = seconds).
411
+ NUM="$(printf '%s' "$MODE" | sed -E 's/[^0-9].*$//')"
412
+ UNIT="$(printf '%s' "$MODE" | sed -E 's/^[0-9]+//')"
413
+ SECS=0
414
+ case "$UNIT" in
415
+ h) SECS=$(( NUM * 3600 )) ;;
416
+ d) SECS=$(( NUM * 86400 )) ;;
417
+ m) SECS=$(( NUM * 60 )) ;;
418
+ s|"") SECS=$(( NUM )) ;;
419
+ *) echo "::warning::unrecognized DOCS_WIKI_WINDOW unit '${UNIT}'; falling back to 'pr'-style (>= PR created_at)"
420
+ PR_EPOCH="$(date -u -d "${PR_CREATED_AT}" +%s 2>/dev/null || echo 0)"
421
+ SECS=0 ;;
422
+ esac
423
+ if [ "$SECS" -gt 0 ]; then
424
+ NOW="$(date -u +%s)"
425
+ CUTOFF=$(( NOW - SECS ))
426
+ if [ "$WIKI_EPOCH" -ge "$CUTOFF" ]; then
427
+ SATISFIED=1
428
+ echo "Wiki updated within window ${MODE} (wiki=${WIKI_EPOCH} >= cutoff=${CUTOFF})."
429
+ else
430
+ echo "Wiki stale beyond window ${MODE} (wiki=${WIKI_EPOCH} < cutoff=${CUTOFF})."
431
+ fi
432
+ elif [ "${PR_EPOCH:-0}" -gt 0 ] && [ "$WIKI_EPOCH" -ge "${PR_EPOCH}" ]; then
433
+ SATISFIED=1
434
+ fi
435
+ fi
436
+ fi
437
+
438
+ # ── Step 3: verdict ─────────────────────────────────────────────
439
+ if [ "$SATISFIED" = "1" ]; then
440
+ echo "## Docs Update: Passed (wiki fresh)" >> "$GITHUB_STEP_SUMMARY"
441
+ exit 0
442
+ fi
443
+
444
+ if [ "$BASE_REF" = "main" ]; then
445
+ 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."
446
+ {
447
+ echo "## Docs Update Required"
448
+ echo ""
449
+ echo "PRs merging into \`main\` must either update **CHANGELOG.md** or **README.md**, or the repo **wiki** must be fresh (mode: \`${DOCS_WIKI_WINDOW}\`)."
450
+ } >> "$GITHUB_STEP_SUMMARY"
451
+ exit 1
452
+ fi
453
+
454
+ echo "::warning::Docs gate unsatisfied (advisory on non-main branch '${BASE_REF}'): update CHANGELOG.md/README.md or the wiki."
455
+ {
456
+ echo "## Docs Update: Advisory"
457
+ echo ""
458
+ echo "No docs diff and wiki not fresh (mode: \`${DOCS_WIKI_WINDOW}\`). Non-blocking on \`${BASE_REF}\`; will hard-fail on \`main\`."
459
+ } >> "$GITHUB_STEP_SUMMARY"
460
+ exit 0
461
+
462
+ wiki-reminder:
463
+ name: Wiki Update Reminder
464
+ runs-on: ubuntu-latest
465
+ if: ${{ github.base_ref == github.event.repository.default_branch }}
466
+ steps:
467
+ - name: Remind to update the wiki
468
+ env:
469
+ TOKEN: ${{ github.token }}
470
+ SERVER: ${{ vars.MOKOGIT_URL || 'https://git.mokoenterprises.us' }}
471
+ REPO: ${{ github.repository }}
472
+ PR: ${{ github.event.pull_request.number }}
473
+ run: |
474
+ set -uo pipefail
475
+ {
476
+ echo "## Wiki Update Reminder"
477
+ echo ""
478
+ echo "Docs are **wiki-first** at MokoEnterprises. If this change affects behavior, usage, configuration, or standards, update the repo wiki:"
479
+ echo ""
480
+ echo "- ${SERVER}/${REPO}/wiki"
481
+ echo ""
482
+ echo "_Non-blocking reminder._"
483
+ } >> "$GITHUB_STEP_SUMMARY"
484
+ # Sticky PR comment: create-or-UPDATE a single comment keyed by a hidden
485
+ # marker (no duplicate reminders across pushes). Best-effort, never fails.
486
+ LIST="${SERVER}/api/v1/repos/${REPO}/issues/${PR}/comments"
487
+ EDIT="${SERVER}/api/v1/repos/${REPO}/issues/comments"
488
+ MARKER="<!-- wiki-reminder -->"
489
+ 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)_")"
490
+ if [ -n "${TOKEN:-}" ] && [ -n "${PR:-}" ]; then
491
+ CID="$(curl -sf -H "Authorization: token ${TOKEN}" "${LIST}?limit=100" 2>/dev/null \
492
+ | 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)"
493
+ PAYLOAD="$(python3 -c "import json,sys; print(json.dumps({'body': sys.stdin.read()}))" <<<"${BODY}")"
494
+ if [ -n "${CID}" ]; then
495
+ curl -sf -X PATCH -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" -d "${PAYLOAD}" "${EDIT}/${CID}" >/dev/null 2>&1 || true
496
+ else
497
+ curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" -d "${PAYLOAD}" "${LIST}" >/dev/null 2>&1 || true
498
+ fi
499
+ fi
500
+ echo "Wiki reminder emitted (non-blocking, sticky)."
501
+
502
+ # ── Universal PR gate checks (folded from pr-check's Validate PR) ─────────
503
+ pr-gate-checks:
504
+ name: PR Gate Checks
505
+ runs-on: ubuntu-latest
506
+ steps:
507
+ - name: Checkout
508
+ uses: actions/checkout@v4
509
+
510
+ - name: Check for merge conflict markers
511
+ run: |
512
+ 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)
513
+ if [ -n "$CONFLICTS" ]; then
514
+ echo "::error::Merge conflict markers found in source files"
515
+ echo "## Conflict Markers Found" >> $GITHUB_STEP_SUMMARY
516
+ echo '```' >> $GITHUB_STEP_SUMMARY
517
+ echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
518
+ echo '```' >> $GITHUB_STEP_SUMMARY
519
+ exit 1
520
+ fi
521
+ echo "No conflict markers found"
522
+
523
+ - name: Reject .gitea and .github workflow dirs
524
+ run: |
525
+ # .gitea/ and .github/ carry their own workflows that CONFLICT with the
526
+ # MokoGIT .mokogit/workflows/ system (duplicate / competing runs). They
527
+ # must never exist in-repo. Fail the PR if either directory is tracked so
528
+ # it gets deleted.
529
+ HITS=$(git ls-files | grep -E '^(\.gitea|\.github)/workflows/' || true)
530
+ if [ -n "$HITS" ]; then
531
+ echo "::error::.gitea/ and .github/ are not allowed - their workflows conflict with .mokogit/workflows/. Delete these directories."
532
+ printf '%s\n' "$HITS" | while IFS= read -r f; do
533
+ [ -n "$f" ] && echo "::error file=${f}::Remove ${f} (.gitea/.github workflows conflict with .mokogit/)"
534
+ done
535
+ {
536
+ echo "## Blocked: .gitea/.github present"
537
+ echo "These directories conflict with the MokoGIT \`.mokogit/workflows/\` system and must be deleted:"
538
+ echo '```'
539
+ printf '%s\n' "$HITS"
540
+ echo '```'
541
+ } >> "$GITHUB_STEP_SUMMARY"
542
+ exit 1
543
+ fi
544
+ echo "No .gitea/.github dirs - OK"
@@ -0,0 +1,110 @@
1
+ # MCP SDK Version Check
2
+ # Copyright (C) 2026 Moko Enterprises <hello@mokoenterprises.us>
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 }}