# MCP reference servers This repository holds the official Model Context Protocol reference servers: seven standalone packages under `src/`, four in TypeScript (published to npm) and three in Python (published to PyPI). They are reference implementations, meant to show how each part of the protocol is used, not general-purpose products. **This file holds the _rules_: the conventions a reviewer cites against a diff.** It is loaded in full at the start of every session, so it stays resident and must be complete enough to work from on its own. Procedures (multi-step recipes with commands and live IDs) belong in skills under `.claude/skills/`, which load on demand. A rule that must never silently drop out of context goes here; a recipe that can be re-read from disk goes in a skill. Every rule in this file is true of the repository as it stands. A rule whose machinery does not exist yet is added by the change that builds that machinery, not in advance. ## Skills index | Skill | Covers | How it loads | | ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | | [`board-ops`](.claude/skills/board-ops/SKILL.md) | `gh project` recipes and the IDs for the Servers V2 board (#43); resolving option IDs by name; the option-deletion hazard and its recovery | Model-invoked, or `/board-ops` | | [`issue-create`](.claude/skills/issue-create/SKILL.md) | The create flow: duplicate check, `v2` + type + server-scope labels, milestone, board card, Status + Priority, and the query that verifies them | Model-invoked, or `/issue-create` | | [`pr-flow`](.claude/skills/pr-flow/SKILL.md) | Issue to PR: branch, DCO signoff and repair, the gate, client evidence, `addCloseIssueReferences`, the Copilot loop and its exits, close-out on merge | Model-invoked, or `/pr-flow` | | [`issue-triage`](.claude/skills/issue-triage/SKILL.md) | Inflow: the class check and canned responses (listings, new servers, duplicates, outside PRs), pass 1 onto the board as Incoming, the priority rubric and its score comment, the board audit | Model-invoked, or `/issue-triage` | | [`pre-push-gate`](.claude/skills/pre-push-gate/SKILL.md) | Running `npm run local:gate` and reading its result; what each stage checks and how to fix it when it fails; waiting on the gate lease | Model-invoked, or `/pre-push-gate` | | [`security-advisory`](.claude/skills/security-advisory/SKILL.md) | A privately reported vulnerability end to end: the `[GHSA-…]` draft card, server or SDK ownership, the reach classes, accepting, the private fork, publishing, public tracking | Model-invoked, or `/security-advisory` | | [`project-structure`](.claude/skills/project-structure/SKILL.md) | What is inside each server: the TypeScript and Python layouts, where each server registers its features, and where a new file goes | Model-invoked, or `/project-structure` | | [`local-dev`](.claude/skills/local-dev/SKILL.md) | Install, build and run each server from the checkout over the transports it implements; local `npx`/`uvx` and client-config runs; stale builds and fresh worktrees; the `overrides`, lockstep and `uv.lock` procedures | Model-invoked, or `/local-dev` | | [`testing`](.claude/skills/testing/SKILL.md) | The in-process protocol-level harness, test placement, the commands per suite, `test` versus `coverage`, and clearing the per-file coverage gate | Model-invoked, or `/testing` | | [`release`](.claude/skills/release/SKILL.md) | A milestone release end to end: the release issue, the preparation PRs on `v2/main` (audit, Version Packages, Python CalVer), the pure `v2/main` → `main` merge PR, the release ledger, and what a maintainer publishes | **Name-only**: `/release` | | [`client-smoke`](.claude/skills/client-smoke/SKILL.md) | Driving a built server with the Inspector CLI (the scripted path), the Inspector web UI (through Playwright) and an LLM client; the CLI's argument split and exit codes; protocol eras and what can be exercised today | Model-invoked, or `/client-smoke` | A PR that adds a skill under `.claude/skills//SKILL.md` adds its row to this table in the same change, and the table never lists a skill that does not exist. How skills are kept reachable is under [Maintaining the skills](#maintaining-the-skills). ## Project structure ``` servers/ ├── src/ One directory per server; each publishes on its own │ ├── everything/ TS @modelcontextprotocol/server-everything npm Exercises every MCP feature; the protocol showcase │ ├── filesystem/ TS @modelcontextprotocol/server-filesystem npm File operations, access limited to allowed directories / Roots │ ├── memory/ TS @modelcontextprotocol/server-memory npm Knowledge-graph persistence │ ├── sequentialthinking/ TS @modelcontextprotocol/server-sequential-thinking npm Step-by-step reasoning │ ├── fetch/ Py mcp-server-fetch PyPI Fetches web content and converts it for LLMs │ ├── git/ Py mcp-server-git PyPI Git repository operations │ └── time/ Py mcp-server-time PyPI Time and timezone conversion ├── .claude/skills/ On-demand procedures (see the Skills index above) ├── .claude/settings.json The SessionStart hook that checks this session's instructions are current ├── .changeset/ Pending changesets for the TypeScript servers, and the changesets config ├── scripts/ The pre-push gate (gate-lease, smoke-servers, validate-py, coverage-py, interface-diff), its guards (verify-*), │ the skills tooling, release tooling (npm-publish-guard, prepare-python-release, │ pack-and-verify, release-manifest, release-notes), and the issue-filing sweeps │ (dependency-refresh, dependabot-alerts, sdk-watch), and the session-start │ instructions check (session-instructions-check) ├── docs/ Design documents; quality-gate.md is the gate's reference (stages, CI vs local, the lease); │ contribution-model.md holds the outside-PR backlog plan; ai-software-factory.md is the │ overview of how the rules, skills, gates and sweeps fit together ├── .github/workflows/ typescript.yml, python.yml (per-package CI), release.yml (publishes on a GitHub Release), │ version-packages.yml (the changesets PR), prepare-python-release.yml (the CalVer PR), │ everything-mcp-diff.yml (each changed server's interface diff on PRs), │ main-source.yml (a PR into main must bring nothing that is not on v2/main), │ and the scheduled sweeps that file issues: dependency-refresh.yml, dependabot-alerts.yml, sdk-watch.yml ├── .github/ISSUE_TEMPLATE/ Bug and feature issue forms; config.yml routes security and new servers away ├── .github/pull_request_template.md The "issues, not PRs" banner that turns outside PRs away ├── RELEASING.md How packages are versioned and published, and how to recover a failed publish └── CONTRIBUTING.md The contribution policy: issues, not PRs; what is accepted ``` The four TypeScript servers are **npm workspaces** of the root `package.json`. The three Python servers are independent `uv` projects, each with its own `pyproject.toml`, `uv.lock` and `.python-version`. Every server versions and publishes independently. A server may carry its own `AGENTS.md` for guidance that only applies inside it (`src/everything/AGENTS.md` does). Claude Code loads a nested `AGENTS.md` when it reads a file in that directory, not at session start, so **a rule that applies repo-wide goes in this file, never only in a nested one.** ### New and rewritten source files open with a purpose header A source file you create, or substantially rewrite (code, tests, scripts, workflows, configuration in a format that has comments), opens with a comment stating what the file is for and the reasoning behind it: the first comment in the file, **after any shebang**. `#!/usr/bin/env node` must stay on line 1 of an executable entry point, or the published bin stops working. This is not a bulk migration: files you only touch in passing keep their current form. Read that header rather than restating its reasoning in this file, since a duplicated rationale goes stale silently. A Markdown document states its purpose in its opening paragraph instead, as this one does. A format with no comment syntax (JSON, such as `package.json`) is exempt. ## Development setup Node **24** for the TypeScript servers: the development version and the oldest supported (`engines` is `>=24`), and CI validates each TypeScript server on 24 and 26 in parallel legs. Python **>= 3.10** for the Python servers, at the version each server's `.python-version` pins. Python dependencies are managed with **`uv`, never `pip`**. ```sh # TypeScript: one install at the root covers all four workspaces npm install npm run build # every workspace npm test -w src/ # one workspace (vitest) npm run coverage -w src/ # the same suite, with the per-file coverage gate # Python: per server cd src/ uv sync --frozen --all-extras --dev uv run pytest uv run --frozen pyright uv run ruff check . uv build npm run coverage:py -- # at the root: the per-file coverage gate # Both languages: the pre-push gate (see Before pushing) npm run local:gate ``` ### Before pushing **Run `npm run local:gate` before every push, and push only when it exits 0.** It runs every check CI runs, for both languages, in one command: `verify:install-fresh`, `verify:dco` (every commit since `origin/v2/main` is signed off), the root `validate` (the guards, then each TypeScript workspace's format check, lint, typecheck, build and tests), `coverage` (each TypeScript workspace's per-file coverage gate), `validate:py` (each Python server's locked sync, `ruff check`, `ruff format --check`, pyright, pytest and build), `coverage:py` (each Python server's per-file coverage gate), `verify:skills:cli`, `smoke` (every server booted over each transport it implements), and `interface-diff` (the MCP interface of each server the change touches, diffed against the base it branched from). The stage-by-stage reference, and what CI runs where, is [`docs/quality-gate.md`](./docs/quality-gate.md); diagnosing a red stage is the `pre-push-gate` skill. - **`npm run validate` and the per-package commands above are the inner loop, not a substitute.** They skip the other language, the pinned skills validator, the boot smoke and the interface diff. - **Format before committing**: `npm run format` at the root (TypeScript), and `uv run --frozen ruff format .` in a Python server you changed. The gate only checks formatting; it never rewrites files. - **Judge the gate by its exit code**, not by reading its output: a Prettier failure prints `[warn]` lines that look like nothing. - **The gate runs under a machine-wide lease**, so a second gate started in another worktree queues behind the first instead of contending with it. A "waiting" line is normal; do not bypass it to save time. - **No test may retry.** A retry reports a test that sometimes fails as passing. `verify:no-test-retries` fails on Vitest's `retry` and on pytest's rerun plugins; fix what makes the test fail. - **`npm run pack:verify` is not a gate stage.** It installs each package's publish artifact into an empty directory and boots it, which needs the network. `release.yml` runs it before anything publishes, and the `release` skill runs it by hand for the ledger. - **`verify-main-source` is not a gate stage either.** It applies only to a pull request into `main` (see the merge PR exception under [Contributing](#contributing)), and every local push targets a v2 branch. `main-source.yml` runs it. - **`local:*` scripts never run in a workflow, and every CI job declares `timeout-minutes`.** The script tests enforce both (`scripts/lib/workflow-gate.mjs`). A check added to CI is added to `local:gate:stages` in the same change, so the first sentence of this section stays true. ### Credentialed workflow jobs A workflow job **holds a credential** when it can mint an OIDC token or push a package (`id-token: write`, `packages: write`), or is handed any secret other than `GITHUB_TOKEN`. Today that is `release.yml`'s publish jobs and `sdk-watch.yml`'s `analyze` and `post` jobs (`ANTHROPIC_API_KEY`). Jobs that hold none can still need pinning under the rules below: `release.yml`'s build jobs and `sdk-watch.yml`'s `sweep`, whose outputs `analyze` reads. - **Every action in a credentialed job is pinned to a full commit SHA, with the release it was resolved from in a trailing comment**: `uses: actions/checkout@<40-hex sha> # v6.1.0`. A tag can be moved, and a moved tag runs new code next to the credential with no change in this repository. The same holds for **a job whose artifact a credentialed job downloads**, since what it builds is what gets published, and for **a job whose outputs a credentialed job reads**, since the reader acts on them. `verify:action-pins` enforces both. Other jobs keep moving major tags. - **A job that holds a publish credential does not install dependencies or build.** `release.yml` builds, tests and verifies each package in a job with no `id-token`, and the publish job only downloads that artifact and hands it to the registry. ### Dependencies - **Pin a transitive dependency with an `overrides` entry** in the root `package.json`, not with `npm audit fix`, which "resolves" an advisory with no upward escape by silently downgrading. - **When you bump a devDependency that several TypeScript workspaces declare** (`typescript`, `vitest`, `@vitest/coverage-v8`, `@types/node`, …), bump it in every workspace that declares it, with one identical range everywhere. The `verify:dep-lockstep` guard (in `validate:guards`, so in CI) fails when two declarations of a package in its `SHARED_DEV_DEPENDENCIES` list differ. - **A Python dependency change updates that server's `uv.lock`** in the same commit (`uv lock`). CI installs with `--frozen` / `--locked` and fails on a stale lockfile. ### Dependency updates are issue-driven **Dependabot opens no pull requests here, for version updates or security updates.** A Dependabot PR carries no `Closes #N` and no board card, so both halves are replaced by scheduled workflows that file **issues**, and a maintainer writes each fix by hand against `v2/main`. Each script's header holds its reasoning; these are the rules. | Sweep | Replaces | Files | Cadence | | ----------------------------------------------------------- | -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | | `dependency-refresh.yml` → `scripts/dependency-refresh.mjs` | `.github/dependabot.yml` (deleted) | **One** tracking issue: `npm outdated` across the root and workspaces, each Python server's `uv lock --upgrade` dry run, and each workflow `uses:` ref against its action's highest release | Monthly | | `dependabot-alerts.yml` → `scripts/dependabot-alerts.mjs` | Security-update PRs (the `automated-security-fixes` **repo setting**, off) | One issue per vulnerable package per lockfile (npm and pip), targeting the lowest patched version outside every applicable range, flagging that no listed fix clears them all, or asking for a decision on an advisory with no patched release | Daily | | `sdk-watch.yml` → `scripts/sdk-watch.mjs` | Nothing: SDK releases were watched by habit | One issue per MCP SDK group behind its registry (npm `@modelcontextprotocol/*`, PyPI `mcp`), plus a Claude analysis comment | Nightly | - **Never re-add `.github/dependabot.yml` or switch `automated-security-fixes` back on.** Keep Dependabot **alerts** on: the alert sweep consumes them. The setting can be flipped in the UI with no commit, so the alert sweep reads it back and fails on `enabled: true` (under the workflow's token it can only report UNVERIFIED, since `administration: read` is not grantable). - **A sweep files an issue, never a PR, and never closes one.** Closing, and deciding whether the card goes to Done or is deleted, stays a maintainer's act. When the work goes away, the refresh and alert sweeps rewrite their issue to say so; the SDK watch leaves a supersession note on an issue a newer release has passed, and otherwise leaves the issue to the upgrade PR that closes it. - **Sweeps do not write the board.** `GITHUB_TOKEN` cannot hold `organization projects: write`. A sweep issue arrives labeled `v2` + `chore` + `dependencies` (+ a `server-` scope for each server it concerns) and carries the open milestone with the nearest due date, so `issue-triage` pass 1 boards it in `Todo`. With no dated milestone open it is filed unmilestoned and boarded in `Incoming`, like any unapproved issue. Boarding from the workflows waits on a GitHub App credential (#5061); do not add board-write code that reads a secret that does not exist. - **A sweep's marker counts only on an issue the automation wrote**: authored by `github-actions` and labeled `chore` + `dependencies`. The repository is public, so a marker alone proves nothing. `scripts/lib/sweep.mjs` holds the rule; every sweep goes through it. - **Every sweep takes `--dry-run`**, which reads what it must and writes nothing, printing each issue, edit and comment it would make with its labels and milestone. Preview a sweep that way, never against the live tracker. Live filing is covered by the script tests, with `gh` faked. - **Scheduled workflows run only from `main`.** A change to a sweep takes effect at the next milestone merge, and each sweep checks `v2/main` out explicitly. - **Alerts are computed from `main`.** The alert sweep re-checks each range against `v2/main`'s lockfile before filing. A vulnerable dependency added on `v2/main` and not yet merged produces no alert at all. - **An SDK package a manifest declares must be in `SDK_GROUPS`** (`scripts/sdk-watch.mjs`), or the SDK watch fails rather than leave it unwatched. - **No model runs in a write-capable job.** `sdk-watch.yml`'s `analyze` job is `contents: read` only, its model gets `--tools "Read,Grep,Glob"` and no Bash at all, the release notes are fetched by a deterministic step, and the analysis is scanned for credentials before it is written out and again before it is posted. `sdk-watch.test.mjs` pins each of these; keep them when editing the job. - **The analysis job's residual credential channel is an accepted risk, for MCP-org upstreams only** (#5069). The model can read `ANTHROPIC_API_KEY`, and its write-up is published, so an encoded key would pass the scans. The maintainers accept that because every SDK the watch reads is an MCP-org SDK. **An SDK group outside the `modelcontextprotocol` org needs that decision taken again first** (federation, or a spend-capped CI key), and `sdk-watch.test.mjs` fails until it is. ## Project status and direction | Branch | Role | PRs target it? | | --------- | ------------------------------------------------------------------------------ | -------------------------------------------------------- | | `v2/main` | **Develop.** All active work lands here. | **Yes**, every PR | | `main` | **Release.** The default branch, and what users see. Not a development branch. | **No**, it only receives milestone merges from `v2/main` | Cut feature branches from **`origin/v2/main`**. **Never open a PR against `main`**, with one exception: a milestone's merge PR, which the `release` skill opens. There is no `v1` line in this repository. **`v2/main` changes only through a merged PR.** Its ruleset, `v2/main: PRs only`, refuses deletion, force-pushes and direct pushes, and has no bypass actors. It requires no approvals, so a maintainer can merge their own PR. No automation pushes to `v2/main`: the Version Packages and Prepare Python Release workflows push branches of their own and open PRs. A workflow that needs to push to `v2/main` directly is a change to this rule and its ruleset, decided first. The `main` guard (**Source is v2/main**) trusts `v2/main`'s history, so this ruleset is what makes that trust hold. - **Repo**: https://github.com/modelcontextprotocol/servers - **Project board**: [Servers V2 (#43)](https://github.com/orgs/modelcontextprotocol/projects/43) Versioning and publishing are described in [`RELEASING.md`](./RELEASING.md). Publishing is a deliberate maintainer action (`release.yml` runs only when a maintainer publishes a GitHub Release), never a side effect of a merge. ### Versions - **A version changes only in a version PR, never by hand and never at release time.** The TypeScript servers are semver, bumped by the changesets "Version Packages" PR; the Python servers are CalVer, stamped by the "Prepare Python Release" PR. No workflow computes a version, and a release publishes whatever version it finds that the registry does not have. - **A PR that changes what a TypeScript server publishes carries a changeset** (`npm run changeset`): patch for a fix; minor for a new tool, prompt, resource or option; major for a breaking change (a tool removed or renamed, a schema change that breaks clients, a protocol or Node floor bump). A change with no effect on a published TypeScript package (docs, skills, workflows, scripts, tests, a Python server) carries none. - **A TypeScript server reports its version from its `package.json`** (the `SERVER_VERSION` its `version.ts` exports), never from a literal in the source: the version PR edits `package.json` only. ## Contributing What the repository accepts is in [`CONTRIBUTING.md`](./CONTRIBUTING.md). Read it rather than a summary. In short: bug fixes, usability improvements and changes that demonstrate MCP protocol features (Resources, Prompts, Roots, not just Tools) are welcome; other new features are selective; **new server implementations are not accepted** (they belong in the [MCP Server Registry](https://github.com/modelcontextprotocol/registry)), and neither are README server-listing changes. **Pull requests are opened by the repository maintainers.** Anyone else, org members with write access included, files a detailed issue instead: the problem, how to reproduce it, the expected behavior, and, if a fix was prototyped, the prompt that produced it rather than a diff. An agent working for someone who is not a maintainer produces that issue, not a PR. The maintainers decided this on #4861. `CONTRIBUTING.md`, the PR template's banner and the issue forms in `.github/ISSUE_TEMPLATE/` all say so; a change to the policy changes all of them together. - **An outside PR is closed, not reviewed or merged**, with a pointer to the issue flow. If it holds a fix worth keeping, file an issue for it first and credit the PR's author there. How the open backlog of outside PRs is worked, and the comment to close them with, is in [`docs/contribution-model.md`](./docs/contribution-model.md). - **Issue forms are served from the default branch** (`main`), so a change under `.github/ISSUE_TEMPLATE/` goes live at the next milestone merge, not when its PR merges to `v2/main`. The server dropdown in each form does not label the issue; triage applies the `server-` scope label from it. **Every commit is signed off** under the [Developer Certificate of Origin](https://developercertificate.org/) (adopted on #4861): commit with `git commit -s`, so the message ends in a `Signed-off-by: Name ` trailer whose name and email match the commit's author or committer. Merge commits and bot-authored commits are exempt. `verify:dco` checks it in the pre-push gate, and the **DCO signoff** job (`.github/workflows/dco.yml`) checks every commit a pull request adds; one unsigned commit fails it. The repair (`git rebase --signoff`) is in the `pr-flow` skill. **Every PR references an issue.** The PR body's first line is `Closes #`. A PR with no linked issue has no board card, so the work is invisible to the board. If there is no issue yet, create it first. This holds for a maintainer's own one-line fix as much as for a feature. - **Exception: a security advisory's fix.** It is written and reviewed in a PR in the advisory's temporary private fork (`servers-ghsa-xxxx-yyyy-zzzz`), which links no public issue, carries no labels, and is tracked by the advisory's `[GHSA-…]` draft card rather than an issue's card, because each of those would disclose the vulnerability before a fix ships. For the same reason the work-start rules on assigning an issue and naming the branch `v2//-` do not apply: its branch is `v2/fix/`, with **no descriptive slug**. It targets `v2/main`, and it is **not merged**. When the fix is ready to merge, close to the release, the draft card converts to an issue that keeps its opaque title, and that branch is pushed here under the same name and merged through a **public PR** that `Closes` that issue and follows every rule above, except that its title and body describe no part of the vulnerability and it gets **no Copilot review loop**: review comments on a public PR are public, and the review already happened in the fork. The fork's PR is then closed. The flow is the `security-advisory` skill. - **Exception: a milestone's merge PR.** It is the one PR that targets `main`. It is a pure merge of `v2/main` (its branch is `origin/v2/main` with no commits of its own, and the merged tree equals `origin/v2/main`'s), and its body's first line is **`Part of #N`** for the milestone's release issue, not `Closes #N`: on the default branch a closing keyword would close that issue at merge, before anything is published. The release issue is closed by hand once the Release is published. In place of the checklist it links the **release ledger**. It is merged with a merge commit, never squashed. Both are enforced on `main`: its ruleset allows only merge commits, and requires the **Source is v2/main** check (`main-source.yml`), which fails a PR whose head carries any commit not already on `v2/main`. That ruleset has no bypass actors; a release that cannot be a pure merge (a release branch, a hotfix) goes through only by a repo admin disabling it for that one merge and re-enabling it at once, which is never automated. The flow is the `release` skill. - **Exception: the two version PRs that automation opens.** Each changes version metadata only, so neither answers the PR checklist below, and each is tied to its milestone's **release issue** rather than to an issue of its own. - The **Prepare Python Release** PR follows the rest of the rules: the workflow takes the release issue's number, writes `Closes #N` as the body's first line, names the branch `v2/chore/-python-calver-` and labels it `v2`. - The changesets **"Version Packages"** PR cannot keep a `Closes` line: the action rewrites its body on every push to `v2/main`, and its branch is the action's own (`changeset-release/v2/main`). The workflow labels it `v2`. It is still **linked** to the release issue like every other PR: with `addCloseIssueReferences` as soon as it is found, since the link lives outside the body and survives the rewrites (on `v2/main` it closes nothing at merge). Its body's first line becomes **`Part of #N` immediately before merging**, after the last regeneration, and is checked to still be there at merge. A `Part of #N` **comment** stays as the durable textual record, since an update does not overwrite it. No other PR is exempt. Every PR answers the PR checklist in its body: the MCP documentation was read for the feature touched, the change follows MCP security best practices, the server's README is updated, and a **server-facing change was tested with an LLM client**, with the scenarios tested named under the body's "How Has This Been Tested?" heading. The body's sections and the full checklist are in the `pr-flow` skill. [`.github/pull_request_template.md`](./.github/pull_request_template.md) is only the "issues, not PRs" banner; it does not carry them. ## Maintenance rules - When adding, removing, renaming, or changing the purpose of a file or folder, update the entry that describes it: the root `README.md`, the server's own `README.md`, and the tree in this file. - A change to a server's tools, resources, prompts, configuration or environment variables updates **that server's `README.md`** in the same PR. - A change to how packages are versioned or published updates [`RELEASING.md`](./RELEASING.md). - When a rule changes, update this file. When a procedure changes, update the skill that owns it, not this file. Link to a document rather than copying it: two copies of a command sequence or an ID are worse than one, because the stale copy is indistinguishable from the live one. ### Maintaining the skills Skills are conditional: a skill's body loads only when it is invoked, so a skill that stops being reachable loses behavior **silently**. Four rules keep that from happening. How to write a description that fires, and eval cases that measure it, is [`docs/skill-authoring.md`](./docs/skill-authoring.md). 1. **`npm run verify:skills` must pass.** It runs inside `validate:guards`, and CI runs it with `verify:skills:cli` (the authoritative `claude plugin validate`, at a pinned CLI version) on every push and pull request. It parses each `SKILL.md`'s frontmatter the way Claude Code does. Malformed YAML loads the body with an _empty_ description, so `/name` still works while the skill can never auto-fire; an unquoted `#` truncates the description silently. **Quote any description containing `#` or `:`**, and keep the opening `---` on the file's first line. **A body holds no `$0`–`$9` and no `$ARGUMENTS`**: Claude Code substitutes them when a skill is invoked with arguments, so a recipe's shell parameters or awk fields reach the model rewritten. 2. **Every skill declares `disable-model-invocation` explicitly. Default it to `false`.** Invoking a skill has no side effects (it loads instructions), and work is usually asked for in prose, which a `true` skill can never answer. A `true` skill also cannot be reached from another skill's body. Reserve `true` for a procedure only ever started deliberately by name. 3. **A model-invoked skill carries committed eval cases** at `.claude/skills//evals/evals.json`: at least five positives and at least one negative. `npm run skills:eval` runs them (it needs the `claude` CLI, or `copilot` with `AGENT=copilot`, and spends real model calls, so it is **not** in the gate). Run the **whole** suite when adding a skill or editing a description, since a new skill can lower the trigger rate of the others, and aim each case at what only the skill holds, never at something this file already answers. 4. **Keep the listing inside its budget.** Claude Code truncates the skill listing when it overflows, dropping the least-invoked entries first. `verify:skills` prints this repo's share against the budget in `scripts/lib/skill-manifest.mjs` and fails when it is exceeded. Put the key use case first in each description. A rule that must never drop out of context stays in this file: in a long session, auto-compaction can drop an older skill's body entirely. ## Issue-driven work style All work is driven by items on the Servers V2 board (#43). The _recipes_ for the flows below are in the `issue-create` and `board-ops` skills; the rules are here. - **Before starting work, check the board for the relevant item.** - **Every board item is a real GitHub issue.** No draft cards. Before creating an issue, search for a matching one in every state; **never create a duplicate**. - **The one exception is a private security advisory**, tracked by a **draft card** titled with the bare id, `[GHSA-xxxx-yyyy-zzzz]`, because a real issue would disclose the vulnerability before a fix exists. The title stays that opaque until publication: board access is wider than advisory access, and a summary can name the server or the attack. It converts to an issue **when its fix is ready to merge**, keeping that opaque title, so the public PR that brings the fix to `v2/main` has an issue to close; the summary joins the title only **once the advisory is published**, never merely once the fix ships. Work the fix leaves open is tracked by a second draft card, `[GHSA-xxxx-yyyy-zzzz follow-up]`, under the same rules, except that it converts only once the advisory is published, since its write-up describes the gap. It stays open after conversion, because its work has not shipped. The `[GHSA-` prefix is load-bearing: **any other draft card is still a defect to delete**. The flow is the `security-advisory` skill. **Accepting, closing and publishing an advisory, and replying to its reporter, are human-only; never automate or bulk-apply any of them.** - **Only issues go on the board, never PRs.** A PR is tracked through its linked issue's card. - **Label every issue and every PR `v2`**, at create time. It marks work tracked by this workflow. - **Label every issue with exactly one type label**: `bug`, `enhancement`, `documentation`, `chore` or `question`. A PR needs no type label; it is classified through the issue it closes. - **Label an issue that concerns one server with its scope label** (`server-everything`, `server-filesystem`, `server-memory`, `server-sequentialthinking`, `server-fetch`, `server-git`, `server-time`). An issue that concerns the repository as a whole carries none. - **Every issue you create gets a milestone.** Milestones are release buckets, so pick by when the work ships. The one exception: an issue that arrives unboarded stays unmilestoned in `Incoming` until a maintainer approves it. - **Every board item has a Priority** (a board field, not a label). - **`Incoming` ⇔ no milestone; every Status past it ⇔ milestoned.** Assigning the milestone _is_ the approval act, so the two go together. `Todo` asserts a maintainer signed off, so never park an unreviewed issue there. An issue you create through the normal flow skips `Incoming`, because filing it was the approval. A `[GHSA-` advisory draft is exempt: a draft card cannot carry a milestone, so its approval act is **accepting the advisory**, which moves it `Incoming` → `Todo`. Its milestone arrives when the card converts to an issue, before the fix's public PR opens. - **`Done` means the work shipped**: its PR merged, or it is a parent whose last sub-issue closed. A duplicate, won't-fix, not-planned or superseded issue shipped nothing, so its card is **deleted** rather than moved to Done. - **All work happens in a git worktree freshly cut from `origin/v2/main`, and the agent session runs inside it.** Not only PR branches: release verification, audits, triage, investigation and anything else done in a checkout. `git fetch origin` first, then `git worktree add` from `origin/v2/main`: a new branch for PR work, `--detach` for work that commits nothing. Never work in the primary checkout, which concurrent agent sessions share, and never in an existing worktree without first confirming it is at the current `origin/v2/main`. - **Start the session in that worktree.** Claude Code loads this file and `.claude/skills/` from the directory the session starts in, once, at session start; a worktree created later does not change what was loaded. A session started in a stale checkout follows stale rules for its whole run, however fresh the branches it cuts are (the v2.0.0 release did, #5167). A session that finds itself elsewhere checks before doing anything: ```sh git fetch origin v2/main git diff --quiet HEAD...origin/v2/main -- ':/AGENTS.md' ':/.claude/' \ || echo "STALE: instructions differ from origin/v2/main" ``` The three dots compare against where `HEAD` branched from `v2/main`, so a branch's own edits to the instructions do not read as stale; the `:/` anchors the paths at the repository root, so the check means the same from any directory. **If it is stale, stop and ask for a session started in a fresh worktree**, rather than continuing on rules you cannot trust. - The `SessionStart` hook in `.claude/settings.json` (`scripts/session-instructions-check.mjs`) runs that check at every session start and prints its verdict into the session, with a note when the session started in the primary checkout. A `STALE` line from it is the same stop. - **A long session re-checks after `v2/main` moves under it**, for example after its own PRs merge, **from the revision it started at, not from `HEAD`**: once the session moves into a newer worktree, that worktree's `HEAD` can match `origin/v2/main` while the rules the session holds are still the old ones. The hook prints the session-start revision and the exact re-check command; without the hook, record `git rev-parse HEAD` in the starting checkout before anything else, and put it in place of `HEAD` above. - **When work begins**, assign the issue to yourself, set Status to **In Progress**, and create a branch named `v2//-` (for example `v2/fix/4810-filesystem-symlink-escape`) from `origin/v2/main`, in a new worktree under the rule above. Remove the worktree once the PR merges or is closed. The recipe is the `pr-flow` skill (step 2, and step 10 for the removal). - **When work is complete**, run the checks under [Before pushing](#before-pushing), open a PR against `v2/main` labeled `v2` with `Closes #` as the body's first line, and set Status to **In Review**. - ⚠️ Closing keywords only auto-link and auto-close for PRs targeting the default branch (`main`). A PR against `v2/main` gets a cross-reference only, so **link it explicitly** with the `addCloseIssueReferences` GraphQL mutation right after opening it. **On merge, close the issue by hand and move its card to Done.** Keep the `Closes` line anyway. - **After opening a PR, run a Copilot review loop to convergence, unprompted** (except a security advisory's public fix PR; see [Contributing](#contributing)). Request a review, wait for the round to post or for Copilot's session to end without one, answer every comment, and request again only when you pushed a fix or the last round ended without a review. Stop on the **first** clean round (no confirming round), a round holding only out-of-scope findings, or two rounds in a row that end without a review. **There is no round cap**: a loop ends for one of those reasons, never because of how many rounds it has run. The recipe is the `pr-flow` skill. - ⚠️ **Copilot does not converge on its own.** Every fix it talks you into beyond the issue is fresh surface for the next round, so accepting scope creep is what makes a review cycle protracted. **Weigh each finding against the issue the PR closes and decline scope expansion**: pre-existing behavior, new capabilities, and hardening the issue did not ask for. A declined finding that is worth doing gets its own issue, linked in the reply. The rule is under [Responding to code reviews](#responding-to-code-reviews). - **If new tasks are discovered during development, create issues** for them and put them on the board, rather than widening the current PR. ## Responding to code reviews - It is not necessary to implement every suggestion. - **Judge each suggestion against the issue the PR closes.** Fix defects in what the PR added. Decline, with a reason, suggestions that expand the PR beyond what the issue calls for (pre-existing behavior, new capabilities, hardening the issue did not ask for), and file an issue for any that is worth doing on its own. - You may implement a suggestion differently, or skip one for a good reason. - **Reply to each review comment in its own thread**, saying what was done or why it was not. A rollup comment elsewhere cannot be connected back to the thread it answers, so the thread stays open showing a finding and no reply. Then post a PR-level summary **in addition**, because inline replies go hidden once the fix is pushed. A finding in a review's "Suppressed comments" block has no thread, so the summary is the only place to answer it. ## Always test new or modified code - **New or changed behavior comes with tests.** TypeScript servers use **vitest** (with `@vitest/coverage-v8`), with tests in the server's `__tests__/` directory. Python servers use **pytest** (with `pytest-asyncio`), with tests in the server's `tests/` directory. - **Coverage is gated per file.** The rule for each language: - **TypeScript**: every file in a server clears **≥ 90 on all four dimensions** (lines, statements, functions and branches). The thresholds are set in each server's `vitest.config.ts` (`perFile: true`). A PR that drops any file below 90 on any dimension fails CI (`typescript.yml` → **Coverage \**) and `npm run local:gate`. - **Python**: every file clears **≥ 90 on lines and on branches**, the two dimensions coverage.py measures (it has no native function dimension). A PR that drops any Python file below 90 on either fails CI (`python.yml` → **Coverage \**) and `npm run local:gate`. - **A genuinely unreachable branch is annotated at the source, never waved through by lowering the gate.** Every ignore carries its reason: `/* v8 ignore next -- */` in TypeScript, `# pragma: no cover # ` in Python. Reach for one only when the code cannot be exercised (a defensive guard for a value the types already rule out, say), never because a test is hard to write. - **Coverage is its own command, separate from the fast test loop.** `npm run coverage -w src/` (or the root `npm run coverage` for every TypeScript server) and `npm run coverage:py` (`-- ` for one) run the suites instrumented and enforce the gate; `test`, `validate` and `uv run pytest` stay fast and do not. CI and `npm run local:gate` run each as a stage of its own. - **Test at the protocol level where you can**: drive the server through an MCP client over an in-memory transport and assert on what comes back, rather than only calling internal functions. - **In tests that expect error output, suppress it from the console.** ## Waiting on long-running work **When you are waiting for something to finish, arm a notifier and stop. Never spend turns polling.** The harness re-invokes you when a backgrounded task exits, so a per-turn `echo`, `tail` or `grep` of a log delivers nothing the notification would not, burns turns, and buries the result under no-op turns. - **One notification ("tell me when this finishes")**: background the command itself, or a single loop that exits on the condition. - **Many ("tell me on each occurrence")**: a monitor over a stream that emits one line per event. - **State the harness cannot observe** (a CI run, a remote review queue) does need polling, but inside **one backgrounded loop that exits when the condition holds**, at an interval matched to how fast the state changes (30s or more for a remote API). - ⚠️ **If a notifier is already armed, wait for it.** Re-checking by hand alongside it is the polling this rule forbids. ## Pacing GitHub writes **Pace bulk GitHub writes: 1 s between requests, never in parallel, under 80 a minute and 500 an hour.** The secondary limit refuses while `gh api rate_limit` still shows quota, and most `gh` writes are GraphQL. After a refusal, wait out `retry-after`, or a minute without one; a refused create can still land, so look before retrying. ## MCP protocol - **Look protocol questions up; do not answer them from memory.** The repo configures the `mcp-docs` MCP server (`.mcp.json`, `https://modelcontextprotocol.io/mcp`). For schema details use the versioned schemas (JSON and TypeScript) in [modelcontextprotocol/schema](https://github.com/modelcontextprotocol/modelcontextprotocol/tree/main/schema). - **Protocol eras**: every TypeScript server serves both 2026-07-28 clients and 2025-era clients from one factory: stdio through the SDK's `serveStdio`, Streamable HTTP through `createMcpHandler`. Keep no state in a map keyed by session id, since 2026-07-28 has no sessions: per-connection state lives on the server instance, and state that must outlive a 2026-07-28 request lives under a handle the server mints. Ask a client for input (elicitation, sampling, roots) by returning `inputRequired(...)`, which the SDK's legacy shim also serves to 2025-era clients. - **Transports**: stdio is the default and every server supports it. `everything` also serves Streamable HTTP and HTTP+SSE. **HTTP+SSE is deprecated**: keep what exists working, and never make it the default or the recommended option in new code or docs. - **Tool annotations**: every tool you add or change sets `readOnlyHint`, `idempotentHint` and `destructiveHint` to values that match what it actually does. - **Tool names**: verb-first (`get-file-info`, not `file-info`). A new tool follows the naming convention of the server it joins: kebab-case in `everything`, snake_case in the others. **Never rename a published tool**: a tool name is part of the server's interface, and renaming it breaks every client configuration and prompt that uses it. - **Resources and prompts** registered in `everything` are kebab-case too. - **Registration**: in `everything`, each feature area exports a `registerTools(server)` / `registerResources(server)` / `registerPrompts(server)` function and is wired into that area's `index.ts`. The other TypeScript servers register directly on the server (`server.registerTool(...)`). Follow the pattern of the server you are in. ## TypeScript instructions - Use TypeScript for all new code, with strict typing for every function and variable. - **ES modules**: relative imports carry the `.js` extension (`import { x } from "./lib.js"`). - **Validate tool input with Zod schemas**, with descriptions on the fields. - **Naming**: camelCase for variables and functions, PascalCase for types and classes, UPPER_CASE for constants, kebab-case for new file names. - **Imports** at the top of the file, external packages first, then internal modules. - Prefer `async`/`await` over callbacks and raw promise chains. - **NEVER use `any` as a type.** - **NEVER suppress errors in the TypeScript configuration or with a lint directive** as a way of satisfying the compiler. - **AVOID double casts (`as unknown as T`).** Prefer a type guard, a narrower single cast, or fixing the underlying type. When one is genuinely unavoidable (a documented gap in a third-party type, for example), it carries an inline comment saying why it is safe and why nothing better works. - **NEVER leave a promise floating.** Every promise is awaited, returned, terminated with `.catch(…)`, or explicitly discarded with `void` plus a one-line comment saying why the caller cannot await it. A floated call reads like an awaited one minus a keyword, and its unhandled rejection surfaces somewhere else entirely. - **Clean up** timers, intervals, subscriptions and child processes when the server shuts down or a transport closes. - Handle errors with `try`/`catch` and return clear error messages to the client, as an MCP error result where the protocol calls for one. ## Python instructions - **Type hints everywhere, and pyright-clean**: CI runs `uv run --frozen pyright` on every Python server. - **Lint with `ruff`** (`uv run ruff check .`). - Follow the server's existing module layout (`src//src/mcp_server_/`) and its async style; test async code with `pytest-asyncio` where the server already uses it. - Manage dependencies with `uv` and commit the updated `uv.lock` (see [Dependencies](#dependencies)). - Builds use **hatchling** through `uv build`; do not add another build backend.