From a256c5aca26df32b41d4f5cbda0f46008327c9bf Mon Sep 17 00:00:00 2001 From: cliffhall Date: Thu, 1 Oct 2026 09:58:56 -0400 Subject: [PATCH 1/7] chore: bring the contribution policy to main ahead of the v2.0.0 merge CONTRIBUTING.md, the PR template and the issue forms, byte-identical to v2/main, so the default branch states the issues-not-PRs policy before external pull requests are turned off. Nothing else changes and nothing is released. Co-Authored-By: Claude Opus 5.5 (1M context) Signed-off-by: cliffhall --- .github/ISSUE_TEMPLATE/1-bug_report.yml | 204 +++++++++++++++++++ .github/ISSUE_TEMPLATE/2-feature_request.yml | 135 ++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 58 ++++++ .github/pull_request_template.md | 54 ++++- CONTRIBUTING.md | 194 +++++++++++++++--- 5 files changed, 606 insertions(+), 39 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/1-bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/2-feature_request.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml diff --git a/.github/ISSUE_TEMPLATE/1-bug_report.yml b/.github/ISSUE_TEMPLATE/1-bug_report.yml new file mode 100644 index 0000000000..df01d473b3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/1-bug_report.yml @@ -0,0 +1,204 @@ +# Bug report form (GitHub issue forms schema). +# +# Requires the facts triage needs before anything else: which server, its +# version, how it was run, the transport, the protocol era the client speaks, +# and the client itself. The server dropdown is how triage picks the +# `server-` scope label; `labels:` below is static, so it cannot apply +# that label itself. `bug` is the type label and `v2` the version label: this +# repository has no v1 line, so every issue is a v2 issue (AGENTS.md). +# +# GitHub serves issue forms from the default branch (`main`), so a change here +# goes live at the next milestone merge from `v2/main`, not when it merges. +name: Bug report +description: Report something broken in one of the reference servers +labels: ["bug", "v2"] +body: + - type: markdown + attributes: + value: | + Thanks for reporting a bug. **Issues are how work reaches these servers.** + + This repository accepts **issues, not pull requests**: design and + implementation are done by the maintainers through a prompt-driven + workflow ([`CONTRIBUTING.md`](https://github.com/modelcontextprotocol/servers/blob/main/CONTRIBUTING.md)). + So if you have already prototyped a fix locally, **the prompt you used + is worth more to us than a diff**. There is a field for it at the bottom + of this form. + + > 🔒 **Do not report security vulnerabilities here.** Use the + > [private advisory form](https://github.com/modelcontextprotocol/servers/security/advisories/new) + > instead. + + - type: dropdown + id: server + attributes: + label: Which server? + description: > + The servers in this repository. A server you found in the MCP Server + Registry or elsewhere is maintained by its own authors; report its bugs + to them. + options: + - everything (@modelcontextprotocol/server-everything) + - filesystem (@modelcontextprotocol/server-filesystem) + - memory (@modelcontextprotocol/server-memory) + - sequentialthinking (@modelcontextprotocol/server-sequential-thinking) + - fetch (mcp-server-fetch) + - git (mcp-server-git) + - time (mcp-server-time) + - More than one server + - The repository itself (CI, docs, templates, release tooling) + validations: + required: true + + - type: input + id: server-version + attributes: + label: Server version + description: > + The version you actually ran, not "latest". For an unreleased build, + give the commit. For a repository-level report, write n/a. + placeholder: "0.6.3" + validations: + required: true + + - type: dropdown + id: install + attributes: + label: How did you run the server? + options: + - npx (npm package) + - uvx or pip (PyPI package) + - Docker image + - From a clone of this repository + - Other (say which below) + - Not applicable (a repository-level report) + validations: + required: true + + - type: dropdown + id: transport + attributes: + label: Transport + description: > + Every server speaks stdio. Only `everything` also serves Streamable HTTP + and the deprecated HTTP+SSE transport. + options: + - stdio + - Streamable HTTP + - HTTP+SSE (deprecated) + - Not sure + - Not applicable (a repository-level report) + validations: + required: true + + - type: dropdown + id: spec-era + attributes: + label: Protocol era + description: > + Which revision of the MCP specification the client spoke. Modern is + 2026-07-28, the stateless revision: there is no `initialize` handshake, + and the protocol version travels with each request. Legacy is + 2025-11-25 or earlier, where the version is agreed in `initialize`. + The client's logs, or the MCP Inspector, show which one was used. + options: + - Modern (2026-07-28) + - Legacy (2025-11-25 or earlier) + - Not sure + - Not applicable (a repository-level report) + validations: + required: true + + - type: input + id: client + attributes: + label: MCP client and version + description: > + The client that talked to the server, with its version. For a + repository-level report, write n/a. + placeholder: "Claude Desktop 1.2.3, MCP Inspector 2.0.0, a custom SDK client" + validations: + required: true + + - type: input + id: environment + attributes: + label: Operating system and runtime + description: Your OS, plus `node --version` or `python --version` as applicable. + placeholder: "macOS 15.5, Node v22.19.0" + validations: + required: false + + - type: textarea + id: config + attributes: + label: Server configuration + description: > + The command, arguments and environment variables the client launched the + server with (for example, the `mcpServers` entry). Redact tokens, + secrets and private paths. + validations: + required: false + + - type: textarea + id: repro + attributes: + label: Steps to reproduce + description: > + Numbered steps from a fresh start of the server, including the tool, + resource or prompt you called and its arguments. + placeholder: | + 1. Start the server with the configuration above + 2. Call `read_text_file` with `{"path": "/allowed/dir/link"}` + 3. ... + validations: + required: true + + - type: textarea + id: expected + attributes: + label: Expected behavior + description: What you expected to happen. + validations: + required: true + + - type: textarea + id: actual + attributes: + label: Actual behavior + description: What happened instead. + validations: + required: true + + - type: textarea + id: logs + attributes: + label: Logs or error output + description: > + The server's stderr, the client's MCP log, or the error the tool + returned. Redact tokens and secrets first. + render: text + validations: + required: false + + - type: textarea + id: prompt + attributes: + label: Already prototyped a fix? + description: > + Please don't attach a diff or open a pull request. Share the **exact + prompt(s)** you used to produce the change, the behavior before and + after, and how you verified it. We reproduce it through our own + workflow so it lands with the right conventions, tests and coverage. + validations: + required: false + + - type: checkboxes + id: acknowledgements + attributes: + label: Before you submit + options: + - label: I searched existing issues and this is not a duplicate. + required: true + - label: This is not a security vulnerability report (those go through the private advisory form). + required: true diff --git a/.github/ISSUE_TEMPLATE/2-feature_request.yml b/.github/ISSUE_TEMPLATE/2-feature_request.yml new file mode 100644 index 0000000000..32ca5fe830 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/2-feature_request.yml @@ -0,0 +1,135 @@ +# Feature request form (GitHub issue forms schema). +# +# Asks for the server and a problem statement first, since the servers are +# reference implementations and a feature is judged by what it demonstrates +# about the protocol (CONTRIBUTING.md, "What we act on"). The server dropdown is +# how triage picks the `server-` scope label; `labels:` below is static. +# `enhancement` is the type label and `v2` the version label: this repository +# has no v1 line, so every issue is a v2 issue (AGENTS.md). +# +# GitHub serves issue forms from the default branch (`main`), so a change here +# goes live at the next milestone merge from `v2/main`, not when it merges. +name: Feature request +description: Suggest an improvement to one of the reference servers +labels: ["enhancement", "v2"] +body: + - type: markdown + attributes: + value: | + Thanks for suggesting an improvement. + + **Maintainers do the implementation here**: this repository accepts + **issues, not pull requests** + ([`CONTRIBUTING.md`](https://github.com/modelcontextprotocol/servers/blob/main/CONTRIBUTING.md)). + That makes a sharply stated **problem** the most valuable thing you can + give us. It is what we design against, and it survives after a specific + solution turns out not to fit. + + These servers are **reference implementations**. We favor changes that + show how a part of the protocol is meant to be used (Resources, Prompts + and Roots as well as Tools) and are selective about other new features. + + > **New servers are not accepted here.** To publish or list a server, + > use the [MCP Server Registry](https://github.com/modelcontextprotocol/registry). + + - type: dropdown + id: server + attributes: + label: Which server? + options: + - everything (@modelcontextprotocol/server-everything) + - filesystem (@modelcontextprotocol/server-filesystem) + - memory (@modelcontextprotocol/server-memory) + - sequentialthinking (@modelcontextprotocol/server-sequential-thinking) + - fetch (mcp-server-fetch) + - git (mcp-server-git) + - time (mcp-server-time) + - More than one server + - The repository itself (CI, docs, templates, release tooling) + validations: + required: true + + - type: dropdown + id: spec-era + attributes: + label: Protocol era + description: > + Which revision of the MCP specification the request concerns. Modern is + 2026-07-28, the stateless revision; legacy is 2025-11-25 or earlier. + options: + - Modern (2026-07-28) + - Legacy (2025-11-25 or earlier) + - Both, or not era-specific + - Not sure + validations: + required: true + + - type: input + id: client + attributes: + label: MCP client + description: The client you use the server with, if it matters to the request. + placeholder: "Claude Desktop, MCP Inspector, a custom SDK client" + validations: + required: false + + - type: textarea + id: problem + attributes: + label: The problem + description: > + What are you trying to do, and what makes it hard or impossible today? + Describe the situation, not the feature, including how often you hit it + and what you do instead right now. + validations: + required: true + + - type: textarea + id: solution + attributes: + label: Solution you have in mind (optional) + description: > + If you have a concrete idea, describe it here. It's fine to leave this + blank; the problem above is the part we need. + validations: + required: false + + - type: textarea + id: protocol + attributes: + label: Which MCP feature does it demonstrate? (optional) + description: > + If the change would show off a part of the protocol (a resource + template, a prompt, Roots, elicitation, and so on), say which, with a + link to the specification section if you have one. + validations: + required: false + + - type: textarea + id: alternatives + attributes: + label: Alternatives or workarounds you have tried + validations: + required: false + + - type: textarea + id: prompt + attributes: + label: Already built it locally? + description: > + Please don't attach a diff or open a pull request. Share the **exact + prompt(s)** you used, the result, and how you verified it. We reproduce + the work through our own workflow so it lands with the right + conventions, tests and coverage. + validations: + required: false + + - type: checkboxes + id: acknowledgements + attributes: + label: Before you submit + options: + - label: I searched existing issues and this is not a duplicate. + required: true + - label: This is a request for this repository or one of its servers, not a new server, the MCP specification or an SDK. + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..e05ce1099f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,58 @@ +# Issue chooser configuration (GitHub issue template config schema). +# +# Blank issues are disabled so every report lands in a form: the bug form +# requires the facts triage needs first (server, version, transport, protocol +# era, client), and the feature form requires the server and a problem +# statement. +# +# Security reports are deliberately a *contact link* rather than a form: a form +# would still open a public issue, which is exactly what a vulnerability report +# must not do. The link leaves the issue flow and opens the private advisory +# form; private vulnerability reporting is enabled on this repository. +# +# New servers and server listings are also contact links, to the MCP Server +# Registry, because this repository accepts neither (CONTRIBUTING.md). +# +# GitHub reads this file from the default branch (`main`), so a change here +# goes live at the next milestone merge from `v2/main`, not when it merges. +blank_issues_enabled: false +contact_links: + - name: "🔒 Security vulnerability: report privately" + url: https://github.com/modelcontextprotocol/servers/security/advisories/new + about: > + Never report a vulnerability in a public issue. Private vulnerability + reporting is enabled on this repository, and this link opens the advisory + form. A vulnerability in an MCP SDK goes to that SDK's repository instead. + + - name: "📦 New server, or listing a server: use the MCP Server Registry" + url: https://github.com/modelcontextprotocol/registry + about: > + This repository does not accept new server implementations or server + listings. Publish your server to the Registry to make it discoverable; + browse published servers at https://registry.modelcontextprotocol.io/. + + - name: '🤝 Contribution policy: why there is no "New pull request"' + url: https://github.com/modelcontextprotocol/servers/blob/main/CONTRIBUTING.md + about: > + This repository accepts issues, not pull requests; maintainers do the + implementation. If you already built a change locally, open an issue and + share the prompt you used rather than a diff. + + - name: "📐 MCP specification: protocol questions and proposals" + url: https://github.com/modelcontextprotocol/modelcontextprotocol/issues + about: > + If the behavior you're reporting is defined by the protocol rather than by + one of these servers, file it against the specification repository. + + - name: "🧰 MCP SDKs: TypeScript and Python" + url: https://github.com/modelcontextprotocol + about: > + The servers are built on the TypeScript and Python SDKs. If a bug + reproduces against the SDK directly, outside these servers, file it in + that SDK's repository. + + - name: "💬 Questions and community" + url: https://modelcontextprotocol.io/community/communication + about: > + Usage questions and discussion: the MCP Contributor Discord and the other + community channels, and how each is used. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index dd93c47980..7a9822734a 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,39 +1,80 @@ - + -To publish your server, follow the [quickstart guide](https://github.com/modelcontextprotocol/registry/blob/main/docs/modelcontextprotocol-io/quickstart.mdx). You can browse published servers at [https://registry.modelcontextprotocol.io/](https://registry.modelcontextprotocol.io/). +> **Heads up:** this repository accepts **issues, not pull requests**, from +> anyone but the repository maintainers. Please read +> [`CONTRIBUTING.md`](https://github.com/modelcontextprotocol/servers/blob/main/CONTRIBUTING.md) before continuing. If you're not a +> maintainer, open an issue (and share the prompt you used, if you've already +> built the change) rather than this PR. To make a server discoverable, publish +> it to the [MCP Server Registry](https://github.com/modelcontextprotocol/registry). + +Closes # + +## Description ## Server Details + -- Server: + +- Server: - Changes to: ## Motivation and Context + ## How Has This Been Tested? + ## Breaking Changes + ## Types of changes + + - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist + + - [ ] I have read the [MCP Protocol Documentation](https://modelcontextprotocol.io) - [ ] My changes follows MCP security best practices - [ ] I have updated the server's README accordingly +- [ ] I have added a changeset (`npm run changeset`) if this changes what a TypeScript server publishes - [ ] I have tested this with an LLM client - [ ] My code follows the repository's style guidelines - [ ] New and existing tests pass locally @@ -41,4 +82,5 @@ To publish your server, follow the [quickstart guide](https://github.com/modelco - [ ] I have documented all environment variables and configuration options ## Additional context + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8ce9a21dd6..6adb107d8c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,40 +1,168 @@ # Contributing to MCP Servers -Thanks for your interest in contributing! Here's how you can help make this repo better. - -We accept changes through [the standard GitHub flow model](https://docs.github.com/en/get-started/using-github/github-flow). - -## Server Listings - -The README no longer contains a list of third-party MCP servers — that list has been retired in favor of the [MCP Server Registry](https://github.com/modelcontextprotocol/registry). To make your server discoverable, follow the [quickstart guide](https://github.com/modelcontextprotocol/registry/blob/main/docs/modelcontextprotocol-io/quickstart.mdx) to publish it there. - -You can browse published servers at [https://registry.modelcontextprotocol.io/](https://registry.modelcontextprotocol.io/). - -## Server Implementations - -We welcome: -- **Bug fixes** — Help us squash those pesky bugs. -- **Usability improvements** — Making servers easier to use for humans and agents. -- **Enhancements that demonstrate MCP protocol features** — We encourage contributions that help reference servers better illustrate underutilized aspects of the MCP protocol beyond just Tools, such as Resources, Prompts, or Roots. For example, adding Roots support to filesystem-server helps showcase this important but lesser-known feature. +Thank you for your interest in improving the MCP reference servers. Contributors +are genuinely valued, and this document explains how to get your input into a +form the maintainers can act on quickly and consistently. + +## TL;DR + +**We accept issues, not pull requests.** Design and implementation are done by +the repository maintainers. If you've already built a fix or feature locally, +share **the prompt you used** to produce it, not the source code. This applies +to everyone outside the repository maintainers, including organization members +who happen to have write access to this repository. + +## Why this policy exists + +The reference servers are developed with an AI-assisted, prompt-driven workflow +built around shared conventions and strict gates (see [`AGENTS.md`](./AGENTS.md)): +seven independently published packages in two languages, each with its own +tests, type checks, lint and release steps, all tracked on one project board. + +A diff written outside that workflow has to be reverse-engineered to fit those +conventions, tests and gates, and it is often faster to re-derive the change than +to adapt the patch. Many outside pull requests also race each other for the same +bug. A well-formed issue captures your **intent**, and the **prompt** behind a +local change lets us reproduce the work inside our own workflow, with the quality +bar already built in. + +This policy is about efficiency, not gatekeeping. Your bug reports, ideas and +prompts directly shape what gets built. + +## Who opens pull requests + +Pull requests against this repository are opened by the **repository +maintainers** only. That includes organization members with write access: being +able to push a branch here is not the same as being asked to. The constraint is +the workflow described above, not permissions. + +If you're not a repository maintainer, open a **detailed issue** instead and a +maintainer will pick it up. A pull request from anyone else is closed with a +pointer back to this document; if it held a fix worth keeping, a maintainer files +an issue for it first, so the work is not lost. + +**Every pull request references an issue**, including the maintainers' own. Work +is tracked on the project board through issues, so a PR without one is invisible +to the board. That is why a well-formed issue is the useful contribution here, +and why writing one is never wasted effort. + +## How to report a bug or request a feature + +Open a well-formed issue. [**New issue**](https://github.com/modelcontextprotocol/servers/issues/new/choose) +offers a **Bug report** and a **Feature request** form, and both start by asking +which server the issue is about. The bug form also asks for the server version, +how you ran it, the transport, the protocol era your client speaks and the client +you used, because those are the facts triage needs first. + +GitHub serves the issue chooser from the repository's **default branch** +(`main`), and development happens on `v2/main`, which reaches `main` at milestone +releases. So the forms appear in the chooser from the first milestone release +that contains them; until then, open a blank issue and give the same details. + +The chooser also links to the private security-advisory form and to the MCP +Server Registry. **Never report a security vulnerability in a public issue**; use +[the private advisory form](https://github.com/modelcontextprotocol/servers/security/advisories/new) +instead. + +### What we act on + +The servers here are **reference implementations**, meant to show how each part +of the protocol is used, not general-purpose products. Issues are most likely to +be picked up when they ask for: + +- **Bug fixes.** +- **Usability improvements**: making the servers easier to use for humans and + agents. +- **Enhancements that demonstrate MCP protocol features.** We especially want the + reference servers to illustrate underused parts of the protocol beyond Tools, + such as Resources, Prompts or Roots. For example, adding Roots support to the + filesystem server showcases an important but lesser-known feature. We're more selective about: -- **Other new features** — Especially if they're not crucial to the server's core purpose or are highly opinionated. The existing servers are reference servers meant to inspire the community. If you need specific features, we encourage you to build enhanced versions and publish them to the [MCP Server Registry](https://github.com/modelcontextprotocol/registry)! We think a diverse ecosystem of servers is beneficial for everyone. - -We don't accept: -- **New server implementations** — We encourage you to publish them to the [MCP Server Registry](https://github.com/modelcontextprotocol/registry) instead. - -## Testing - -When adding or configuring tests for servers implemented in TypeScript, use **vitest** as the test framework. Vitest provides better ESM support, faster test execution, and a more modern testing experience. - -## Documentation -Improvements to existing documentation is welcome - although generally we'd prefer ergonomic improvements than documenting pain points if possible! +- **Other new features**, especially ones that are not central to a server's + purpose or are highly opinionated. If you need a specific feature, we encourage + you to build an enhanced version and publish it to the + [MCP Server Registry](https://github.com/modelcontextprotocol/registry). A + diverse ecosystem of servers is good for everyone. +- **Wholly new documentation**, especially if it is not vendor neutral (for + example, how to run a particular server with a particular client). + Improvements to existing documentation are welcome, though we generally prefer + fixing the rough edge to documenting it. -We're more selective about adding wholly new documentation, especially in ways that aren't vendor neutral (e.g. how to run a particular server with a particular client). - -## Community - -[Learn how the MCP community communicates](https://modelcontextprotocol.io/community/communication). +We don't accept: -Thank you for helping make MCP servers better for everyone! \ No newline at end of file +- **New server implementations.** Publish them to the + [MCP Server Registry](https://github.com/modelcontextprotocol/registry) + instead. +- **Server listings.** The README's list of third-party servers has been retired + in favor of the Registry. To make your server discoverable, follow the + Registry's [quickstart guide](https://github.com/modelcontextprotocol/registry/blob/main/docs/modelcontextprotocol-io/quickstart.mdx). + You can browse published servers at + [registry.modelcontextprotocol.io](https://registry.modelcontextprotocol.io/). + +## If you've already fixed it locally + +Please don't send a diff or open a pull request. Instead, open an issue that +includes: + +- **The prompt(s) you used** to generate the change: the exact text, so we can + reproduce it through our own workflow. +- **The behavior before and after** your change. +- **How you verified it**: the steps you ran, the tests you added, what you + observed, and the MCP client you tried it with. + +We'll reproduce the change through our workflow so it lands with the right +conventions, tests and coverage. + +## What makes a good issue + +A great issue gives us everything we need to act without a round-trip: + +- **The server** it concerns (`everything`, `filesystem`, `memory`, + `sequentialthinking`, `fetch`, `git` or `time`), and its version. +- **A clear reproduction or use case**: exact steps to reproduce a bug, or a + concrete description of the problem a feature would solve. +- **Expected and actual behavior**: what you expected, and what you saw instead. +- **The client and environment**: the MCP client and its version, the protocol + era it speaks, the transport, how you ran the server (`npx`, `uvx`, Docker, from + source), your OS, and the relevant configuration with secrets redacted. +- **The exact prompt text**, if you generated a local change. + +If you're unsure how to scope something, open the issue anyway and say so. We'll +help shape it. + +## Want to work on the servers with us? + +The issues-only policy is about how **unsolicited patches** are handled; it is not +a closed door. If you'd like to contribute at a deeper level, in general or in a +specific area, we'd like to hear from you. See +[how the MCP community communicates](https://modelcontextprotocol.io/community/communication) +for the Contributor Discord, the community calls, and how each channel is used. +From there we can scope a piece of work with you and supervise it through our +workflow. + +All participation is governed by the [Code of Conduct](./CODE_OF_CONDUCT.md). + +## For maintainers + +The rules for maintainers and the agents working for them are in +[`AGENTS.md`](./AGENTS.md), and the procedures are in the skills it indexes. Before +pushing, run `npm run format` at the repository root and then +**`npm run local:gate`**, which runs every check CI runs for both languages: +the repo-wide guards, each TypeScript server's `validate` (format check, lint +where a warning fails like an error, typecheck, build, tests), each Python +server's `validate:py` chain, the skills validator, and a boot smoke of every +server. [`docs/quality-gate.md`](./docs/quality-gate.md) describes each stage. +While iterating, `npm run validate -w src/` checks a single TypeScript +server and `npm run validate:py -- ` a single Python one; neither +replaces the gate. + +A pull request that changes what a TypeScript server publishes also carries a +**changeset** (`npm run changeset`), which is how that server's next version +and its CHANGELOG entry are decided; [`.changeset/README.md`](./.changeset/README.md) +says when one is needed and which bump to pick. The Python servers are versioned +by date instead. [`RELEASING.md`](./RELEASING.md) describes both, and how a +release is published. + +Thank you for helping make the MCP servers better for everyone! From 9e62490b72bb49c3a9581998f09e3db02bce5d5c Mon Sep 17 00:00:00 2001 From: cliffhall Date: Sat, 3 Oct 2026 23:04:14 -0400 Subject: [PATCH 2/7] docs(contributing): sync with v2/main after #4959 Takes CONTRIBUTING.md from v2/main, which dropped the transitional blank-issue fallback (#4940), so this branch stays byte-identical to v2/main and main does not receive the stale sentence. Co-Authored-By: Claude Opus 5.5 Signed-off-by: cliffhall --- CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6adb107d8c..dcb466b864 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -56,8 +56,8 @@ you used, because those are the facts triage needs first. GitHub serves the issue chooser from the repository's **default branch** (`main`), and development happens on `v2/main`, which reaches `main` at milestone -releases. So the forms appear in the chooser from the first milestone release -that contains them; until then, open a blank issue and give the same details. +releases. So a change to the forms goes live at the next milestone merge, not +when it merges into `v2/main`. The chooser also links to the private security-advisory form and to the MCP Server Registry. **Never report a security vulnerability in a public issue**; use From e4e721e3f81be0bcebd7379af4d06fbe60607ef8 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Sat, 3 Oct 2026 23:35:58 -0400 Subject: [PATCH 3/7] chore: sync the PR template with v2/main after #4961 Takes .github/pull_request_template.md from v2/main, which reduced it to the issues-not-PRs banner (#4960), so this branch stays byte-identical to v2/main. Co-Authored-By: Claude Opus 5.5 Signed-off-by: cliffhall --- .github/pull_request_template.md | 80 -------------------------------- 1 file changed, 80 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 7a9822734a..90f4c2921e 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,86 +1,6 @@ - - > **Heads up:** this repository accepts **issues, not pull requests**, from > anyone but the repository maintainers. Please read > [`CONTRIBUTING.md`](https://github.com/modelcontextprotocol/servers/blob/main/CONTRIBUTING.md) before continuing. If you're not a > maintainer, open an issue (and share the prompt you used, if you've already > built the change) rather than this PR. To make a server discoverable, publish > it to the [MCP Server Registry](https://github.com/modelcontextprotocol/registry). - -Closes # - -## Description - -## Server Details - - - -- Server: -- Changes to: - -## Motivation and Context - - - -## How Has This Been Tested? - - - -## Breaking Changes - - - -## Types of changes - - - -- [ ] Bug fix (non-breaking change which fixes an issue) -- [ ] New feature (non-breaking change which adds functionality) -- [ ] Breaking change (fix or feature that would cause existing functionality to change) -- [ ] Documentation update - -## Checklist - - - -- [ ] I have read the [MCP Protocol Documentation](https://modelcontextprotocol.io) -- [ ] My changes follows MCP security best practices -- [ ] I have updated the server's README accordingly -- [ ] I have added a changeset (`npm run changeset`) if this changes what a TypeScript server publishes -- [ ] I have tested this with an LLM client -- [ ] My code follows the repository's style guidelines -- [ ] New and existing tests pass locally -- [ ] I have added appropriate error handling -- [ ] I have documented all environment variables and configuration options - -## Additional context - - From a20778763c78013ee61696f6c3cd7e76f51f81f6 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Sat, 3 Oct 2026 23:59:18 -0400 Subject: [PATCH 4/7] chore: sync the bug report form with v2/main after #4965 Takes .github/ISSUE_TEMPLATE/1-bug_report.yml from v2/main, whose Server version placeholder now shows both version schemes (#4964), so this branch stays byte-identical to v2/main. Co-Authored-By: Claude Opus 5.5 Signed-off-by: cliffhall --- .github/ISSUE_TEMPLATE/1-bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/1-bug_report.yml b/.github/ISSUE_TEMPLATE/1-bug_report.yml index df01d473b3..752ceb698c 100644 --- a/.github/ISSUE_TEMPLATE/1-bug_report.yml +++ b/.github/ISSUE_TEMPLATE/1-bug_report.yml @@ -57,7 +57,7 @@ body: description: > The version you actually ran, not "latest". For an unreleased build, give the commit. For a repository-level report, write n/a. - placeholder: "0.6.3" + placeholder: "1.0.0 (npm) or 2026.8.1 (PyPI)" validations: required: true From e87a2de17d4c2babf79d5a14e47d344f77b3ec67 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Sat, 10 Oct 2026 01:03:56 -0400 Subject: [PATCH 5/7] docs(contributing): sync with v2/main after 1cdd4a70 Brings main's CONTRIBUTING.md byte-identical to v2/main's, so the v1.0.0 milestone merge (#5080) produces v2/main's tree without a conflict. Co-Authored-By: Claude Opus 5.5 Signed-off-by: cliffhall --- CONTRIBUTING.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dcb466b864..99464a5090 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -151,9 +151,10 @@ The rules for maintainers and the agents working for them are in pushing, run `npm run format` at the repository root and then **`npm run local:gate`**, which runs every check CI runs for both languages: the repo-wide guards, each TypeScript server's `validate` (format check, lint -where a warning fails like an error, typecheck, build, tests), each Python -server's `validate:py` chain, the skills validator, and a boot smoke of every -server. [`docs/quality-gate.md`](./docs/quality-gate.md) describes each stage. +where a warning fails like an error, typecheck, build, tests) and per-file +coverage gate (`coverage`), each Python +server's `validate:py` chain and per-file coverage gate (`coverage:py`), the +skills validator, and a boot smoke of every server. [`docs/quality-gate.md`](./docs/quality-gate.md) describes each stage. While iterating, `npm run validate -w src/` checks a single TypeScript server and `npm run validate:py -- ` a single Python one; neither replaces the gate. From f39bc6bbe7d9a74b6e6d9a6f0bb3e34ef1dd1810 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Sun, 11 Oct 2026 01:06:59 -0400 Subject: [PATCH 6/7] ci: bring the main-source guard to main ahead of the next milestone merge The guard (#5119, PR #5120 on v2/main) runs on pull_request_target, which loads the workflow and script from main. Bring the same three files here byte-for-byte, so the check can run on, and be required for, PRs into main before the next milestone merge carries them. Part of #5119 Co-Authored-By: Claude Opus 5.5 Signed-off-by: cliffhall --- .github/workflows/main-source.yml | 59 ++++++++ scripts/verify-main-source.mjs | 142 ++++++++++++++++++ scripts/verify-main-source.test.mjs | 219 ++++++++++++++++++++++++++++ 3 files changed, 420 insertions(+) create mode 100644 .github/workflows/main-source.yml create mode 100644 scripts/verify-main-source.mjs create mode 100644 scripts/verify-main-source.test.mjs diff --git a/.github/workflows/main-source.yml b/.github/workflows/main-source.yml new file mode 100644 index 0000000000..7a2ee9e5e6 --- /dev/null +++ b/.github/workflows/main-source.yml @@ -0,0 +1,59 @@ +# A pull request into `main` brings nothing that is not already on `v2/main` +# (#5119). `main` only ever receives a milestone's merge PR, a pure merge of +# `v2/main`; GitHub rulesets cannot restrict a PR's source branch, so this +# check, required by the `main` ruleset, is what does. The rule and why it is +# on the head commit rather than the branch name are in +# `scripts/verify-main-source.mjs`. +# +# On `pull_request_target`, unlike dco.yml: it runs `main`'s copy of this +# workflow and of the script, so a PR cannot edit the guard into passing. That +# is safe here because nothing from the PR runs: the checkout is `main`, and +# the PR's head is only fetched as git objects for `git merge-base` to read. +# Never add a step that checks out, installs or runs the PR's code. +# `edited` is there so retargeting a PR onto `main` runs the check. +on: + pull_request_target: + types: [opened, synchronize, reopened, edited] + branches: [main] + +# Read-only, and handed no secret, so this job holds no credential and stays +# outside the SHA-pin rule (`verify:action-pins`). +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + main-source: + timeout-minutes: 5 + name: Source is v2/main + runs-on: ubuntu-latest + steps: + # The base branch (`main`), which `pull_request_target` checks out by + # default; never the PR's ref. + - uses: actions/checkout@v6 + with: + persist-credentials: false + + # The PR's head and `v2/main`, as objects only. A fork PR's head is + # reachable here through `refs/pull//head`. Full history, so + # `git merge-base` can walk both. + - name: Fetch the PR head and v2/main + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + git fetch --no-tags --unshallow origin \ + "+refs/heads/v2/main:refs/remotes/origin/v2/main" \ + "+refs/pull/${PR_NUMBER}/head:refs/remotes/pr/head" + + - uses: actions/setup-node@v7 + with: + node-version: 22 + + # Node built-ins and git only, so no `npm ci`. + - name: Check the head is already on v2/main + env: + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: node scripts/verify-main-source.mjs --head "$HEAD_SHA" diff --git a/scripts/verify-main-source.mjs b/scripts/verify-main-source.mjs new file mode 100644 index 0000000000..280f16db1c --- /dev/null +++ b/scripts/verify-main-source.mjs @@ -0,0 +1,142 @@ +#!/usr/bin/env node +// Guard: a pull request into `main` brings nothing that is not already on +// `v2/main` (#5119). +// +// `main` is the release line. The one PR that may target it is a milestone's +// merge PR, a pure merge of `v2/main` (AGENTS.md, "Contributing"). GitHub +// rulesets can require a PR and its approvals, but cannot say where a PR comes +// from, so before this guard a PR from any branch could merge into `main`. +// +// The rule is on the COMMIT, not the branch name: the PR's head must be an +// ancestor of the source (`origin/v2/main`), or the source itself. The +// `release` skill opens the merge PR from `v2/chore/-release-`, +// a branch pushed from `origin/v2/main`, so a check on the head's name would +// refuse the one PR the rule exists to allow. An ancestor rather than only an +// equal commit, because `v2/main` keeps moving while the merge PR is open, and +// the PR is still a pure merge of what it was opened with. +// +// CI runs it from `main-source.yml` on `pull_request_target`, which runs +// `main`'s copy of the workflow and of this script, so a PR cannot edit the +// guard into passing. The PR's head is only fetched as git objects; none of +// its code runs. It is not a `local:gate` stage: every local push targets a +// v2 branch, where the rule does not apply. + +import { spawnSync } from "node:child_process"; +import path from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +/** The branch every commit on `main` must already be on. */ +export const DEFAULT_SOURCE = "origin/v2/main"; + +/** How many of the offending commits the report lists. */ +export const MAX_LISTED = 20; + +/** + * Pure: `--head ` (required) and `--source `. + * + * @param {string[]} argv + */ +export function parseArgs(argv) { + const opts = { source: DEFAULT_SOURCE, head: "" }; + for (let i = 0; i < argv.length; i++) { + const flag = argv[i]; + if (flag !== "--source" && flag !== "--head") + throw new Error(`unknown argument: ${flag}`); + const value = argv[++i]; + if (!value) throw new Error(`${flag} needs a revision`); + opts[flag.slice(2)] = value; + } + if (!opts.head) throw new Error("--head is required"); + return opts; +} + +/** + * Pure: the report for a head that carries commits the source does not. + * + * @param {{ source: string, head: string, commits: string[] }} args + * `commits` is `git log --oneline ..`, one per entry + */ +export function formatReport({ source, head, commits }) { + const n = commits.length; + const listed = commits.slice(0, MAX_LISTED).map((c) => ` ${c}`); + if (n > MAX_LISTED) listed.push(` … and ${n - MAX_LISTED} more`); + return [ + `verify:main-source: ${head} carries ${n} commit${n === 1 ? "" : "s"} that ${source} does not:`, + ...listed, + "", + "A pull request into `main` must be a pure merge of `v2/main`: its head", + "is a commit already on `v2/main`. Land the change on `v2/main` through", + "its own PR, and it reaches `main` with the next milestone merge (the", + "`release` skill).", + ].join("\n"); +} + +function git(args, cwd) { + const r = spawnSync("git", args, { cwd, encoding: "utf8" }); + if (r.error) throw r.error; + return r; +} + +/** + * Check that `head` is `source` or an ancestor of it, in the repository at + * `cwd`. + * + * @returns {{ code: number, output: string }} + */ +export function verifyMainSource({ source, head, cwd }) { + for (const rev of [source, head]) { + if ( + git(["rev-parse", "--verify", "--quiet", `${rev}^{commit}`], cwd).status + ) + return { + code: 1, + output: + `verify:main-source: cannot resolve ${rev}.` + + (rev === DEFAULT_SOURCE + ? " Fetch it (`git fetch origin v2/main`), or name another source with --source." + : ""), + }; + } + const anc = git(["merge-base", "--is-ancestor", head, source], cwd); + if (anc.status === 0) + return { + code: 0, + output: `verify:main-source: ${head} is on ${source}; nothing else comes with it.`, + }; + if (anc.status !== 1) + return { + code: 1, + output: `verify:main-source: git merge-base failed:\n${anc.stderr}`, + }; + const log = git( + ["log", "--oneline", "--no-decorate", `${source}..${head}`], + cwd, + ); + if (log.status) + return { + code: 1, + output: `verify:main-source: git log failed:\n${log.stderr}`, + }; + const commits = log.stdout.split("\n").filter((l) => l.trim() !== ""); + return { code: 1, output: formatReport({ source, head, commits }) }; +} + +if ( + process.argv[1] && + import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href +) { + let opts; + try { + opts = parseArgs(process.argv.slice(2)); + } catch (e) { + console.error(`verify:main-source: ${e.message}`); + process.exit(2); + } + const repoRoot = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + "..", + ); + const { code, output } = verifyMainSource({ ...opts, cwd: repoRoot }); + (code === 0 ? console.log : console.error)(output); + process.exit(code); +} diff --git a/scripts/verify-main-source.test.mjs b/scripts/verify-main-source.test.mjs new file mode 100644 index 0000000000..2f457a7c4c --- /dev/null +++ b/scripts/verify-main-source.test.mjs @@ -0,0 +1,219 @@ +// Tests for `verify-main-source.mjs` (#5119): the arguments and the report, +// the guard end to end against throwaway repositories shaped like the +// release flow, a merge PR from a branch pushed off `v2/main` passes, as does +// one `v2/main` has since moved past; a branch with a commit of its own, or a +// merge of `v2/main` into something else, fails. Last, the properties of +// `main-source.yml` its safety rests on. Run via `npm run test:scripts`. + +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { devNull, tmpdir } from "node:os"; +import path from "node:path"; +import { parse } from "yaml"; +import { + DEFAULT_SOURCE, + MAX_LISTED, + formatReport, + parseArgs, + verifyMainSource, +} from "./verify-main-source.mjs"; + +test("parseArgs: --head is required, --source defaults to origin/v2/main", () => { + assert.deepEqual(parseArgs(["--head", "abc"]), { + source: DEFAULT_SOURCE, + head: "abc", + }); + assert.deepEqual(parseArgs(["--source", "v2/main", "--head", "abc"]), { + source: "v2/main", + head: "abc", + }); + assert.throws(() => parseArgs([]), /--head is required/); + assert.throws(() => parseArgs(["--head"]), /--head needs a revision/); + assert.throws(() => parseArgs(["abc"]), /unknown argument/); +}); + +test("formatReport: names the commits, caps the list, and says where the change belongs", () => { + const commits = Array.from( + { length: MAX_LISTED + 3 }, + (_, i) => `${String(i).padStart(7, "0")} commit ${i}`, + ); + const out = formatReport({ source: "origin/v2/main", head: "abc", commits }); + assert.match( + out, + new RegExp( + `abc carries ${MAX_LISTED + 3} commits that origin/v2/main does not`, + ), + ); + assert.match(out, /0000000 commit 0/); + assert.doesNotMatch(out, new RegExp(`commit ${MAX_LISTED}\\b`)); + assert.match(out, /… and 3 more/); + assert.match(out, /pure merge of `v2\/main`/); + assert.match( + formatReport({ source: "s", head: "h", commits: ["1234567 one"] }), + /carries 1 commit that s does not/, + ); +}); + +// End to end, against a real repository: `main` and `v2` stand in for `main` +// and `origin/v2/main`, sharing a root. + +function repo() { + const dir = mkdtempSync(path.join(tmpdir(), "verify-main-source-")); + const env = { + ...process.env, + GIT_CONFIG_GLOBAL: devNull, + GIT_CONFIG_NOSYSTEM: "1", + GIT_AUTHOR_NAME: "Ada Lovelace", + GIT_AUTHOR_EMAIL: "ada@example.com", + GIT_COMMITTER_NAME: "Ada Lovelace", + GIT_COMMITTER_EMAIL: "ada@example.com", + }; + const git = (...args) => { + const r = spawnSync("git", args, { cwd: dir, env, encoding: "utf8" }); + assert.equal(r.status, 0, `git ${args.join(" ")}: ${r.stderr}`); + return r.stdout.trim(); + }; + git("init", "-q", "-b", "main"); + git("commit", "-q", "--allow-empty", "-m", "root"); + git("switch", "-q", "-c", "v2"); + git("commit", "-q", "--allow-empty", "-m", "v2 work"); + return { + dir, + git, + check: (head) => verifyMainSource({ source: "v2", head, cwd: dir }), + done: () => rmSync(dir, { recursive: true, force: true }), + }; +} + +test("verifyMainSource: a release branch pushed from v2 passes", (t) => { + const r = repo(); + t.after(r.done); + r.git("branch", "release", "v2"); + const { code, output } = r.check("release"); + assert.equal(code, 0, output); + assert.match(output, /release is on v2/); +}); + +test("verifyMainSource: a head v2 has since moved past still passes", (t) => { + const r = repo(); + t.after(r.done); + r.git("branch", "release", "v2"); + r.git("commit", "-q", "--allow-empty", "-m", "later v2 work"); + assert.equal(r.check("release").code, 0); +}); + +test("verifyMainSource: a branch with a commit of its own fails, naming it", (t) => { + const r = repo(); + t.after(r.done); + r.git("switch", "-q", "-c", "topic", "v2"); + r.git("commit", "-q", "--allow-empty", "-m", "sneaky fix"); + const sha = r.git("rev-parse", "--short", "HEAD"); + const { code, output } = r.check("topic"); + assert.equal(code, 1); + assert.match(output, /topic carries 1 commit that v2 does not/); + assert.ok(output.includes(`${sha} sneaky fix`), output); + assert.doesNotMatch(output, /v2 work/); +}); + +test("verifyMainSource: a branch cut from main fails", (t) => { + const r = repo(); + t.after(r.done); + r.git("switch", "-q", "-c", "hotfix", "main"); + r.git("commit", "-q", "--allow-empty", "-m", "hotfix"); + const { code, output } = r.check("hotfix"); + assert.equal(code, 1); + assert.match(output, /hotfix carries 1 commit/); +}); + +test("verifyMainSource: v2 merged into another branch fails, merge commit and all", (t) => { + const r = repo(); + t.after(r.done); + r.git("switch", "-q", "-c", "mixed", "main"); + r.git("commit", "-q", "--allow-empty", "-m", "side work"); + r.git("merge", "-q", "--no-ff", "--no-edit", "v2"); + const { code, output } = r.check("mixed"); + assert.equal(code, 1); + assert.match(output, /mixed carries 2 commits/); + assert.match(output, /side work/); +}); + +test("verifyMainSource: an unresolvable source or head fails and says so", (t) => { + const r = repo(); + t.after(r.done); + const noSource = verifyMainSource({ + source: DEFAULT_SOURCE, + head: "HEAD", + cwd: r.dir, + }); + assert.equal(noSource.code, 1); + assert.match(noSource.output, /cannot resolve origin\/v2\/main\. Fetch it/); + const noHead = r.check("no-such-branch"); + assert.equal(noHead.code, 1); + assert.match(noHead.output, /cannot resolve no-such-branch\.$/); +}); + +test("verifyMainSource: run as a script, a bad argument exits 2", () => { + const r = spawnSync( + process.execPath, + [path.join(import.meta.dirname, "verify-main-source.mjs")], + { encoding: "utf8" }, + ); + assert.equal(r.status, 2); + assert.match(r.stderr, /--head is required/); +}); + +// The workflow's safety rests on properties of its YAML; pin them, so an edit +// that would run the PR's code, or stop wiring the guard, fails here. +const workflow = parse( + readFileSync( + path.join(import.meta.dirname, "..", ".github/workflows/main-source.yml"), + "utf8", + ), +); +const job = workflow.jobs["main-source"]; + +test("main-source.yml: runs main's copy, on PRs into main only, read-only", () => { + assert.deepEqual(Object.keys(workflow.on), ["pull_request_target"]); + assert.deepEqual(workflow.on.pull_request_target.branches, ["main"]); + assert.ok(workflow.on.pull_request_target.types.includes("edited")); + assert.deepEqual(workflow.permissions, { contents: "read" }); + assert.equal(job.permissions, undefined); + assert.deepEqual(Object.keys(workflow.jobs), ["main-source"]); + assert.equal(job.name, "Source is v2/main"); + assert.ok(job["timeout-minutes"]); +}); + +test("main-source.yml: checks out main, never the PR, and fetches the PR as objects only", () => { + const checkout = job.steps.filter((s) => + /actions\/checkout/.test(s.uses ?? ""), + ); + assert.equal(checkout.length, 1); + assert.equal(checkout[0].with?.ref, undefined); + assert.equal(checkout[0].with?.repository, undefined); + assert.equal(checkout[0].with["persist-credentials"], false); + const runs = job.steps.map((s) => s.run ?? "").join("\n"); + const refspec = '"+refs/pull/${PR_NUMBER}/head:refs/remotes/pr/head"'; + assert.ok(runs.includes(refspec), "the PR head is fetched"); + // Past that fetch, nothing names the PR's ref, or switches to, installs or + // executes a tree. + const rest = runs.replace(refspec, ""); + assert.doesNotMatch(rest, /pr\/head|refs\/pull/); + assert.doesNotMatch(rest, /git (checkout|switch|reset|worktree)|npm |npx /); + // No step reads a secret; expressions reach the shell only through env. + for (const step of job.steps) { + assert.doesNotMatch(JSON.stringify(step), /secrets\./); + assert.doesNotMatch(step.run ?? "", /\$\{\{/); + } +}); + +test("main-source.yml: hands the event's head SHA to the guard", () => { + const step = job.steps.find((s) => + /verify-main-source\.mjs/.test(s.run ?? ""), + ); + assert.ok(step, "a step runs the guard"); + assert.equal(step.env.HEAD_SHA, "${{ github.event.pull_request.head.sha }}"); + assert.match(step.run, /--head "\$HEAD_SHA"/); + assert.doesNotMatch(step.run, /--source/); +}); From 306f5a40e848abd09475fe18184662413b8003a7 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Mon, 19 Oct 2026 00:26:39 -0400 Subject: [PATCH 7/7] ci: run the main-source guard on Node 24 again (#5168) Re-applies #5169's change to main-source.yml on top of the merge with main, so it reaches main as an ordinary change to a shared file. Co-Authored-By: Claude Opus 5.5 (1M context) Signed-off-by: cliffhall --- .github/workflows/main-source.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main-source.yml b/.github/workflows/main-source.yml index 7a2ee9e5e6..d1da81dd27 100644 --- a/.github/workflows/main-source.yml +++ b/.github/workflows/main-source.yml @@ -50,7 +50,7 @@ jobs: - uses: actions/setup-node@v7 with: - node-version: 22 + node-version: 24 # Node built-ins and git only, so no `npm ci`. - name: Check the head is already on v2/main