From f8be399e9e80105d45a06f325230f17df09c72df Mon Sep 17 00:00:00 2001 From: fOuttaMyPaint Date: Sun, 14 Jun 2026 19:13:37 -0400 Subject: [PATCH] feat(scaffold): correct mcp-server template set and release/publish chain [skip version] mcp-server borns were inheriting cursor-plugin doc/meta boilerplate and a broken release/publish setup. Fix at the generator source: - Type-aware doc templates (CLAUDE, AGENTS, CONTRIBUTING, ROADMAP, site.json, CHANGELOG, .cursorrules, .gitignore): mcp-server output now describes a TypeScript MCP server (src/ layout, provider adapters, tool registration, mcp-tools.json sync, stdio, npx install) with no skills/rules/.mdc/ .cursor-plugin/plugin.json/validate.yml/marketplace language. - Emit ci.yml (build+test on Node 20/22) and a corrected release.yml for mcp-server; the new release.mcp.yml.j2 auto-bumps from conventional commits (npm version keeps package.json + lockfile in sync), updates the README badge, commits with [skip version], and creates the release with a PAT-or-fallback token so publish.yml's release event actually fires. - publish.yml.j2: add workflow_dispatch and an already-published guard so a re-dispatch is idempotent. - build_registry_entry language is now type-aware (TypeScript for mcp-server). - Born-green test asserts the new mcp-server emit set (ci.yml, release.yml) as emitted-but-not-drift-required, so siblings without them do not go red. No STANDARDS_VERSION or VERSION change (scaffolding is decoupled by design). Signed-off-by: fOuttaMyPaint --- scaffold/generator.py | 4 +- scaffold/templates/AGENTS.md.j2 | 66 +++++++++++++--- scaffold/templates/CHANGELOG.md.j2 | 4 + scaffold/templates/CLAUDE.md.j2 | 24 +++++- scaffold/templates/CONTRIBUTING.md.j2 | 29 ++++++- scaffold/templates/ROADMAP.md.j2 | 22 ++++++ scaffold/templates/ci.yml.j2 | 28 +++++++ scaffold/templates/cursorrules.j2 | 7 ++ scaffold/templates/gitignore.j2 | 10 +++ scaffold/templates/publish.yml.j2 | 24 +++++- scaffold/templates/release.mcp.yml.j2 | 105 ++++++++++++++++++++++++++ scaffold/templates/site.json.j2 | 6 ++ tests/test_scaffold_born_green.py | 15 +++- 13 files changed, 324 insertions(+), 20 deletions(-) create mode 100644 scaffold/templates/ci.yml.j2 create mode 100644 scaffold/templates/release.mcp.yml.j2 diff --git a/scaffold/generator.py b/scaffold/generator.py index c2434e1..8940a4a 100644 --- a/scaffold/generator.py +++ b/scaffold/generator.py @@ -192,6 +192,8 @@ def _render_repo(output_dir: Path, ctx: dict, *, verbose: bool) -> None: # repos do NOT get cursor-plugin-specific validate.yml / release.yml. if repo_type == "mcp-server": write_file(output_dir, ".github/workflows/publish.yml", render_template(env, "publish.yml.j2", ctx), verbose=verbose) + write_file(output_dir, ".github/workflows/ci.yml", render_template(env, "ci.yml.j2", ctx), verbose=verbose) + write_file(output_dir, ".github/workflows/release.yml", render_template(env, "release.mcp.yml.j2", ctx), verbose=verbose) write_file(output_dir, ".github/workflows/pages.yml", render_template(env, "pages.mcp.yml.j2", ctx), verbose=verbose) else: write_file(output_dir, ".github/workflows/validate.yml", render_template(env, "validate.yml.j2", ctx), verbose=verbose) @@ -297,7 +299,7 @@ def build_registry_entry( "extras": {}, "topics": [repo_type, "developer-tools"], "status": "active", - "language": "Python", + "language": "TypeScript" if repo_type == "mcp-server" else "Python", "license": SPDX[license_key], "pagesType": "static", "hasCI": True, diff --git a/scaffold/templates/AGENTS.md.j2 b/scaffold/templates/AGENTS.md.j2 index 2f27772..f1a164b 100644 --- a/scaffold/templates/AGENTS.md.j2 +++ b/scaffold/templates/AGENTS.md.j2 @@ -21,7 +21,12 @@ This is a Cursor IDE plugin for {{ name | replace(' Developer Tools', '') | repl This is an MCP server. It contains: - **`src/`** -- TypeScript source code +- **`src/providers/`** -- provider adapters implementing the `Provider` interface, wired into `ProviderManager` +- **`src/tools/`** -- the registered MCP tools (stdio transport only) - **`package.json`** -- npm package manifest (version source of truth) +- **`mcp-tools.json`** -- enumerates the MCP tools this server exposes +- **`docs/`** -- documentation and GitHub Pages site +- **`CHANGELOG.md`** -- release history {% endif %} ## Branching and commit model @@ -35,10 +40,10 @@ This is an MCP server. It contains: ## CI/CD workflows +{% if type == 'cursor-plugin' %} ### `validate.yml` (runs on PR and push to main) Checks: -{% if type == 'cursor-plugin' %} - JSON validity for plugin.json{% if has_mcp %}, mcp.json{% endif %} - Plugin manifest required fields, kebab-case name, skill/rule file existence @@ -47,20 +52,11 @@ Checks: {% if has_mcp %} - Python syntax for MCP server modules {% endif %} -{% else %} -- TypeScript compilation -- Lint checks -- Test suite -{% endif %} ### `release.yml` (runs on push to main, ignores docs/md changes) Automatic flow: -{% if type == 'cursor-plugin' %} 1. Reads current version from `plugin.json` -{% else %} -1. Reads current version from `package.json` -{% endif %} 2. Determines bump type from conventional commit messages since last tag 3. Computes new semver version 4. Updates version files and README badge @@ -73,6 +69,37 @@ Builds and deploys the documentation site on push to main. ### `stale.yml` Marks issues/PRs as stale after 30 days of inactivity. +{% else %} +### `ci.yml` (runs on PR and push to main) + +Builds and runs the test suite on Node 20 and 22: +- TypeScript build (`npm run build`) +- Test suite (`npm test`, vitest, offline) + +### `release.yml` (runs on push to main) + +Conventional-commit auto-bump: determines the bump type from commit messages since the last tag, updates `package.json`, creates a git tag and GitHub Release. + +### `publish.yml` (runs on release published or workflow_dispatch) + +Publishes the package to npm. + +### `drift-check.yml` + +Checks this repo against the ecosystem standards for drift. + +### `pages.yml` (deploys docs/ to GitHub Pages) + +Builds and deploys the documentation site on push to main. + +### `stale.yml` + +Marks issues/PRs as stale after 30 days of inactivity. + +### `label-sync.yml` + +Keeps repository labels in sync. +{% endif %} ## Version management @@ -86,9 +113,15 @@ Marks issues/PRs as stale after 30 days of inactivity. ## Code conventions +{% if type == 'cursor-plugin' %} - No hardcoded credentials -- CI scans for password/token/api_key patterns. - Skills must have YAML frontmatter starting with `---`. - Rules use `.mdc` extension with frontmatter. +{% else %} +- No hardcoded credentials -- CI scans for password/token/api_key patterns. +- Conventional commits; never hand-edit the version. +- Keep `mcp-tools.json` in sync with the tools registered in `src/tools/`. +{% endif %} ## Adding content @@ -105,6 +138,19 @@ Marks issues/PRs as stale after 30 days of inactivity. 1. Create `rules/.mdc` with frontmatter (`description`, `globs`, `alwaysApply`) 2. Add the path to `plugin.json` under `"rules"` 3. Use `feat:` commit prefix +{% else %} +### New provider adapter + +1. Implement the `Provider` interface in `src/providers/` +2. Register the adapter in `ProviderManager` +3. Use `feat:` commit prefix + +### New tool + +1. Register the tool in `src/tools/` +2. Add it to `mcp-tools.json` +3. Add vitest tests +4. Use `feat:` commit prefix {% endif %} ## License diff --git a/scaffold/templates/CHANGELOG.md.j2 b/scaffold/templates/CHANGELOG.md.j2 index 18f438d..9a6c5a1 100644 --- a/scaffold/templates/CHANGELOG.md.j2 +++ b/scaffold/templates/CHANGELOG.md.j2 @@ -20,5 +20,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/). - MCP server scaffold {% endif %} {% endif %} +{% if type == 'cursor-plugin' %} - CI/CD workflows (validate, release, pages, stale) +{% else %} +- CI/CD workflows (ci, release, publish, drift-check, pages, stale, label-sync) +{% endif %} - GitHub Pages documentation site diff --git a/scaffold/templates/CLAUDE.md.j2 b/scaffold/templates/CLAUDE.md.j2 index 45fcc89..2023f3c 100644 --- a/scaffold/templates/CLAUDE.md.j2 +++ b/scaffold/templates/CLAUDE.md.j2 @@ -23,21 +23,32 @@ This file provides guidance for Claude Code when working in this repository. - MCP tools: `mcp-server/tools/` {% endif %} {% else %} -- Source: `src/` -- Package manifest: `package.json` +- Source: `src/` (TypeScript) +- Provider adapters: `src/providers/` (implement the `Provider` interface, wired into `ProviderManager`) +- Tools: `src/tools/` +- Package manifest: `package.json` (version source of truth) +- Tool list: `mcp-tools.json` (enumerates the MCP tools) {% endif %} - Docs site: `docs/` - CI workflows: `.github/workflows/` ## Conventions +{% if type == 'cursor-plugin' %} - Use conventional commits (`feat:`, `fix:`, `chore:`, `docs:`) -- Never manually edit the version in {% if type == 'cursor-plugin' %}plugin.json{% else %}package.json{% endif %} -- CI handles it +- Never manually edit the version in plugin.json -- CI handles it - All skills need YAML frontmatter with title, description, globs - All rules need frontmatter with description, globs, alwaysApply +{% else %} +- Use conventional commits (`feat:`, `fix:`, `chore:`, `docs:`) +- Never manually edit the version in `package.json` -- CI auto-bumps it +- Provider adapters live in `src/providers/` and implement the `Provider` interface, wired into `ProviderManager`; tools live in `src/tools/` +- Keep `mcp-tools.json` in sync with the registered tools +{% endif %} ## Testing +{% if type == 'cursor-plugin' %} {% if has_mcp %} ```bash cd mcp-server && pip install -r requirements.txt @@ -49,3 +60,10 @@ python3 -m py_compile server.py python3 -c "import json; json.load(open('.cursor-plugin/plugin.json'))" ``` {% endif %} +{% else %} +```bash +npm run build +npm test +npm run typecheck +``` +{% endif %} diff --git a/scaffold/templates/CONTRIBUTING.md.j2 b/scaffold/templates/CONTRIBUTING.md.j2 index 14a238f..52523f7 100644 --- a/scaffold/templates/CONTRIBUTING.md.j2 +++ b/scaffold/templates/CONTRIBUTING.md.j2 @@ -15,6 +15,7 @@ Thank you for your interest in contributing. Use [Conventional Commits](https://www.conventionalcommits.org/): +{% if type == 'cursor-plugin' %} - `feat:` -- new feature, skill, rule, or MCP tool - `fix:` -- bug fix - `docs:` -- documentation changes @@ -38,6 +39,32 @@ Use [Conventional Commits](https://www.conventionalcommits.org/): 1. Ensure CI passes (the `validate.yml` workflow checks structure and quality) 2. Update `CHANGELOG.md` if the change is user-facing 3. Use a descriptive PR title following conventional commit format +{% else %} +- `feat:` -- new feature, provider adapter, or tool +- `fix:` -- bug fix +- `docs:` -- documentation changes +- `chore:` -- maintenance, dependency updates +- `refactor:` -- code restructuring + +### Provider adapters + +- Implement the `Provider` interface in `src/providers/` +- Register the adapter in `ProviderManager` + +### Tools + +- Register the tool in `src/tools/` +- Add it to `mcp-tools.json` +- Add vitest tests + +Never hand-edit the version; CI auto-bumps `package.json`. + +## Pull Request Process + +1. Ensure CI passes (`npm run build`, `npm test`, `npm run typecheck`) +2. Update `CHANGELOG.md` if the change is user-facing +3. Use a descriptive PR title following conventional commit format +{% endif %} ## Inbound license grant and DCO @@ -52,7 +79,7 @@ By submitting a contribution to this repository, you certify that you have the r Every commit in a pull request must carry a `Signed-off-by:` trailer matching the commit author. Sign at commit time with the `-s` flag: ```bash -git commit -s -m "feat: add new skill" +{% if type == 'cursor-plugin' %}git commit -s -m "feat: add new skill"{% else %}git commit -s -m "feat: add new tool"{% endif %} ``` This appends a line like `Signed-off-by: Jane Developer ` to the commit message. The GitHub DCO App enforces this on every PR. diff --git a/scaffold/templates/ROADMAP.md.j2 b/scaffold/templates/ROADMAP.md.j2 index 411a719..c440a89 100644 --- a/scaffold/templates/ROADMAP.md.j2 +++ b/scaffold/templates/ROADMAP.md.j2 @@ -6,6 +6,7 @@ ## {{ name }} +{% if type == 'cursor-plugin' %} ### v0.1.x -- Foundation - [ ] Initial skills and rules @@ -30,3 +31,24 @@ - [ ] Full test coverage - [ ] Marketplace listing - [ ] Complete documentation +{% else %} +### v0.1.x -- Foundation + +- [x] Core provider adapters +- [x] Initial tool surface +- [x] stdio transport +- [ ] CI/CD workflows +- [ ] GitHub Pages documentation site + +### v0.2.0 -- Expansion + +- [ ] Additional provider adapters +- [ ] Richer tooling +- [ ] Streaming support + +### v1.0.0 -- Stable Release + +- [ ] Broader provider coverage +- [ ] Full test coverage +- [ ] Complete documentation +{% endif %} diff --git a/scaffold/templates/ci.yml.j2 b/scaffold/templates/ci.yml.j2 new file mode 100644 index 0000000..0fe1ed9 --- /dev/null +++ b/scaffold/templates/ci.yml.j2 @@ -0,0 +1,28 @@ +{% raw %}name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + build-and-test: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [20, 22] + steps: + - uses: actions/checkout@v6 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v6 + with: + node-version: ${{ matrix.node-version }} + cache: npm + - run: npm ci + - run: npm run build + - run: npm test +{% endraw %} \ No newline at end of file diff --git a/scaffold/templates/cursorrules.j2 b/scaffold/templates/cursorrules.j2 index ac2f0b1..28aa268 100644 --- a/scaffold/templates/cursorrules.j2 +++ b/scaffold/templates/cursorrules.j2 @@ -19,7 +19,14 @@ This is a {{ type }} repository for {{ name | replace(' Developer Tools', '') | ## Conventions +{% if type == 'cursor-plugin' %} - Use conventional commits (feat:, fix:, chore:, docs:) - Never manually edit the version -- CI handles it - Skills need YAML frontmatter: title, description, globs - Rules need frontmatter: description, globs, alwaysApply +{% else %} +- Use conventional commits (feat:, fix:, chore:, docs:) +- Never manually edit the version -- CI auto-bumps package.json +- Keep mcp-tools.json in sync with the registered tools +- Provider adapters live in src/providers/; tools live in src/tools/ +{% endif %} diff --git a/scaffold/templates/gitignore.j2 b/scaffold/templates/gitignore.j2 index c3572c4..10edb77 100644 --- a/scaffold/templates/gitignore.j2 +++ b/scaffold/templates/gitignore.j2 @@ -1,3 +1,4 @@ +{% if type == 'cursor-plugin' %} __pycache__/ *.pyc *.pyo @@ -11,3 +12,12 @@ build/ Thumbs.db node_modules/ *.log +{% else %} +node_modules/ +dist/ +build/ +.env +.DS_Store +Thumbs.db +*.log +{% endif %} diff --git a/scaffold/templates/publish.yml.j2 b/scaffold/templates/publish.yml.j2 index bbca65d..b53a66c 100644 --- a/scaffold/templates/publish.yml.j2 +++ b/scaffold/templates/publish.yml.j2 @@ -1,8 +1,9 @@ -name: Publish to npm +{% raw %}name: Publish to npm on: release: types: [published] + workflow_dispatch: {} permissions: contents: read @@ -22,8 +23,23 @@ jobs: - run: npm ci - run: npm run build - run: npm test -{% raw %} - - run: npm publish --provenance --access public + + - name: Check if this version is already published + id: pub + run: | + set -euo pipefail + name=$(node -p "require('./package.json').name") + ver=$(node -p "require('./package.json').version") + if npm view "$name@$ver" version >/dev/null 2>&1; then + echo "skip=true" >> "$GITHUB_OUTPUT" + echo "$name@$ver is already on npm; skipping publish (idempotent re-dispatch)." + else + echo "skip=false" >> "$GITHUB_OUTPUT" + fi + + - name: Publish to npm + if: steps.pub.outputs.skip == 'false' + run: npm publish --provenance --access public env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} -{% endraw %} +{% endraw %} \ No newline at end of file diff --git a/scaffold/templates/release.mcp.yml.j2 b/scaffold/templates/release.mcp.yml.j2 new file mode 100644 index 0000000..c0faa3a --- /dev/null +++ b/scaffold/templates/release.mcp.yml.j2 @@ -0,0 +1,105 @@ +{% raw %}name: Release + +on: + push: + branches: [main] + workflow_dispatch: {} + +permissions: + contents: write + +concurrency: + group: release + cancel-in-progress: false + +jobs: + release: + name: Version, tag, and release + runs-on: ubuntu-latest + # Skip the release commit's own push (it carries [skip version]) so the + # job never re-triggers itself into a loop. + if: "!contains(github.event.head_commit.message, '[skip version]')" + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + # A PAT lets the release event trigger publish.yml. GITHUB_TOKEN + # pushes/releases do NOT trigger downstream workflows by design, + # which is why publish never fires without this. Falls back to + # GITHUB_TOKEN (publish.yml can then be run via workflow_dispatch). + token: ${{ secrets.RELEASE_PAT != '' && secrets.RELEASE_PAT || secrets.GITHUB_TOKEN }} + + - uses: actions/setup-node@v6 + with: + node-version: 22 + + - name: Determine bump from conventional commits + id: bump + run: | + set -euo pipefail + last_tag=$(git describe --tags --abbrev=0 2>/dev/null || echo "") + if [ -z "$last_tag" ]; then + range_log=$(git log --pretty=format:'%s%n%b') + else + range_log=$(git log "${last_tag}..HEAD" --pretty=format:'%s%n%b') + fi + bump=patch + if printf '%s\n' "$range_log" | grep -qE 'BREAKING CHANGE' \ + || printf '%s\n' "$range_log" | grep -qE '^[a-z]+(\([^)]*\))?!:'; then + bump=major + elif printf '%s\n' "$range_log" | grep -qE '^feat(\([^)]*\))?:'; then + bump=minor + fi + echo "bump=$bump" >> "$GITHUB_OUTPUT" + echo "Computed bump: $bump (since ${last_tag:-})" + + - name: Apply version bump + id: ver + run: | + set -euo pipefail + new=$(npm version "${{ steps.bump.outputs.bump }}" --no-git-tag-version) + new=${new#v} + echo "version=$new" >> "$GITHUB_OUTPUT" + echo "New version: $new" + + - name: Update README version badge + run: | + set -euo pipefail + v="${{ steps.ver.outputs.version }}" + if [ -f README.md ]; then + sed -i -E "s|(badge/version-)[0-9]+\.[0-9]+\.[0-9]+(-blue)|\1${v}\2|g" README.md || true + fi + + - name: Guard against re-tagging an existing version + id: check + run: | + set -euo pipefail + v="${{ steps.ver.outputs.version }}" + if git rev-parse "v$v" >/dev/null 2>&1; then + echo "skip=true" >> "$GITHUB_OUTPUT" + echo "Tag v$v already exists; skipping release." + else + echo "skip=false" >> "$GITHUB_OUTPUT" + fi + + - name: Commit, tag, and create release + if: steps.check.outputs.skip == 'false' + env: + GH_TOKEN: ${{ secrets.RELEASE_PAT != '' && secrets.RELEASE_PAT || secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + v="${{ steps.ver.outputs.version }}" + IFS='.' read -r major minor _patch <<< "$v" + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add package.json package-lock.json README.md + git commit -m "chore(release): v$v [skip version]" + git tag "v$v" + git tag -f "v$major" + git tag -f "v$major.$minor" + git push origin HEAD:main + git push origin "v$v" + git push origin "v$major" --force + git push origin "v$major.$minor" --force + gh release create "v$v" --title "v$v" --generate-notes +{% endraw %} \ No newline at end of file diff --git a/scaffold/templates/site.json.j2 b/scaffold/templates/site.json.j2 index 6dbcd14..1fb29dd 100644 --- a/scaffold/templates/site.json.j2 +++ b/scaffold/templates/site.json.j2 @@ -7,8 +7,14 @@ "github": "https://github.com/{{ repo_owner }}/{{ repo_name }}" }, "installSteps": [ +{% if type == 'cursor-plugin' %} "Open Cursor IDE and go to Settings > Extensions", "Search for {{ name }}", "Click Install and reload" +{% else %} + "Install with npx -y @{{ repo_owner | lower }}/{{ slug }}", + "Add to your Claude Desktop or Cursor mcpServers config", + "Restart your MCP client" +{% endif %} ] } diff --git a/tests/test_scaffold_born_green.py b/tests/test_scaffold_born_green.py index 2d114ac..df35d6b 100644 --- a/tests/test_scaffold_born_green.py +++ b/tests/test_scaffold_born_green.py @@ -59,6 +59,15 @@ OPTIONAL_FOR_BOTH = frozenset({"label-sync.yml", "pages.yml"}) +# Workflows a type emits beyond its drift-required set and the +# optional-for-both pair. mcp-server repos emit a build/test CI and an +# auto-release workflow; these are intentionally NOT drift-required (a sibling +# that lacks them must not go red), so they live here rather than in +# drift-checker.config.json. +EMITTED_EXTRA: dict[str, frozenset[str]] = { + "mcp-server": frozenset({"ci.yml", "release.yml"}), +} + # Each case is a distinct render. ``type`` is the intended repo type, ``args`` # are extra CLI flags, ``detect`` is the type _detect_repo_type MUST return. # The "*-with-skills" / "*-empty" variants are the regression guards: @@ -286,7 +295,11 @@ def test_emitted_workflow_set_exact(rendered, label): for p in (repo / ".github" / "workflows").iterdir() if p.suffix in (".yml", ".yaml") ) - expected = _required_workflows(repo_type) | OPTIONAL_FOR_BOTH + expected = ( + _required_workflows(repo_type) + | OPTIONAL_FOR_BOTH + | EMITTED_EXTRA.get(repo_type, frozenset()) + ) assert present == expected, ( f"{label} emitted workflows {sorted(present)} != expected " f"{sorted(expected)} (required for {repo_type} union optional-for-both)"